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:
168
docs/plans/2026-03-12-live-announce-dashboard-design.md
Normal file
168
docs/plans/2026-03-12-live-announce-dashboard-design.md
Normal 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.
|
||||
Reference in New Issue
Block a user