feat: add public index page with censored playlist and live reveals

Public-facing page at / shows the current show's playlist with tracks
obscured until the admin marks them as announced. Tracks reveal in
real-time via a new unauthenticated /ws/public WebSocket. Server-side
censorship on /public/playlist strips track details from unannounced
items and sanitizes announced tracks to only expose frontend-needed
fields (no raw_json, track_id, etc). Past episodes are browsable with
fully revealed but sanitized tracklists.

Also fixes RuntimeError on backfill shutdown by closing the httpx
client on the same event loop that created it.

Made-with: Cursor
This commit is contained in:
cottongin
2026-04-01 23:41:17 -04:00
parent 11f13c86b5
commit 425a7047c3
8 changed files with 786 additions and 17 deletions

View File

@@ -7,7 +7,7 @@ from fastapi.testclient import TestClient
from ntr_fetcher.dashboard import create_dashboard_router
from ntr_fetcher.db import Database
from ntr_fetcher.models import Track
from ntr_fetcher.websocket import AnnounceManager
from ntr_fetcher.websocket import AnnounceManager, PublicManager
@pytest.fixture
@@ -23,11 +23,17 @@ def manager():
@pytest.fixture
def app(db, manager):
def public_manager():
return PublicManager()
@pytest.fixture
def app(db, manager, public_manager):
a = FastAPI()
router = create_dashboard_router(
db=db,
manager=manager,
public_manager=public_manager,
admin_token="test-token",
web_user="nick",
web_password="secret",
@@ -250,3 +256,59 @@ def test_set_announced_invalid_position(client, db):
headers={"Authorization": "Bearer test-token"},
)
assert resp.status_code == 404
# --- Public WebSocket tests ---
def test_ws_public_connects_without_auth(app):
with TestClient(app) as c:
with c.websocket_connect("/ws/public") as ws:
pass
def test_ws_public_receives_reveal_on_announce(app, db, public_manager):
_seed_show(db)
with TestClient(app) as c:
with c.websocket_connect("/ws/public") as ws:
c.post(
"/admin/announce",
json={"show_id": 1, "position": 1},
headers={"Authorization": "Bearer test-token"},
)
msg = ws.receive_json()
assert msg["type"] == "reveal"
assert msg["position"] == 1
assert msg["track"]["title"] == "Song A"
assert msg["track"]["artist"] == "Artist A"
def test_ws_public_receives_reveal_on_announced_toggle(app, db, public_manager):
_seed_show(db)
with TestClient(app) as c:
with c.websocket_connect("/ws/public") as ws:
c.post(
"/admin/announced",
json={"show_id": 1, "position": 1, "announced": True},
headers={"Authorization": "Bearer test-token"},
)
msg = ws.receive_json()
assert msg["type"] == "reveal"
assert msg["position"] == 1
assert msg["track"]["title"] == "Song A"
def test_ws_public_receives_hide_on_unannounce(app, db, public_manager):
show = _seed_show(db)
db.set_track_announced(show.id, 1, True)
with TestClient(app) as c:
with c.websocket_connect("/ws/public") as ws:
c.post(
"/admin/announced",
json={"show_id": 1, "position": 1, "announced": False},
headers={"Authorization": "Bearer test-token"},
)
msg = ws.receive_json()
assert msg["type"] == "hide"
assert msg["position"] == 1
assert "track" not in msg