docs: add live announce dashboard design document

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
This commit is contained in:
cottongin
2026-03-12 07:04:53 -04:00
parent 9664b8225d
commit 47a78b09a7

View File

@@ -0,0 +1,168 @@
# 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": "<admin_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.