- Python 51.2%
- HTML 44.6%
- JavaScript 2.6%
- Dockerfile 0.6%
- Nix 0.6%
- Other 0.4%
|
Some checks failed
CI/CD Workflow / Test Backend (Python) (push) Successful in 35s
CI/CD Workflow / Test Frontend (Node.js) (push) Successful in 10s
CI/CD Workflow / Build and Publish Docker Image (push) Failing after 8s
CI/CD Workflow / Trigger Dockhand Deployment (push) Has been skipped
|
||
|---|---|---|
| .forgejo/workflows | ||
| .opencode | ||
| .sisyphus/plans | ||
| backend | ||
| data | ||
| frontend | ||
| mcp-server | ||
| n8n | ||
| test_reports | ||
| traefik | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.override.yml | ||
| docker-compose.yml | ||
| flake.lock | ||
| flake.nix | ||
| import_data.py | ||
| pytest.ini | ||
| README.md | ||
PCM - Podcast Content Manager
A lightweight web app for managing podcast episode research — collecting news links, assigning them to episodes, and archiving after broadcast.
Features
- Inbox → Planned → Archived workflow for news items
- Episode management with episode numbers and air dates
- Tagging & Topics for categorizing news with colors
- One-click archive when an episode airs (archives all assigned news)
- Responsive UI with Tailwind CSS card layout
- OAuth2 authentication (Google, GitHub, OIDC providers)
- Multi-user support with isolated data
- API tokens for programmatic access
- MCP server for AI assistant integration
- Incoming links with webhook processing
Tech Stack
- Backend: Python 3.11, FastAPI, SQLite
- Frontend: Vanilla JS, Tailwind CSS
- Deployment: Docker Compose / Kubernetes
Quick Start
docker compose up -d --build
- Frontend: http://localhost:8080
- Backend API: http://localhost:8000/docs (Swagger UI)
Data is persisted in ./data/pcm.db (SQLite).
Configuration
Copy .env.example to .env and configure:
cp .env.example .env
Environment Variables
| Variable | Default | Description |
|---|---|---|
HOST |
(required for Traefik) | Your domain, e.g. pcm.example.com |
TRAEFIK_EXTERNAL |
false |
Set true to join existing Traefik network |
TRAEFIK_NETWORK |
pcm-network |
Docker network name |
CERT_RESOLVER |
hetzner |
Traefik certificate resolver name |
API_BASE_URL |
/api |
Path prefix for the API |
DATABASE_PATH |
/app/data/pcm.db |
SQLite database path |
BACKEND_URL |
http://localhost:8000 |
Backend URL for CORS and OAuth |
FRONTEND_URL |
http://localhost:8080 |
Frontend URL for OAuth redirect |
CORS_ORIGINS |
http://localhost:8080 |
Comma-separated allowed origins |
JWT_SECRET_KEY |
(auto-generated) | Secret for JWT tokens |
JWT_EXPIRE_DAYS |
7 |
JWT token expiration |
SECURE_COOKIES |
true |
Require HTTPS for cookies |
OAuth Providers
Configure at least one provider to enable login:
Google OAuth
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
Get credentials at: https://console.cloud.google.com/apis/credentials
GitHub OAuth
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
Get credentials at: https://github.com/settings/developers
Generic OIDC (Keycloak, Authentik, etc.)
OIDC_ISSUER=https://your-idp.example.com
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_NAME=Keycloak
Deployment
Traefik (Automated HTTPS)
Deploy behind an existing Traefik reverse proxy. Containers join Traefik's Docker network and are auto-discovered.
Prerequisites on the host:
- Traefik is running with its Docker provider enabled
- The network Traefik uses is named
pcm-network
If your Traefik uses a different network name, either rename it:
docker network create pcm-network
docker network connect pcm-network <your-traefik-container>
Or adjust your Traefik config to use pcm-network.
Deploy:
# Start with Traefik integration
TRAEFIK_EXTERNAL=true HOST=pcm.example.com docker compose up -d --build
Traefik labels on the containers handle routing, TLS, and certificate provisioning via your existing DNS challenge resolver.
Docker Compose Services
| Service | Port | Description |
|---|---|---|
backend |
8000 | FastAPI backend with health check |
frontend |
80 | Static HTML/JS served by nginx |
mcp-server |
(disabled) | MCP server for AI assistants (enable with --profile mcp) |
MCP Server (AI Integration)
The MCP server allows AI assistants (Claude Desktop, etc.) to add news items directly to PCM.
Enable MCP server:
docker compose --profile mcp up -d
Configure in Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"pcm": {
"command": "docker",
"args": ["exec", "-i", "pcm-mcp", "python3", "/app/server.py"],
"env": {
"DB_PATH": "/app/data/pcm.db"
}
}
}
}
See mcp-server/README.md for full documentation.
API Documentation
All endpoints (except /health) require authentication via session cookie or API token.
Authentication
- Session cookie: Login via
/api/auth/login/{provider}→ setssession_tokencookie - API Token: Include header
Authorization: Bearer <token>(for programmatic access)
Health Check
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Database health check (no auth required) |
Episodes
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/episodes |
List all episodes with news count |
| POST | /api/episodes |
Create episode {number, air_date, status} |
| PUT | /api/episodes/{id} |
Update episode {air_date, status} |
| DELETE | /api/episodes/{id} |
Delete episode (moves news to inbox) |
| POST | /api/episodes/{id}/archive |
Mark aired + archive all planned news |
News Items
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/news |
List news items (filters: status, episode_id) |
| POST | /api/news |
Create news item |
| PUT | /api/news/{id} |
Update news fields |
| POST | /api/news/{id}/assign/{ep_id} |
Assign to episode (status → planned) |
| DELETE | /api/news/{id} |
Hard delete |
Tags
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tags |
List all tags |
| POST | /api/tags |
Create tag {name} |
| PUT | /api/tags/{id} |
Update tag name (updates all news items) |
| DELETE | /api/tags/{id} |
Delete tag (removes from all news items) |
Topics (Labels)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/topics |
List all topics with colors |
| POST | /api/topics |
Create topic {name, color} |
| PUT | /api/topics/{id} |
Update topic {name, color} |
| DELETE | /api/topics/{id} |
Delete topic (fails if in use) |
| GET | /api/labels |
List topic names only (alias) |
API Tokens
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tokens |
List user's API tokens |
| POST | /api/tokens |
Create token {name, scopes, expires_in_days} |
| DELETE | /api/tokens/{id} |
Revoke token |
Scopes: read, write (default: both)
Incoming Links
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/links |
List incoming links (filter: status) |
| POST | /api/links |
Submit URL for processing {url, title, summary, highlights} |
| GET | /api/links/{id} |
Get link details |
| DELETE | /api/links/{id} |
Delete link |
| POST | /api/links/{id}/process |
Trigger processing |
| POST | /api/links/{id}/retry |
Retry failed processing |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/webhooks |
List webhook configs |
| POST | /api/webhooks |
Create webhook {url, name} |
| PUT | /api/webhooks/{id} |
Update webhook {url, name, is_active} |
| DELETE | /api/webhooks/{id} |
Delete webhook |
| POST | /api/webhooks/callback |
AI agent callback (creates news from processed link) |
Database Schema
-- Users (OAuth accounts)
users (id, email, name, provider, provider_user_id, picture, created_at, last_login_at)
-- Episodes
episodes (id, number, air_date, status)
-- News items
news_items (id, url, title, summary, highlights, status, tags, label, episode_id, created_at)
-- Topics/Labels with colors
topics (id, name, color)
-- Tags
tags (id, name)
-- API tokens
api_tokens (id, name, token_hash, scopes, expires_at, last_used_at)
-- Incoming links queue
incoming_links (id, url, title, summary, highlights, status, error_message, processed_at)
-- Webhook configs
webhook_configs (id, url, name, is_active)
Kubernetes Deployment Example
apiVersion: apps/v1
kind: Deployment
metadata:
name: pcm
namespace: pcm
spec:
replicas: 1
strategy:
type: Recreate # SQLite = RWO volume
selector:
matchLabels:
app: pcm
template:
metadata:
labels:
app: pcm
spec:
containers:
- name: backend
image: ghcr.io/kreativmonkey/pcm-backend:latest
ports:
- containerPort: 8000
volumeMounts:
- name: data
mountPath: /app/data
- name: frontend
image: ghcr.io/kreativmonkey/pcm-frontend:latest
ports:
- containerPort: 80
volumes:
- name: data
persistentVolumeClaim:
claimName: pcm-data
---
apiVersion: v1
kind: Service
metadata:
name: pcm
namespace: pcm
spec:
selector:
app: pcm
ports:
- name: api
port: 8000
targetPort: 8000
- name: web
port: 80
targetPort: 80
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pcm-data
namespace: pcm
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
Note: Use
strategy: Recreatebecause SQLite needs exclusive RWO access. For production, consider switching to PostgreSQL.
Future Plans
- Drag & drop between columns
- RSS feed import
- Export to show notes (Markdown)
- Browser Add-On
- Teams integration