Files
NtR-soudcloud-fetcher/docs/plans/2026-03-12-live-announce-dashboard-design.md
cottongin 47a78b09a7 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
2026-03-12 07:04:53 -04:00

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

  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.