CLAUDE.md is the agent-facing repo guide; this is the same project
described for a human walking up to the repo cold. Covers the
architecture, the daily-operation commands, the fresh-rig bring-up,
the critical gotchas (OBS scene-JSON-on-exit, pipewire-pulse restart,
the Flatpak detection trap), and pointers into docs/ and archived/.
Moves the seven HTML scene dirs (landing/, loading/, game/, desktop/,
goodbye/, music-box/, music/) and the superseded bash helpers (setup.sh,
loading.sh, playlist.sh, telemetry.sh) into archived/ rather than deleting.
The Laravel webapp/ replaces all of them; archived/README.md spells out
the rollback procedure.
Also:
- rewrite the relevant sections of CLAUDE.md so it points at webapp/
blade views, the Artisan commands, and the supervisord lifecycle
(`supervisorctl restart obs-webapp` after route / controller / .env
changes; Blade view edits are still safe-while-running via OBS's
Refresh cache).
- extend scripts/deploy-rig.sh to install php + composer + supervisor,
run `composer install`, copy obs-webapp.supervisord.conf into
/etc/supervisor.d/, start the program, and call `php artisan
rig:setup` + `rig:telemetry --collect` instead of the old bash.
- .gitignore catches the generated machine-local files that came along
when the old scene dirs moved (telemetry.js, loading.json, playlist.js,
cmd.js, obs-config.js).
- daemon.blade.php is now passive — listens to mpd:state but does not
play audio or broadcast its own queue, so it stops fighting with
bridges/mpd-state.py (the post-browser-daemon-migration source of
truth for mpd:state).
- nc.blade.php overrides .terminal { overflow: hidden } from hud.css
so the `┤ TERMINAL ├` and `┤ NOW PLAYING ├` pane-tabs stick above
the pane border instead of being clipped.
Replaces the per-scene HTML directories (landing/, loading/, game/,
desktop/, goodbye/, music-box/, music/) with a single Laravel app
serving every overlay over HTTP. Supervisord runs `php artisan serve`
on 127.0.0.1:1118 and the OBS scene JSON now references HTTP routes
instead of file:// URLs.
Highlights:
- public/css/hud.css consolidates the duplicated HUD chrome,
scanlines/vignette/flicker, terminal styling, and pulse keyframes
that were copy-pasted across all seven scenes.
- Blade partials own hud-strip, crt-overlays, obs-ws-scripts,
camera-frame, screen-frame; the seven scenes extend a shared
overlay layout.
- Artisan commands (`rig:setup`, `rig:telemetry`, `rig:loading`,
`rig:playlist`, `rig:cmd`) replace the shell scripts that wrote
per-rig JSON snapshots. TwitchHelix + HardwareSnapshot services
handle the work the bash + Python helpers used to.
- ObsWsClient + MusicCommandController kill the 250 ms cmd.js poll
in the music daemon: POST /cmd/{skip|prev|pause|resume} opens a
short-lived Pawl WS, authenticates, and broadcasts mpd:cmd.
- AudioController streams files from the configured music dirs so
CEF can load tracks under the HTTP origin (Chromium blocks
HTTP-origin pages from loading file:// media).
- DataController serves /data/playlist.js (with ETag mtime cache)
and /cover.jpg (no-store) so the existing overlays' window.__PLAYLIST
and cover.jpg cache-bust pattern keeps working.
scripts/obs-webapp.supervisord.conf is the supervisord unit; install
to /etc/supervisor.d/obs-webapp.conf.
scripts/rewrite-scene-urls.py is a one-shot tool that rewrites
basic/scenes/Default_Stream_HUD.json from file:// to HTTP URLs.
basic/scenes/Default_Stream_HUD.json.pre-webapp is the rollback
artifact (made with OBS closed; full pre-migration state).
The seven old scene directories, vendor/, and the bash scripts are
still on disk pending visual verification; the next commit will
prune them.
Landing's top-left identifier upgraded from "OPHI-118" to
"OPHI-118 // [<RIG>]" where the rig portion (read from
window.__TEL.rig in landing/telemetry.js) does a cyberpunk
decoder reveal on first paint and re-glitches briefly every
~9 s. Bracket dimming + amber flash during scramble keep the
layout width-stable.
CLAUDE.md updated to reflect everything since the last sync:
new Landing rig display, Music Box terminal pattern + dedicated
chat status strip, widget at 549×880 (camera-paired), Desktop
HUD fork, bridge nextTitles[] payload, bot !queue/!info/per-user
cooldown + Mattermost going-live notifier (MM_HOOK / STATUS_URL).
Plus an OBS-detection note: Flatpak's inner binary is `obs`, not
`flatpak run …` — `flatpak ps` or `pgrep -x obs` are the only
reliable checks. The naive `pgrep -fa 'flatpak run …'` returns
"off" while OBS is up and led to JSON edits during a live session.
New full-screen scene (Music Box, Ctrl+M) for music-only foreground use.
Built on the same terminal pattern as Goodbye/Loading: typed boot
sequence (music --boot --shuffle, [audio] subscribing, library count,
queue: shuffle on, standby ✓), scrolling [audio] now: <title> history
on track changes, static-HTML pinned block at the bottom (3-up
next/then/then + arrow-pulse now-playing line). Below the terminal,
a dedicated "CHAT !skip · !queue · !info" status strip — kept outside
the scrolling area on purpose so the command help stays visible
regardless of terminal contents.
Desktop scene now mirrors Game's right-sidebar layout:
- Camera moved into top-right at the Game-scene transform (1937,98 ×
0.286), always visible (vs. Game which toggles per-session).
- New Music Box widget (music-box/widget.html) directly below camera,
same width (549 px) so the two pair as one column. 880 px tall to
fill the right sidebar down to the canvas bottom. Authored at native
pixel size with bounds_type 0 (no stretch) — pinned 3-up next +
inline arrow/title/time + chat help; same scrolling [audio] history
pattern as the full-screen Music Box.
- HUD swapped from the shared Game HUD to a forked desktop/index.html:
same top-strip + camera-frame OBS-sync, but no game-name slot
(status reads "DESKTOP"), no manifest dependence, no status-bar
music widget (lives in the dedicated sidebar source now).
Pinned blocks in both Music Box overlays are static HTML so the
structure renders even if a script throws — JS only updates text
content.
Chat commands:
- !skip: per-user 60 s cooldown (was global 5 s). Each chatter gets one
skip per minute; rejection includes a wait hint so users know
it's their own gate.
- !queue: peek next 3 tracks from the MPD queue. Read-only, no cooldown.
- !info: full metadata for now-playing — artist · title · album (year)
· genre, with missing tags skipped to avoid empty parens.
Mattermost going-live notifier (optional, MM_HOOK env):
- Polls https://ophi118.com/api/status.json (the public service in
station/server/) every 60 s and POSTs a single message to the
configured Mattermost webhook on each offline→live transition.
- Transition detection keys on Twitch's started_at, not just the live
flag, so a Helix flap inside one stream doesn't double-fire. Cold
start mid-stream is intentionally silent — records the current
started_at, awaits next genuine transition. Override the polled
endpoint with STATUS_URL for private status services.
.env.example documents the new MM_HOOK / STATUS_URL knobs.
Adds nextTitles (array of up to 3 display strings) plus mirrored single
nextTitle/nextArtist/nextTrackTitle/nextFile for the immediate next
track. Drives the Music Box overlays' 3-up "coming next" lines without
overlays needing to query MPD themselves.
Reads MPD status.nextsong, walks playlistinfo for the next 3 indices,
applies the existing _flatten_tag + display-string cascade. Fields are
None / [] at end of queue. Backwards-compatible: overlays that don't
read the new keys are unaffected.
Self-hosted async Python bot, runs as systemd --user unit
(obs-twitch-bot.service, sibling to obs-mpd-bridge). Connects to Twitch IRC
over TLS as vault118, listens in #ophi118, advances MPD on !skip and posts
the new track back to chat. 5s cooldown swallows skip-spam silently.
Strips Unicode Tags-block codepoints (U+E0000–U+E007F) before matching the
command so Twitch's anti-duplicate suffix doesn't break every-other !skip
when the broadcaster fires the same command twice in a row.
Bot account is separate (vault118) to keep the broadcaster's display name
out of bot replies; .env holds access token + refresh token + client id
(refresh + client id stored for future token rotation, unused today).
Captures the current OBS scene/window state after wiring Music Daemon,
Project Loading, and Game scenes through the UI. Untitled.json now
includes the new scenes and their browser sources.
Landing index.html: reverted to the simpler pre-doomguard version
(static signal bars, plain footer rig, no telemetry rotation, no
watermark animation), keeping the SIGNAL grid centering and the small
#rig opacity transition.
OBS rewrites these files on shutdown — expect future commits to show
similar dock-geometry diff noise.
Streaming overlay for the Game scene. Reads the manifest written by
loading.sh (game name, subtitle, mic state) and pulls live state from
OBS WebSocket on the same connection that delivers music updates.
- Top HUD: TRANSMISSION ID / SIGNAL bars / UTC clock — same vibe as
Landing/Loading, sized smaller so it doesn't dominate the gameplay.
- Camera frame (top-right, 480×270 with cyan corner ticks): decorative
drop-target. The actual OBS camera source layers ON TOP; a centered
placeholder (camcorder SVG + 'PLACE CAMERA HERE' + frame coordinates
'2008, 100 · 480×270') shows during setup and gets covered when the
camera source is positioned. Configurable via URL params:
?camera=Webcam&scene=Game.
- Status bar (bottom): solid gradient + cyan top edge so it reads on
any gameplay background. Two lines:
LIVE · GAME · SUBTITLE
elapsed timer · mic state · now-playing track
- Live camera-frame sync: queries GetSceneItemEnabled on connect and
subscribes to SceneItemEnableStateChanged, so toggling the camera's
eye-icon in OBS hides/shows the frame within ~50ms — overrides the
manifest's static value.
- Music sync: shares the same WS connection; now-playing line updates
from mpd:state broadcasts.
- CRT overlays kept lighter than Landing (gameplay must remain readable).
Music persistence across scene switches by extracting playback into a
separate browser source (music/index.html) — single source instance
shared across scenes via OBS's "Add Existing", so it doesn't restart on
scene change.
Loading and Game overlays subscribe to mpd:state CustomEvents over OBS
WebSocket; the daemon broadcasts at 1 Hz plus on every track change.
File-based command pipeline lets bash control playback without a
WebSocket client of its own:
- vendor/obs-ws-mini.js — minimal WS v5 client with HMAC-SHA256 auth,
hard timeout, close-rejection, and a pure-JS SHA-256 fallback for
CEF file:// origins where crypto.subtle is unavailable.
- setup.sh — generates vendor/obs-config.js from the OBS plugin config;
checks the live socket instead of the (lagging) config file.
- music/index.html — invisible audio worker. Plays through a shuffled
queue with hardened error/stall recovery, polls loading/cmd.js for
bash-issued commands, broadcasts mpd:state via WS. Includes a
diagnostic status block (visible if the source's eye-icon is on).
- loading/index.html — strips local audio playback; subscribes to
mpd:state to drive the now-playing terminal line live. Removes the
redundant trailing prompt; terminal lines anchor at the bottom.
- loading/playlist.sh — rebuilt as subcommand API:
sync (default), skip, prev, pause, resume, status
control commands write loading/cmd.js (id, type, ts), which the
daemon polls every 250ms.
- loading/convert.sh — strips video tracks from .webm music downloads
to audio-only m4a (~95% smaller, ~20× faster decode); fixes the
CPU pegging from CEF decoding 1080p VP9 video for music playback.
- .gitignore — track manifests, ignore generated wrappers + secrets +
the audio cache.