Approved design for a web dashboard that lets the host announce tracks to IRC in real-time during live shows via WebSocket push. Made-with: Cursor
7.3 KiB
Live Announce Dashboard — Design Document
Date: 2026-03-12 Status: Approved
Purpose
A web dashboard that lets Nick announce tracks to IRC in real-time during his live show. He sees the current and previous week's playlists, clicks "Announce" next to a track, and the IRC bot immediately emits the announcement to #sewerchat (configurable). No one in chat needs to request it — the host pushes announcements on his own schedule.
Architecture
Communication: WebSocket hub
The API gains a WebSocket endpoint at /ws/announce. IRC bot plugins (Sopel and Limnoria) connect as subscribers. When Nick clicks "Announce" on the dashboard, the web page POSTs to /admin/announce, the API formats the message and broadcasts it to all connected WS subscribers, and each bot emits it to the configured channel.
Nick's browser API server IRC bot plugin
| | |
| POST /admin/announce | |
| {show_id, position} | |
|--------------------------->| |
| | WS: {"type":"announce", |
| | "message":"Now Playing:|
| | Song #3: ..."} |
| |------------------------->|
| | |
| 200 {"status":"announced"}| bot.say(message)
|<---------------------------| -> #sewerchat
Why POST instead of WS for sending
The dashboard authenticates via session cookie. A plain POST with the existing session is simpler than mixing cookie auth with a WebSocket send channel. HTTP round-trip is <50ms, broadcast is instant.
Reconnection
Bot plugins auto-reconnect with exponential backoff (5s, 10s, 20s, capped at 60s). Announcements during disconnection are lost. Nick can see bot connection status on the dashboard and re-announce if needed.
Authentication & Sessions
Login flow
- Nick visits
/dashboard— no valid session cookie → redirect to/login. /loginserves a styled HTML login form.- Form POSTs to
/loginwith username + password. - Server validates against
NTR_WEB_USER/NTR_WEB_PASSWORDenv vars. Success → signed session cookie (ntr_session) + redirect to/dashboard. Failure → re-render login with error. - Cookie signed with
NTR_SECRET_KEY. HTTPOnly, SameSite=Lax. /logoutclears the cookie, redirects to/login.
Session expiry
24 hours.
New config values
| Variable | Default | Description |
|---|---|---|
NTR_WEB_USER |
(required if dashboard enabled) | Dashboard login username |
NTR_WEB_PASSWORD |
(required if dashboard enabled) | Dashboard login password |
NTR_SECRET_KEY |
(required if dashboard enabled) | Cookie signing key |
Dashboard-optional
If these three env vars are absent, the dashboard routes are not mounted. The API works exactly as before.
Web Dashboard UI
Single page at /dashboard, no client-side routing.
Layout
- Header: "NtR Playlist Dashboard" + connection status indicator (green dot = bots connected, gray = none) + logout link.
- Current Show: Episode number, date range, track count. Track table with columns:
#,Title,Artist,Link,Announcebutton. Button flashes checkmark on success, shows error toast on failure. - Previous Show: Same layout, collapsed by default (click to expand). Same announce functionality.
Connection status
The dashboard opens a read-only WebSocket to /ws/announce (authenticated via token query param from the session). The API broadcasts {"type": "status", "subscribers": N} on connect/disconnect events. Zero subscribers → buttons show "No bots connected" warning.
Styling
Pico CSS via CDN. Dark theme. Minimal custom CSS for button states and status indicator. Responsive for phone use.
No JS framework
Vanilla fetch() for announce POST, vanilla WebSocket for status. Total JS under 100 lines.
Announcement Format
Now Playing: Song #3: Running Through My Mind by Purrple Panther - https://soundcloud.com/...
Format logic lives in the API (/admin/announce endpoint), not in the bot plugins.
Bot Plugin Changes
Both Sopel and Limnoria plugins gain identical behavior, maintaining 1:1 feature parity.
New config values
| Limnoria (camelCase) | Sopel (snake_case) | Default | Description |
|---|---|---|---|
wsUrl |
ws_url |
ws://127.0.0.1:8000/ws/announce |
WebSocket endpoint |
announceChannel |
announce_channel |
#sewerchat |
Channel for announcements |
WebSocket client lifecycle
- On plugin load, spawn a background daemon thread connecting to the WS endpoint.
- Send
{"type": "subscribe", "token": "<admin_token>"}on connect. - On
{"type": "announce", "message": "..."}, send the message to the configured channel viairc.queueMsg(Limnoria) /bot.say(Sopel). - On disconnect, reconnect with exponential backoff (5s → 60s cap).
- On plugin unload, close the connection cleanly.
Threading
WS client runs in a dedicated daemon thread using the websocket-client library (synchronous, blocking). bot.say() and irc.queueMsg() are thread-safe in both frameworks.
New dependency
websocket-client — listed in each plugin's docs, not in the main pyproject.toml (plugins deploy independently).
No changes to existing commands
!N, !playlist, !song, !lastshow, !status, !refresh all work as before.
API Endpoints (New)
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/login |
none | Login page |
POST |
/login |
none | Validate credentials, set session |
GET |
/logout |
session | Clear session, redirect |
GET |
/dashboard |
session | Dashboard page |
POST |
/admin/announce |
bearer OR session | Broadcast track announcement |
WS |
/ws/announce |
token in first message | Bot subscriber + dashboard status |
POST /admin/announce
Request: {"show_id": 10, "position": 3}
Response: {"status": "announced", "message": "Now Playing: Song #3: ..."}
The API looks up the track, formats the message, and broadcasts to all WS subscribers.
New Files
| File | Purpose |
|---|---|
src/ntr_fetcher/dashboard.py |
FastAPI router: login, logout, dashboard, announce |
src/ntr_fetcher/websocket.py |
WebSocket manager: subscriber tracking, broadcast |
src/ntr_fetcher/static/dashboard.html |
Dashboard page (HTML + inline JS + CSS) |
src/ntr_fetcher/static/login.html |
Login page |
Modified Files
| File | Change |
|---|---|
src/ntr_fetcher/config.py |
Add web_user, web_password, secret_key (optional) |
src/ntr_fetcher/api.py |
Mount dashboard router + WS if dashboard config present |
src/ntr_fetcher/main.py |
Pass new config to create_app |
plugins/limnoria/NtrPlaylist/plugin.py |
WS client thread + announce channel |
plugins/limnoria/NtrPlaylist/config.py |
wsUrl, announceChannel registry values |
plugins/sopel/ntr_playlist.py |
WS client thread + announce channel |
Unchanged Files
db.py, models.py, soundcloud.py, poller.py, week.py — no modifications.