A professional-grade web interface for managing IPTV configurations with Dispatcharr. Built with React + TypeScript and Python FastAPI.
ECM gives you full control over your IPTV setup: manage M3U accounts and EPG sources, create and organize channels with drag-and-drop, automate channel creation with a powerful rules engine, probe stream health, build FFmpeg commands visually, and monitor live streaming stats — all from a single interface.
services:
ecm:
image: ghcr.io/motwakorb/enhancedchannelmanager:latest
ports:
- "6100:6100" # HTTP (configurable via ECM_PORT)
- "6143:6143" # HTTPS (configurable via ECM_HTTPS_PORT)
volumes:
- ./config:/config
environment:
- PUID=1000
- PGID=1000
- ECM_PORT=6100
- ECM_HTTPS_PORT=6143
# The image ships a HEALTHCHECK with `start_period=120s` baked in
# (bd-ul0ah). On long-running installs the first-run migrations can
# run against a bloated SQLite WAL file and take >30s — Docker's
# default start_period would mark the container unhealthy before
# migrations finish. If you need to override, set the healthcheck
# block here; operators on consistently fast installs can lower it.That's it. Open http://localhost:6100 and the setup wizard will guide you through creating an admin account and connecting to Dispatcharr.
To add the optional MCP server for managing ECM through Claude, add the MCP service to your compose file:
services:
ecm:
image: ghcr.io/motwakorb/enhancedchannelmanager:latest
ports:
- "6100:6100"
- "6143:6143"
volumes:
- ./config:/config
environment:
- PUID=1000
- PGID=1000
ecm-mcp:
image: ghcr.io/motwakorb/enhancedchannelmanager-mcp:latest
ports:
- "6101:6101"
volumes:
- ./config:/config:ro
environment:
- ECM_URL=http://ecm:6100
- MCP_PORT=6101
depends_on:
ecm:
condition: service_healthyOr if you're building from source, use the MCP compose overlay:
docker compose -f docker-compose.yml -f docker-compose.mcp.yml up -dReaching the MCP container from ECM — ECM's Settings > MCP Integration status badge probes the MCP server's /health endpoint. By default it targets ecm-mcp:6101, which Docker DNS resolves to the MCP container on the canonical compose network — no extra configuration needed. If you run both containers with network_mode: host (host network namespace shared), set MCP_HOST=localhost on the ECM service so the probe targets the host loopback instead of the (non-existent on that topology) ecm-mcp DNS name.
Reaching ECM from the MCP container — the symmetric case. The MCP server calls ECM's backend API at its ECM_URL (default http://ecm:6100, Docker DNS on the canonical compose network). If both containers run with network_mode: host, the ecm service name has no DNS entry on the shared host network and the backend answers only on the host loopback, so every MCP tool call fails with All connection attempts failed. Fix: set ECM_URL=http://localhost:6100 on the MCP service. See MCP integration troubleshooting.
Upgrade note (v0.17.1-0066+): If you previously ran ECM and ecm-mcp with
network_mode: hostand never setMCP_HOST, you need to act. Earlier versions hardcodedlocalhostas the probe target; v0.17.1-0064 changed the default toecm-mcp(the canonical compose service name). On a host-networking deploy,ecm-mcpdoes not resolve — so after pullingdev, your Settings > MCP Integration badge will show "MCP server not reachable" even when MCP is healthy. Fix: addMCP_HOST=localhostto the ECM service'senvironmentblock in your compose file and restart the container.
See MCP Server (Claude Integration) for setup instructions.
User / Group Identifiers:
- PUID (default: 1000) — User ID the application runs as
- PGID (default: 1000) — Group ID the application runs as
Set these to match the owner of your bind-mounted volumes to avoid permission issues. Find your IDs with id your_user.
Port Configuration:
- ECM_PORT (default: 6100) — HTTP interface (always available as fallback)
- ECM_HTTPS_PORT (default: 6143) — HTTPS interface (when TLS is configured in Settings)
Volumes:
/config— Persistent storage for database, settings, logos, TLS certificates, and backups
ECM supports full backup and restore of all configuration. You can create backups from Settings, or restore from a backup during the first-run setup wizard.
# Frontend
cd frontend && npm install && npm run dev
# Backend
cd backend && pip install -r requirements.txt && uvicorn main:app --reloadFull CRUD for channels and groups with a split-pane layout. Drag-and-drop streams onto channels, reorder streams by priority, bulk-create channels from stream groups with smart name normalization (quality variants, country prefixes, timezone handling), and organize everything into numbered channel groups. Staged edit mode lets you queue changes locally and commit or discard them as a batch.
Manage Standard M3U, XtreamCodes, and HD Homerun accounts. Link related accounts so group enable/disable changes cascade automatically. Track changes detected across M3U refreshes with filtering, search, and optional email digest notifications.
Configure multiple XMLTV and Schedules Direct EPG sources with drag-and-drop priority ordering. Create dummy EPG entries for channels without guide data. Bulk EPG assignment uses country-aware matching, call sign scoring, and HD preference to automatically map EPG data to channels.
EPG grid view with now-playing highlights, date/time navigation, channel profile filtering, and click-to-edit channel metadata.
A rules-based automation engine for channel creation, stream merging, and lifecycle management. Build complex conditions (stream name, group, quality, codec, normalized matching, etc.) with AND/OR logic, then define actions (create channel/group, merge streams, assign metadata, set variables, name transforms). Per-rule normalization group selection lets each rule apply specific normalization groups. Supports dry-run preview, execution rollback, YAML import/export, orphan reconciliation, and a diagnostic debug bundle for troubleshooting.
Visual interface for constructing FFmpeg commands with Simple (three-step IPTV wizard) and Advanced modes. Includes 8 built-in IPTV presets, hardware acceleration support (CUDA, QSV, VAAPI), annotated command preview with tooltips, saved profiles, and direct push to Dispatcharr as stream profiles.
Automated stream probing with configurable schedules, batch sizes, retry logic, and rate limit detection. Profile-aware probing distributes connections across M3U profiles. Results drive smart stream sorting by resolution, bitrate, framerate, video codec, and M3U priority with configurable ordering for deprioritized stream categories. Black screen detection identifies streams showing dark/blank content, and low FPS detection flags streams below a configurable threshold (5/10/15/20 FPS). Both are deprioritized in Smart Sort. A strikeout system tracks consecutive failures for bulk cleanup.
Browse, search, upload, and assign logos to channels. Supports URL import and file upload to Dispatcharr with usage tracking and pagination.
Live dashboard showing active channels, M3U connection counts, per-channel FFmpeg metrics (speed, FPS, bitrate), and bandwidth charts. Enhanced analytics include unique viewer tracking with Dispatcharr user identification, per-channel bandwidth, popularity scoring with trend analysis, and watch history with user attribution.
Activity log tracking all changes to channels, EPG, and M3U accounts with filtering by category, action type, and time range.
Comprehensive configuration including Dispatcharr connection, channel defaults, stream name normalization (tag-based and rule-based engines), stream probing, scheduled tasks (EPG/M3U refresh, probing, cleanup), alert methods (Discord, Telegram, email), authentication (local + Dispatcharr SSO), user management, TLS certificates, VLC integration, appearance themes, and backup/restore.
First-run setup wizard, local auth with bcrypt hashing, Dispatcharr SSO, account linking, email-based password reset, and CLI password reset for lockout recovery. JWT-based sessions with automatic token refresh.
In-app notification bell with history, active task pinning, and external alert methods (Discord webhooks, Telegram bots, SMTP email) with digest batching and source filtering.
ECM includes an MCP (Model Context Protocol) server: an optional sidecar container that exposes ECM's functionality to Claude — Claude Desktop, Claude Code, or any MCP-capable client — so you can manage your install in plain language instead of clicking through the UI. 124 tools across 14 domains (channels, channel groups, M3U accounts, EPG sources, streams, normalization, auto-creation, stats, scheduled tasks, profiles, export, journal, notifications, system), plus an overview resource that gives Claude a one-shot snapshot of your install.
Everything Claude does runs against your live ECM through the API — it's the same operations the UI performs, just driven by conversation. Mutating actions report the resulting state back (e.g. the new channel's group and number) so you can confirm the change took effect.
Things you can ask Claude to do:
Channels & streams
- "List every channel in the Sports group that has fewer than 2 streams"
- "Find duplicate channels and merge each set, keeping the one with the most streams"
- "Add these 8 streams to channel 412 and put the 1080p ones first"
- "Renumber the News group starting at 200"
- "Build a channel lineup from this list of names and fuzzy-match streams to each"
Auto-creation
- "Run the auto-creation pipeline and tell me what it created"
- "Analyze my auto-creation rules and flag anything misconfigured" — the Rule Analyzer catches regex/structural mistakes (
UK|that matches everything,^4Ktyped under Contains that matches nothing, double-escape typos, OR-arms missing a group guard, merges into empty groups) without running the rule - "Create a rule that auto-creates channels for any stream whose name contains 'PPV', into the Events group"
- "Why didn't the 4K Sports rule match anything?" — Claude can pull a debug bundle and analyze it
- "Clear all the channels that auto-creation made in the Test group"
M3U & EPG
- "Refresh all my M3U accounts and report any failures"
- "Which channels are missing an EPG ID?" → "assign tvg_ids to those"
- "Probe every stream in the Movies group and list the dead ones"
Housekeeping & insight
- "Show me this week's watch stats" / "which channels has nobody watched in 30 days?"
- "What scheduled tasks are enabled?" / "run the cleanup task"
- "Back up my config" / "give me an overview of this ECM install"
settings.jsonfield reference
Field in settings.jsonWhat it is for urlDispatcharr base URL dispatcharr_api_keyDispatcharr REST API token — ECM uses this to talk to Dispatcharr. (Canonical field name as of v0.17.1, GH #273. Operators upgrading from v0.17.0 or earlier will have the value in the legacy api_keyfield; ECM auto-migrates on next startup with a one-time[CONFIG] Reading deprecated 'api_key' field …WARN log.) Never replace it with an MCP key.api_keyDEPRECATED legacy alias for dispatcharr_api_key. ECM still reads this for one release of back-compat (v0.17.x). The first read after upgrade emits a deprecation WARN and silently mirrors the value intodispatcharr_api_keyon the next save. Rename or remove this field once you confirmdispatcharr_api_keyis populated.mcp_api_keyECM MCP static key — the ecm-mcpsidecar uses this to authenticate calls to ECM via the?api_key=path. This is what the Generate / Regenerate button in Settings > MCP Integration writes. The?api_key=path is the supported MCP authentication method.When rotating an MCP key, the new key goes in
mcp_api_key. Do not touchdispatcharr_api_key(or its legacyapi_keyalias) — overwriting either with an MCP key breaks every channel and stream operation (ECM returns 401 to Dispatcharr). If you seeapi_key_configured: falsefrom the/healthendpoint after a rotation, the diagnostic'sstatusfield will indicate whethermcp_api_keyis missing from the file (field_missing), blank (field_empty), or the file itself is unreadable (file_not_found/invalid_json) — useGET http://YOUR_ECM_HOST:6100/api/healthto check.Migration example. A v0.17.0
settings.jsonfrom an operator hit by GH #273:{ "url": "http://dispatcharr:9191", "api_key": "real-dispatcharr-rest-token-abc", "mcp_api_key": "ecm-mcp-key-xyz" }After the first v0.17.1 startup and the next settings save, the file becomes:
{ "url": "http://dispatcharr:9191", "dispatcharr_api_key": "real-dispatcharr-rest-token-abc", "api_key": "real-dispatcharr-rest-token-abc", "mcp_api_key": "ecm-mcp-key-xyz" }Both fields hold the same Dispatcharr token (the duplicate is intentional — external scripts that still read
api_keykeep working). Once you've removed any such scripts, you can manually delete the legacyapi_keyline; ECM will keep usingdispatcharr_api_keyfrom then on.
- Generate an API key in ECM Settings > MCP Integration (this writes to
mcp_api_keyinsettings.json) - Start the MCP container — add the
ecm-mcpservice to your compose file (see With MCP Server) and start it on port 6101 - Connect Claude — choose your method:
ECM's MCP server is authenticated with a static API key (mcp_api_key), passed as the ?api_key= query parameter. Both connection methods below run on your machine and connect to ECM over your LAN/VPN — nothing needs to be exposed to the public internet.
| Method | Node.js? | Best for |
|---|---|---|
| Claude Desktop — mcp-remote bridge | Yes (LTS 18+ on the Claude Desktop machine) | Claude Desktop users; private/homelab deploys; existing setups |
Claude Code — .mcp.json |
No | Claude Code in any project; direct HTTP, no Node.js |
✅ Works on a private network — no public exposure. mcp-remote runs on your machine and connects to ECM over your LAN/VPN, so ECM never has to be reachable from the internet (the cost is needing Node.js on the Claude Desktop machine).
Claude Desktop talks to remote MCP servers through the mcp-remote bridge. Add this to your claude_desktop_config.json:
Prerequisite: Claude Desktop does not bundle Node.js. The
mcp-remotebridge is an npm package that Claude Desktop runs vianpx, so you need Node.js installed on the same machine as Claude Desktop (any current LTS — Node 18+ — is fine). Install it from nodejs.org (or via a package manager:winget install OpenJS.NodeJS.LTSon Windows,brew install nodeon macOS,apt install nodejs npmon Debian/Ubuntu). Without Node on PATH, Claude Desktop fails to launch the MCP server with aspawn npx ENOENTerror in its logs.
{
"mcpServers": {
"ecm": {
"command": "npx",
"args": [
"mcp-remote",
"http://YOUR_ECM_HOST:6101/mcp?api_key=YOUR_API_KEY",
"--allow-http"
]
}
}
}(--allow-http is needed because the endpoint is plain HTTP.)
Note: the
?api_key=query parameter in these URLs is yourmcp_api_keyvalue fromsettings.json— the key generated in ECM Settings > MCP Integration. It is not your Dispatcharrapi_key.
✅ Works on a private network — no Node.js, no public exposure. Claude Code speaks the HTTP transport natively and connects directly from your machine, so a LAN/VPN-reachable ECM is all you need. This is the simplest private path if you use Claude Code.
Create a .mcp.json file in any project directory where you want ECM tools available:
{
"mcpServers": {
"ecm": {
"type": "http",
"url": "http://YOUR_ECM_HOST:6101/mcp?api_key=YOUR_API_KEY"
}
}
}To connect:
- Create the
.mcp.jsonfile above in your project root (replaceYOUR_ECM_HOSTandYOUR_API_KEY) - Start Claude Code in that directory — it auto-detects
.mcp.jsonon launch - Run
/mcpto reconnect if the MCP server restarts - Ask Claude to manage your channels — e.g. "list my channels", "create an auto-creation rule for sports", "probe all streams"
If running ECM locally, use localhost as your host. If the MCP container is on the same Docker network as Claude Code, use the container name (ecm-mcp).
Upgrading from an earlier version: the MCP server moved from the deprecated SSE transport (/sse + /messages/) to the modern Streamable HTTP transport on a single /mcp endpoint. If you have an existing config pointing at http://YOUR_ECM_HOST:6101/sse?api_key=... (or "type": "sse" in a .mcp.json), change the path to /mcp (and "type": "http" for Claude Code). The /sse endpoint was removed in this version. API-key auth is unchanged.
Redeploying or rotating the MCP key: use Settings > MCP Integration > Regenerate Key — this updates mcp_api_key in settings.json. Then update the ?api_key= value in your Claude Desktop / Claude Code config. Do not edit dispatcharr_api_key (or its legacy api_key alias) in settings.json — that is the Dispatcharr REST token and is separate (see the field reference at the top of this section). As of v0.17.1 (GH #273) the Dispatcharr token lives in dispatcharr_api_key; the legacy api_key field is still read for one release of back-compat with a deprecation WARN.
For the full reference — step-by-step connection setup, key rotation details, and troubleshooting — see docs/user_guide/integrations/mcp.md.
| Tool | Description |
|---|---|
| Channels (18) | |
list_channels |
List channels with optional group/search/stream count filtering |
get_channel |
Get detailed channel info (streams, EPG, logo) |
create_channel |
Create a new channel |
update_channel |
Update channel name, number, or group |
delete_channel |
Delete a channel |
bulk_delete_channels |
Delete multiple channels at once |
add_stream_to_channel |
Add a stream to a channel |
remove_stream_from_channel |
Remove a stream from a channel |
reorder_streams |
Reorder streams within a channel by priority |
assign_channel_numbers |
Bulk-assign sequential channel numbers |
merge_channels |
Merge multiple channels into one |
find_duplicate_channels |
Scan for channels with matching normalized names |
bulk_merge_duplicate_channels |
Merge multiple groups of duplicates at once |
bulk_commit_channels |
Commit a batch of channel operations atomically |
build_channel_lineup |
Bulk-create channels and fuzzy-match streams |
clear_auto_created |
Remove auto-created channels by group |
bulk_add_streams_to_channel |
Add multiple streams to a channel in one backend call (single Dispatcharr roundtrip) |
bulk_assign_epg |
Assign EPG IDs (tvg_id) to multiple channels |
| Groups (8) | |
list_channel_groups |
List all groups with channel counts |
create_channel_group |
Create a new group |
get_orphaned_groups |
Find groups with no channels |
delete_channel_group |
Delete a group, optionally deleting its channels |
delete_orphaned_groups |
Delete groups with no channels assigned |
get_hidden_groups |
List hidden channel groups |
get_auto_created_groups |
List auto-created groups |
get_groups_with_streams |
List groups with stream count info |
| Streams (17) | |
list_streams |
List streams with group/provider/search filtering |
search_streams |
Search streams by name across all providers |
get_streams_by_ids |
Fetch detailed info for specific stream IDs |
get_streams_for_channel |
Get streams assigned to a channel |
get_stream_health |
Stream health summary from last probe |
probe_streams |
Start probing all streams (background) |
probe_single_stream |
Probe one specific stream |
probe_bulk_streams |
Probe multiple streams at once |
get_probe_progress |
Check ongoing probe status |
get_probe_results |
Results from the most recent probe |
get_struck_out_streams |
List streams with consecutive failures |
cleanup_struck_out_streams |
Remove struck-out streams from channels |
bulk_remove_streams |
Remove multiple streams from a channel |
cancel_probe |
Cancel a running probe |
bulk_search_streams |
Search multiple stream names in one call |
fuzzy_match_stream |
Find best fuzzy match for a stream name |
match_streams_to_channels |
Match streams to channels by name similarity |
| M3U (9) | |
list_m3u_accounts |
List all M3U provider accounts |
get_m3u_account |
Get detailed account info |
create_m3u_account |
Create a new M3U account |
update_m3u_account |
Update account name or URL |
delete_m3u_account |
Delete an M3U account |
refresh_m3u |
Refresh a specific M3U account |
refresh_all_m3u |
Refresh all M3U accounts |
update_m3u_group_settings |
Enable/disable stream groups on an account |
bulk_update_m3u_group_settings |
Enable/disable multiple stream groups at once |
| EPG (10) | |
list_epg_sources |
List EPG data sources |
create_epg_source |
Create a new EPG source |
update_epg_source |
Update an EPG source |
delete_epg_source |
Delete an EPG source |
refresh_epg |
Refresh a specific EPG source |
match_channels_epg |
Auto-match channels to EPG data |
refresh_all_epg |
Refresh multiple or all EPG sources at once |
get_epg_grid |
What's on TV now — EPG schedule grid |
list_dummy_epg_profiles |
List dummy EPG profiles |
generate_dummy_epg |
Regenerate dummy EPG XMLTV data |
| Auto-Creation (12) | |
list_auto_creation_rules |
List all rules |
get_auto_creation_rule |
Get rule details (conditions, actions, normalization groups, sort config) |
create_auto_creation_rule |
Create a rule with conditions, actions, and per-rule normalization groups |
update_auto_creation_rule |
Update an existing rule (supports normalization_group_ids) |
delete_auto_creation_rule |
Delete a rule |
toggle_auto_creation_rule |
Enable/disable a rule |
duplicate_auto_creation_rule |
Duplicate a rule |
run_auto_creation |
Run pipeline (dry_run=true by default) |
list_auto_creation_executions |
View execution history |
rollback_auto_creation |
Undo an execution |
get_auto_creation_debug_bundle |
Info about the diagnostic debug bundle for troubleshooting |
bulk_toggle_auto_creation_rules |
Toggle multiple rules at once |
| Export (6) | |
list_export_profiles |
List export profiles |
create_export_profile |
Create an export profile |
delete_export_profile |
Delete an export profile |
generate_export |
Generate M3U/XMLTV for a profile |
list_cloud_targets |
List cloud storage targets |
publish_export |
Publish to a cloud target |
| FFmpeg (14) | |
ffmpeg_capabilities |
Detect system FFmpeg capabilities |
ffmpeg_probe |
Probe a media source for stream info |
ffmpeg_list_configs |
List saved FFmpeg configurations |
ffmpeg_create_config |
Create new FFmpeg configuration |
ffmpeg_get_config |
Get specific configuration |
ffmpeg_update_config |
Update configuration |
ffmpeg_delete_config |
Delete configuration |
ffmpeg_validate |
Validate builder state |
ffmpeg_generate_command |
Generate annotated FFmpeg command |
ffmpeg_list_jobs |
List transcoding jobs |
ffmpeg_create_job |
Create and queue transcoding job |
ffmpeg_get_job |
Get job status and progress |
ffmpeg_cancel_job |
Cancel running job |
ffmpeg_delete_job |
Delete job record |
| Tasks (7) | |
list_tasks |
List scheduled tasks and status |
run_task |
Run a task immediately |
cancel_task |
Cancel a running task |
get_task_history |
View task execution history |
list_task_schedules |
List schedules for a task |
create_task_schedule |
Create a schedule for a task (interval / daily / weekly / biweekly / monthly) |
delete_task_schedule |
Delete a schedule |
| Stats (7) | |
get_channel_stats |
Channel viewing stats and active viewers |
get_top_watched |
Most-watched channels by viewing time |
get_bandwidth |
Bandwidth usage (today, week, month, all-time) |
get_popularity_rankings |
Channel popularity scores and trending |
get_watch_history |
Watch history with user attribution and filters (channel, IP, days) |
get_unique_viewers |
Unique viewer counts by channel |
compute_stream_sort |
Compute optimal stream sort order (resolution, bitrate, video codec, etc.) |
| System (6) | |
get_settings |
ECM settings overview |
create_backup |
Create config backup |
get_export_sections |
List available YAML export sections |
list_saved_backups |
List saved YAML backup files |
delete_saved_backup |
Delete a saved backup file |
get_journal |
Activity audit log (with limit/category filters) |
| Notifications (5) | |
list_notifications |
List notifications with unread count |
mark_notifications_read |
Mark all as read |
delete_all_notifications |
Clear all notifications |
list_alert_methods |
List configured alert methods (Discord, Telegram, email) |
test_alert_method |
Send a test notification through an alert method |
| Profiles (3) | |
list_channel_profiles |
List channel profiles |
list_stream_profiles |
List stream profiles |
apply_profile_to_channels |
Bulk-assign a profile to channels |
| Normalization (2) | |
test_normalization |
Test how stream names normalize |
list_normalization_rules |
List normalization rule groups |
Three read-only MCP resources provide quick context without a tool call: ecm://stats/overview, ecm://channels/summary, and ecm://tasks/status.
# Interactive mode (lists users, prompts for password)
docker exec -it enhancedchannelmanager python /app/reset_password.py
# Non-interactive
docker exec enhancedchannelmanager python /app/reset_password.py -u admin -p 'NewPass123'
# Skip password strength validation
docker exec enhancedchannelmanager python /app/reset_password.py -u admin -p 'simple' --force./scripts/search-stream.sh http://dispatcharr:9191 admin password "ESPN"| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite, @dnd-kit |
| Backend | Python, FastAPI, 20+ modular API routers |
| MCP Server | Python, FastMCP, Streamable HTTP transport, 124 tools |
| Deployment | Docker Compose, two containers (ECM + MCP) |
Interactive API docs are available at /api/docs (Swagger UI) and /api/redoc. See docs/api.md for the full endpoint reference.
- v0.17.2 — Full Stats v2 MCP coverage (8 new tools: provider stats, per-user watch time, trending, activity, channel bandwidth; media-server attribution now queryable via Claude); 30+ MCP correctness fixes from a live sweep of all tools; MCP API-key timing-attack hardening
- v0.17.1 — Plex + Jellyfin user attribution and multi-viewer display; real client IP threading through the attribution pipeline; SSRF hardening on media-server test-connection endpoints
- v0.17.0 — Stats v2 foundation:
session_telemetrytable, per-user watch-time API, Alembic schema migration system, Prometheus/metrics, structured JSON logging with trace IDs; ghost-source-channel fix after merge in edit mode - v0.16.0 — MCP server for natural language channel management via Claude (124 tools, 14 domains, Streamable HTTP transport); auto-creation rule analyzer; Alembic migrations; structured logging; per-rule normalization and sort options (an earlier 0.16.0 build was rolled back on 2026-04-20 before any external consumer pulled it; this is the shipping release, cut 2026-05-12)
- v0.15.1 — OWASP hardening (security headers, CORS, rate limiting, NIST password policy, log redaction, path validation)
- v0.15.0 — Server-side EPG matching, stream normalization, PUID/PGID support, low FPS detection, export/publish pipeline
- v0.14.0 — Dummy EPG profiles, auto-creation pipeline, normalization engine
- v0.13.0 — Backend modularization (20+ routers), auth system, task engine
See CHANGELOG.md [Unreleased] for the canonical list of fixes and features queued for the next cut.