Files
ytplayer/CLAUDE.md
Jonathan Sykes 3afb22b305 document repo map and local dev, add deploy-prod skill
- 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
2026-07-11 13:44:57 +08:00

5.9 KiB
Raw Blame History

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 (project ytplayer, env production).
  • Pushing to git does NOT deploy. Trigger explicitly (build ≈ 56 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 — the buildTag (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.172ssh root@10.8.0.2 (WireGuard). Container name: ytplayer-main-1dihzn-ytplayer-1. DB: libsql file /app/data/ytplayer.db (query with docker exec <c> bun -e '...' using @libsql/client).

Update-flow architecture (fragile — read before touching)

  • GET /sw.js is served by the server with the real BUILD_TAG injected by regex over the fallback expression in frontend/sw.js. Never switch back to an exact-string replace: when the fallback literal was bumped (v1.0.3v1.0.4) the exact match silently failed, the SW version froze, and no client ever received another update while /api/version kept announcing one — the "Update available keeps showing" bug.
  • BUILD_TAG hashes every file under ./public recursively. 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 waiting transiently and must not banner.
  • "Refresh UI" (frontend/sw-update.js): if no worker is waiting yet (banner came from the /api/version poll), it calls reg.update(), waits for installed, posts SKIP_WAITING, waits for controllerchange, then reloads once.

Data model quirks

  • Client state persists in localStorage key _ytpdata and syncs (debounced 400 ms) to POST /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 (profiles table, /api/profile/*): named cross-device sync where the lowercase profile NAME is the only credential (passkey-style, by design). Client stores data.profile = {name, syncedAt}; sync is last-write-wins — push debounced on every persist(), pull on app launch when the server's updated_at is newer than the local syncedAt.

Testing

  • Unit: node --test frontend/ (sw, sw-update, async-guard).
  • E2E: npx playwright test — WebKit iPhone-12 profile against a static serve of frontend/ (needs npx 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.