- CLAUDE.md gains a repo map, endpoint list, and local dev setup (server serves only ./public — symlink ../frontend once) - new deploy-prod skill captures the Dokploy deploy + buildTag verification flow; track .agents/.claude skills and skills-lock.json - ignore server/data, test-results, .claude/worktrees
5.9 KiB
ytplayer — project facts for agents
What this is
Ad-free YouTube player. Three shells share frontend/: web PWA (Bun + Hono in
server/, the production deployment), Tauri (Windows), zero-native (Linux/macOS).
The PWA is what runs in production; legacy/ holds the old native-only docs.
Repo map
| Path | What lives there |
|---|---|
frontend/app.js |
The entire UI (~3.6k lines, no framework): player, playlists, search, settings, sync |
frontend/sw.js + frontend/sw-update.js |
Service worker + update flow (FRAGILE — see below) |
frontend/opfs.js / opfs-worker.js |
OPFS offline audio cache |
frontend/fingerprint.js |
Browser fingerprint used as the sync key |
frontend/async-guard.js |
Stale-async-response guard (unit-tested) |
server/server.js |
Bun + Hono backend — endpoint list is in its header comment |
server/db.js |
libsql schema/queries (users, profiles, video_history…) |
bin/yt-dlp |
Downloaded by npm run setup, gitignored |
scripts/ |
icon generation, yt-dlp setup, push helper |
tests/ |
Playwright e2e specs · unit tests live next to sources in frontend/*.test.js |
legacy/ |
Old native-shell docs — do not treat as current |
API endpoints: GET /api/search|channel|streams|download/:id|version|user/data|profile/load,
POST /api/user/sync|profile/create|profile/save, GET /sw.js (BUILD_TAG-injected), GET /* static.
JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone.
Local dev
npm run setup # download bin/yt-dlp (once)
cd server && bun install
ln -s ../frontend public # once — the server serves ONLY ./public (Docker copies frontend/ there)
bun --hot server.js # http://localhost:3000
node --test frontend/ # unit tests (run from repo root)
npx playwright test # e2e (see Testing below)
Local DB file: server/data/ytplayer.db (gitignored). BUILD_TAG is computed from ./public contents.
Production deployment (web PWA)
- URL: https://worship.hesed.sbs (Traefik label in
docker-compose.yml) - Runs on the homelab Dokploy remote node; control plane is Dokploy on the VPS
(
193.160.119.172, API key in~/development/.secrets/dokploy-api.env). - Compose ID:
wprYCM8T51f7JtSHb983p(projectytplayer, envproduction). - Pushing to git does NOT deploy. Trigger explicitly (build ≈ 5–6 min):
ssh -i ~/.ssh/tmp_vps/dokploy_session root@193.160.119.172 \ "curl -s -X POST -H 'x-api-key: $KEY' -H 'Content-Type: application/json' \ -d '{\"composeId\":\"wprYCM8T51f7JtSHb983p\"}' http://localhost:3000/api/compose.deploy" # poll composeStatus via /api/compose.one?composeId=... until done|error - Confirm the deploy landed:
curl https://worship.hesed.sbs/api/version— thebuildTag(content hash of every file under./public) must change. - Homelab node is NOT always reachable on LAN; SSH via the VPS hop:
ssh root@193.160.119.172→ssh root@10.8.0.2(WireGuard). Container name:ytplayer-main-1dihzn-ytplayer-1. DB: libsql file/app/data/ytplayer.db(query withdocker exec <c> bun -e '...'using@libsql/client).
Update-flow architecture (fragile — read before touching)
GET /sw.jsis served by the server with the realBUILD_TAGinjected by regex over the fallback expression infrontend/sw.js. Never switch back to an exact-string replace: when the fallback literal was bumped (v1.0.3→v1.0.4) the exact match silently failed, the SW version froze, and no client ever received another update while/api/versionkept announcing one — the "Update available keeps showing" bug.BUILD_TAGhashes every file under./publicrecursively. Don't reduce it to a file subset; a change to an unlisted shell file would stop busting caches.- The update banner only shows when a waiting SW exists and the page already
has a controller — a first install (fresh visit, or after Settings → Force
refresh unregisters) passes through
waitingtransiently and must not banner. - "Refresh UI" (
frontend/sw-update.js): if no worker is waiting yet (banner came from the/api/versionpoll), it callsreg.update(), waits forinstalled, posts SKIP_WAITING, waits forcontrollerchange, then reloads once.
Data model quirks
- Client state persists in localStorage key
_ytpdataand syncs (debounced 400 ms) toPOST /api/user/sync, keyed by a browser fingerprint. - A-B loop markers are per-song-per-playlist: stored on the playlist's own
copy of the video (
entry.ab = {a, b}) when playback source is that playlist;data.abMarkers[videoId]is only the fallback for non-playlist playback. - Online profiles (
profilestable,/api/profile/*): named cross-device sync where the lowercase profile NAME is the only credential (passkey-style, by design). Client storesdata.profile = {name, syncedAt}; sync is last-write-wins — push debounced on every persist(), pull on app launch when the server'supdated_atis newer than the localsyncedAt.
Testing
- Unit:
node --test frontend/(sw, sw-update, async-guard). - E2E:
npx playwright test— WebKit iPhone-12 profile against a static serve offrontend/(needsnpx playwright install webkit). A spurious update banner will make the settings-panel specs fail with#modal intercepts pointer events— that failure mode is a real app bug, not test flake. - Test records on prod use
Probe */Recon *names; clean via the container DB, children (video_history) first.
Harness
Skills live in .agents/skills/ (symlinked into .claude/skills/):
- deploy-prod — the Dokploy deploy + buildTag verification flow (manual-only; use for any "deploy"/"release" request).
- mobile-app-ui-design — UI/UX design work on the PWA screens.
Commit rules
One changeset = one commit, single-line imperative message, no AI attribution
of any kind (global rule). git push origin main pushes to both remotes.