Document cache layout, offline thumbnails and test invocation
This commit is contained in:
57
CLAUDE.md
57
CLAUDE.md
@@ -73,6 +73,39 @@ Local DB file: `server/data/ytplayer.db` (gitignored). `BUILD_TAG` is computed f
|
||||
from the `/api/version` poll), it calls `reg.update()`, waits for `installed`,
|
||||
posts SKIP_WAITING, waits for `controllerchange`, then reloads once.
|
||||
|
||||
## Cache layout & offline thumbnails
|
||||
- Three cache families, and the split matters on activate: the **versioned shell
|
||||
cache** `ytplayer-<BUILD_TAG>` is evicted on every deploy, while the **utility
|
||||
caches** `ytplayer-thumbs` and `ytplayer-fonts` are listed in `UTILITY_CACHES`
|
||||
and deliberately survive it. Adding a new utility cache means adding it there
|
||||
too, or it gets wiped on the next deploy.
|
||||
- **Thumbnails are cache-first, not stale-while-revalidate** — a given thumbnail
|
||||
URL is immutable, so revalidating just burns a round trip per image per launch.
|
||||
- **The opaque-response trap (this silently emptied the thumb cache for months).**
|
||||
An `<img>` to another origin is a **no-cors** request, so `fetch(request)` in the
|
||||
SW resolves to an **opaque** response with `status === 0` — not 200. The old
|
||||
guard was `if (r.status === 200) cache.put(...)`, which rejected every single
|
||||
thumbnail, so `ytplayer-thumbs` was permanently empty and offline showed broken
|
||||
images (measured on prod: 0 entries after browsing pages full of visible thumbs).
|
||||
Fix in `thumbnail()`: re-issue the request in `cors` mode — ytimg/ggpht all send
|
||||
`Access-Control-Allow-Origin: *` — and cache that readable response; an opaque
|
||||
one is accepted only as a last resort. **Never reintroduce a bare `status === 200`
|
||||
check on a cross-origin subresource.**
|
||||
- Thumbnail hosts live in `THUMB_HOSTS`. An unlisted host doesn't error — it just
|
||||
bypasses the cache and breaks offline, so add mirrors/avatar hosts there.
|
||||
- The thumb cache is capped at `THUMB_CACHE_MAX` (800, oldest-first via the
|
||||
insertion-ordered `cache.keys()`). Keep a cap: CacheStorage and the OPFS offline
|
||||
audio share one origin quota, and opaque entries are padded to ~7 MB each for
|
||||
quota accounting, so an unbounded thumb cache can evict saved audio.
|
||||
- App side (`app.js`): `warmThumb()` pulls artwork through the SW when a video is
|
||||
saved offline, and `warmOfflineThumbs()` runs a bounded launch backfill
|
||||
(`THUMB_WARM_MAX` = 400, 4 at a time) over cached ids + playlist videos so
|
||||
libraries saved before this fix repair themselves. Thumbnails only cache when
|
||||
something requests them — a device needs one online launch to get offline art.
|
||||
- `staleWhileRevalidate()` (fonts) must not `return cached || networkFetch` bare:
|
||||
the fetch resolves to `null` offline and `respondWith(null)` throws. It ends
|
||||
with `|| Response.error()`.
|
||||
|
||||
## 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.
|
||||
@@ -84,13 +117,33 @@ Local DB file: `server/data/ytplayer.db` (gitignored). `BUILD_TAG` is computed f
|
||||
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`.
|
||||
- **Profile share links**: `?profile=<name>` is consumed by `adoptProfileFromUrl()`
|
||||
at boot, before any rendering. It strips the param via `replaceState` (so a
|
||||
reload can't re-fire it) and confirms first when the device already has
|
||||
playlists/history or another profile — adopting *replaces* the synced slice.
|
||||
The name comes off the URL untrusted, hence `escapeHtml()` on it.
|
||||
- **Empty-home playlist grid**: `renderHomePlaylists()` swaps the branding hero in
|
||||
`#playerPlaceholder` for the user's playlists (hero is the no-playlists
|
||||
fallback). It is driven from the tail of `renderSidebar()` — not from
|
||||
`render()` — so every playlist mutation refreshes both in one place.
|
||||
|
||||
## Testing
|
||||
- Unit: `node --test frontend/` (sw, sw-update, async-guard).
|
||||
- Unit: **`node --test frontend/*.test.js`** (sw, sw-update, async-guard, video-edit).
|
||||
The directory form `node --test frontend/` fails on Node 22 with
|
||||
`Cannot find module .../frontend` — it resolves the dir as a module, not a test
|
||||
glob. That's the harness, not the tests.
|
||||
- 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.
|
||||
— that failure mode is a real app bug, not test flake. On the WSL laptop the
|
||||
WebKit install fails host validation (missing `libgtk-4`, `libgstreamer*`, …,
|
||||
needs `sudo npx playwright install-deps`); chromium is already downloaded, so
|
||||
ad-hoc rendering/offline checks can drive it directly instead.
|
||||
- Service-worker behaviour (thumb caching, offline) is only provable in a real
|
||||
browser: serve `frontend/` statically, let the SW take control (needs a second
|
||||
reload), then `context.setOffline(true)` and assert `img.naturalWidth > 0` plus
|
||||
the `ytplayer-thumbs` entry count. Asserting against the browser's own HTTP
|
||||
cache proves nothing — check the Cache API entry count.
|
||||
- Test records on prod use `Probe */Recon *` names; clean via the container DB,
|
||||
children (`video_history`) first.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user