# 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 1. Nick visits `/dashboard` — no valid session cookie → redirect to `/login`. 2. `/login` serves a styled HTML login form. 3. Form POSTs to `/login` with username + password. 4. Server validates against `NTR_WEB_USER` / `NTR_WEB_PASSWORD` env vars. Success → signed session cookie (`ntr_session`) + redirect to `/dashboard`. Failure → re-render login with error. 5. Cookie signed with `NTR_SECRET_KEY`. HTTPOnly, SameSite=Lax. 6. `/logout` clears 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`, `Announce` button. 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 1. On plugin load, spawn a background daemon thread connecting to the WS endpoint. 2. Send `{"type": "subscribe", "token": ""}` on connect. 3. On `{"type": "announce", "message": "..."}`, send the message to the configured channel via `irc.queueMsg` (Limnoria) / `bot.say` (Sopel). 4. On disconnect, reconnect with exponential backoff (5s → 60s cap). 5. 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.