From 803193046e4bc3b916284d87a5a6d9e237155b69 Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Sun, 12 Jul 2026 21:39:41 +0200 Subject: [PATCH] feat(career): venue select/unselect UX, intro manifest support, fullmatch guards - 'Play here' now also defaults the visualization to Venue (remembering the prior viz); active venues show 'Leave venue' which restores it and sets the '__none__' override so no installed venue silently reapplies. - Pack manifests may ship an intro block (flyover video + ambience mp3); files validate like loops/stingers, .mp3 added to the serving whitelist. - Codex preflight: whitelist regexes use fullmatch (trailing-newline names could validate but 500 on serving). Co-Authored-By: Claude Fable 5 --- plugins/career/routes.py | 19 +++++++++++-------- plugins/career/screen.js | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/plugins/career/routes.py b/plugins/career/routes.py index b9d7310..dfc6834 100644 --- a/plugins/career/routes.py +++ b/plugins/career/routes.py @@ -35,7 +35,7 @@ from fastapi.responses import FileResponse PLUGIN_ID = "career" VENUE_ID_RE = re.compile(r"^[a-z0-9_-]{1,40}$") -PACK_FILENAME_RE = re.compile(r"^[a-z0-9_-]{1,64}\.(mp4|webm|json)$") +PACK_FILENAME_RE = re.compile(r"^[a-z0-9_-]{1,64}\.(mp4|webm|mp3|json)$") REQUIRED_LOOPS = ("bored", "neutral", "engaged", "ecstatic") DOWNLOAD_CHUNK = 1024 * 256 @@ -109,13 +109,16 @@ def _validate_pack_dir(pack_dir: Path): loops = manifest.get("loops") or {} for state in REQUIRED_LOOPS: name = loops.get(state) - if not name or not PACK_FILENAME_RE.match(name): + if not name or not PACK_FILENAME_RE.fullmatch(name): raise ValueError(f"manifest is missing the '{state}' loop") if not (pack_dir / name).is_file(): raise ValueError(f"loop file '{name}' missing from pack") for name in (manifest.get("stingers") or {}).values(): - if name and (not PACK_FILENAME_RE.match(name) or not (pack_dir / name).is_file()): + if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()): raise ValueError(f"stinger file '{name}' invalid or missing") + for name in (manifest.get("intro") or {}).values(): + if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()): + raise ValueError(f"intro file '{name}' invalid or missing") def _download_pack(venue_id, pack, progress): @@ -149,7 +152,7 @@ def _download_pack(venue_id, pack, progress): if info.is_dir(): continue name = Path(info.filename).name - if name != info.filename or not PACK_FILENAME_RE.match(name): + if name != info.filename or not PACK_FILENAME_RE.fullmatch(name): raise ValueError(f"unexpected file in pack: {info.filename!r}") with zf.open(info) as src, open(extract_dir / name, "wb") as dst: shutil.copyfileobj(src, dst) @@ -205,7 +208,7 @@ def setup(app, context): @app.post(f"/api/plugins/{PLUGIN_ID}/packs/{{venue_id}}/download") def start_download(venue_id: str): - venue = _venue(venue_id) if VENUE_ID_RE.match(venue_id) else None + venue = _venue(venue_id) if VENUE_ID_RE.fullmatch(venue_id) else None if venue is None: raise HTTPException(404, "Unknown venue.") pack = venue.get("pack") @@ -227,7 +230,7 @@ def setup(app, context): @app.delete(f"/api/plugins/{PLUGIN_ID}/packs/{{venue_id}}") def delete_pack(venue_id: str): - if not VENUE_ID_RE.match(venue_id) or _venue(venue_id) is None: + if not VENUE_ID_RE.fullmatch(venue_id) or _venue(venue_id) is None: raise HTTPException(404, "Unknown venue.") with _lock: running = _state["downloads"].get(venue_id) @@ -239,7 +242,7 @@ def setup(app, context): @app.get(f"/api/plugins/{PLUGIN_ID}/venues/{{venue_id}}/{{filename}}") async def get_pack_file(venue_id: str, filename: str): - if not VENUE_ID_RE.match(venue_id) or not PACK_FILENAME_RE.match(filename): + if not VENUE_ID_RE.fullmatch(venue_id) or not PACK_FILENAME_RE.fullmatch(filename): raise HTTPException(404, "Not found.") path = _venue_dir(venue_id) / filename # Defense-in-depth beyond the regexes (same recipe as highway_3d): @@ -251,7 +254,7 @@ def setup(app, context): raise HTTPException(404, "Not found.") if not resolved.is_file(): raise HTTPException(404, "Not found.") - media = {"mp4": "video/mp4", "webm": "video/webm", + media = {"mp4": "video/mp4", "webm": "video/webm", "mp3": "audio/mpeg", "json": "application/json"}[resolved.suffix.lstrip(".").lower()] return FileResponse( resolved, diff --git a/plugins/career/screen.js b/plugins/career/screen.js index 58e0c03..ddbe463 100644 --- a/plugins/career/screen.js +++ b/plugins/career/screen.js @@ -13,6 +13,8 @@ const API = '/api/plugins/career'; const VENUE_OVERRIDE_KEY = 'feedBack-career-venue'; + const NO_VENUE = '__none__'; + const PREV_VIZ_KEY = 'feedBack-career-prev-viz'; const POLL_MS = 2000; let _state = null; @@ -46,11 +48,12 @@ const gen = ++_manifestReqGen; const unlocked = state.venues.filter((v) => v.unlocked); let venue = null; - try { - const override = localStorage.getItem(VENUE_OVERRIDE_KEY); + let override = null; + try { override = localStorage.getItem(VENUE_OVERRIDE_KEY); } catch (_) { /* ok */ } + if (override !== NO_VENUE) { venue = unlocked.find((v) => v.id === override && v.installed) || null; - } catch (_) { /* ok */ } - if (!venue) venue = lastOf(unlocked.filter((v) => v.installed)); + if (!venue) venue = lastOf(unlocked.filter((v) => v.installed)); + } if (!venue) { if (_appliedManifestVenue !== null) { _appliedManifestVenue = null; @@ -82,8 +85,12 @@ action = `
Downloading… ${pct}%
`; } else if (v.installed) { + const active = localStorage.getItem(VENUE_OVERRIDE_KEY) === v.id; + const main = active + ? `` + : ``; action = `
- + ${main}
`; } else if (v.has_pack) { @@ -225,9 +232,28 @@ fetch(`${API}/packs/${delBtn.dataset.careerDelete}`, { method: 'DELETE' }) .then(refresh); } else if (playBtn) { - try { localStorage.setItem(VENUE_OVERRIDE_KEY, playBtn.dataset.careerPlay); } catch (_) { /* ok */ } + try { + localStorage.setItem(VENUE_OVERRIDE_KEY, playBtn.dataset.careerPlay); + // Selecting a venue makes the Venue visualization the default; + // remember what the user had so Leave venue can restore it. + const cur = localStorage.getItem('vizSelection'); + if (cur && cur !== 'venue') localStorage.setItem(PREV_VIZ_KEY, cur); + localStorage.setItem('vizSelection', 'venue'); + if (typeof window.setViz === 'function') window.setViz('venue'); + } catch (_) { /* ok */ } _appliedManifestVenue = null; // force manifest re-push refresh(); + } else if (e.target.closest('[data-career-unselect]')) { + try { + localStorage.setItem(VENUE_OVERRIDE_KEY, NO_VENUE); + const prev = localStorage.getItem(PREV_VIZ_KEY); + if (prev) { + localStorage.setItem('vizSelection', prev); + if (typeof window.setViz === 'function') window.setViz(prev); + } + } catch (_) { /* ok */ } + _appliedManifestVenue = null; + refresh(); } }