From a73781a048844fcf23bc11b3eb6e42a67f2b7237 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Thu, 2 Jul 2026 21:15:35 +0800 Subject: [PATCH] docs: record deploy procedure, update-flow architecture, and data-model quirks --- CLAUDE.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e03d961 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# 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. + +## 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 ≈ 5–6 min): + ```bash + 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.172` → `ssh root@10.8.0.2` (WireGuard). Container name: + `ytplayer-main-1dihzn-ytplayer-1`. DB: libsql file `/app/data/ytplayer.db` + (query with `docker exec 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.3`→`v1.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. + +## 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. + +## 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.