diff --git a/docs/pwa-plan-oracle-review.txt b/docs/pwa-plan-oracle-review.txt new file mode 100644 index 0000000..135f7f9 --- /dev/null +++ b/docs/pwa-plan-oracle-review.txt @@ -0,0 +1,210 @@ +[oracle] cursor/openai/gpt-5.6-sol · effort=medium · 5 files · ~6700 tok · gateway +Verdict: APPROVE WITH CHANGES + +1. High — Use Serwist 10+ in configurator mode + +Pick Serwist, using its post-`next build` configurator flow and InjectManifest-style `self.__SW_MANIFEST`. + +Why: +- Designed for Next.js App Router and current Turbopack builds. +- Generates a revisioned precache after Next has emitted routes and chunks. +- Supplies Workbox-compatible strategies and expiration plugins without maintaining lifecycle code manually. +- Direct Workbox InjectManifest would require custom discovery of Next build outputs. +- Hand-rolled v2 unnecessarily retains cache revisioning, expiration, update lifecycle, and fallback risks already visible in `public/sw.js`. + +Do not combine a build-hash `CACHE_NAME` with revisioned precaching. Let Serwist manage precache revisions and cleanup; explicitly name/version only runtime caches. + +The Docker build must run: + +1. `next build` +2. `serwist build` +3. Copy the generated `public/sw.js` and Next output into the runtime image + +2. High — Next.js RSC requests need an explicit network-only rule + +“Network-first navigations” is insufficiently precise for Next.js 16. App Router Flight responses are build-specific and must not enter a general runtime cache. + +Before all broad routes, bypass caching for requests identified by: +- `RSC` header +- `Next-Router-State-Tree` +- `Accept: text/x-component` +- Next prefetch/Flight request forms + +Use: +- Navigation documents: network with `/offline` fallback; preferably do not retain arbitrary HTML. +- RSC/Flight and TMDB API calls: `NetworkOnly`. +- `/_next/static/`: revisioned precache or cache-first. +- TMDB images: bounded cache-first. +- Everything else: network-only unless intentionally classified. + +This avoids stale RSC payloads referencing incompatible chunks. + +3. High — Update behavior needs stricter lifecycle definition + +The PR2 design is directionally correct, but “MessageChannel update flow” needs an exact protocol: + +- New worker installs and waits. +- Client detects `registration.waiting`. +- Toast button sends `SKIP_WAITING`. +- Client waits for `controllerchange`. +- Reload exactly once after controller change. +- Activation removes only obsolete SkyStream caches, never every cache indiscriminately. + +Do not reload immediately after posting the message. + +Also serve `/sw.js` with `Cache-Control: no-cache` and register it with `updateViaCache: 'none'`. This matters behind Cloudflare. The registration currently embedded in `src/app/layout.jsx` has neither update handling nor cache control configuration. + +4. High — iOS version and seven-day eviction assumptions are wrong + +There is no consumer “iOS 19” release under Apple’s current naming. The current test target should be stated explicitly—presumably iOS/iPadOS 26 Safari and Home Screen web apps. + +Installed standalone Home Screen web apps are exempt from WebKit’s seven-day ITP deletion of script-writable storage. Ordinary Safari sites remain subject to it. Storage can still be removed under pressure or by the user. + +Replace PR3’s “accept iOS 7-day site-data eviction” with: +- Verify standalone storage isolation. +- Treat cache and localStorage as non-durable anyway. +- Optionally inspect/request persistence through `navigator.storage`. +- Test cold launch after device restart and storage pressure. + +5. High — Cross-origin iframe playback limits parent control + +In `src/components/StreamingPlayerModal.jsx`, the actual media element belongs to VidSrc’s origin. SkyStream cannot directly: +- Set Media Session metadata or action handlers. +- Reliably manage play/pause, seeking, PiP, or audio focus. +- Guarantee inline playback or autoplay. +- Correct provider behavior after WebKit changes. + +The iframe currently allows only: + +`encrypted-media; autoplay; fullscreen` + +Add `picture-in-picture` and, if supported by the provider/tested WebKit version, `mediasession`. This only delegates permission; it does not make VidSrc implement those features. + +Keep the origin-checked `postMessage` handling, but validate the complete message schema and numeric ranges rather than trusting every `PLAYER_EVENT` from the provider origin. + +6. High — iOS Home Screen PiP remains broken + +As of current WebKit releases, Picture-in-Picture can fail in installed Home Screen apps even when `document.pictureInPictureEnabled` reports true. The iframe makes capability detection even harder because SkyStream cannot inspect its video element. + +Required behavior: +- Do not promise PiP for installed iOS. +- Offer “Open in Safari” or the existing “Open in new tab” as the fallback. +- Test native fullscreen exit, rotation, Dynamic Island, home-indicator overlap, interruptions, lock screen controls, and resume after backgrounding. +- Do not rely on manifest `orientation`; iOS may ignore it. + +For first-party trailer playback, separately test: +- `playsInline` +- user-gesture playback +- native HLS before hls.js fallback +- Screen Wake Lock where available +- backgrounding and audio interruption +- PiP capability in Safari versus standalone mode + +7. Medium — Drop `window-controls-overlay` + +Drop it for this mobile-first target. It is desktop-only, unsupported on Android and iOS, and creates future desktop title-bar layout obligations. + +Use: + +- `display: "standalone"` +- optionally `display_override: ["standalone"]`, though that adds little + +Add `window-controls-overlay` later only with a designed and tested desktop title-bar experience. + +8. Medium — Static offline fallback is the correct scope + +Use a static `/offline` page. Cached-catalog browsing is scope creep because it requires: +- Persisting and versioning TMDB data +- Expiration semantics +- Offline image behavior +- Empty/stale-state UX +- Storage budgeting and privacy decisions +- Clear messaging that playback remains unavailable + +A polished offline page with retry and cached shell assets provides the right value for an online-first streaming application. + +Ensure `/offline` is statically renderable and included in the precache. Its required CSS, fonts, and chunks must also be available offline. + +9. Medium — TMDB image policy needs tighter bounds + +Entry count alone is not enough because poster/backdrop sizes vary significantly. Use both: +- `maxEntries` +- `maxAgeSeconds` + +Cache only the expected TMDB image host and pathname pattern. Accept opaque responses explicitly with cacheable statuses `[0, 200]`. Do not create a generic cross-origin image rule. + +Place explicit network-only exclusions for Google Analytics and VidSrc before this route. + +10. Medium — PR boundaries are not independently deployable as written + +Required reordering: + +PR1: +- All real icons, including the 180px Apple icon +- Manifest identity and install fields +- `viewportFit: "cover"` +- Baseline shell safe-area handling +- Validate Search `/` and Discover `/home` shortcut URLs + +PR2: +- Static offline route +- Serwist build integration and Docker changes +- Runtime policies +- Waiting-worker toast and activation protocol +- Cloudflare/SW response headers +- Migration cleanup for `skystream-v1` +- RSC/Flight exclusions + +PR3: +- iOS playback, standalone, storage, safe-area and A2HS testing +- Targeted CSS fixes discovered by testing + +PR4: +- Android install UX and screenshots +- `beforeinstallprompt` +- Analytics and docs +- Automated installability/mobile tests + +The Apple icon is duplicated between PR1 and PR3; keep it entirely in PR1. The update toast must ship with PR2, otherwise PR2 can leave a waiting worker with no activation UI. + +11. Medium — “Audit 53 CSS files” is not a useful acceptance criterion + +Safe areas should normally be applied at layout boundaries, fixed headers/footers, fullscreen dialogs, and floating controls—not indiscriminately in every component stylesheet. + +Start with: +- `src/components/Layout.css` +- `src/components/StreamingPlayerModal.css` +- global/body background and overscroll styling +- fixed-position buttons such as BackToTop +- any trailer fullscreen controls + +Test portrait, landscape, keyboard-open, fullscreen transitions, and standalone mode. + +12. Low — Manifest fields are partly platform-specific + +`launch_handler`, shortcuts, screenshots, and rich install UI are mostly Chromium enhancements; Safari may ignore them. Keep them only when valid and tested—they are harmless progressive enhancement. + +Use a stable `id`, likely `/`, and never change it casually after users install. Supply screenshot `sizes`, `type`, and `form_factor`. Ensure maskable icons have real safe-zone padding rather than merely labeling the regular icon maskable. + +13. Low — Replace “Lighthouse PWA pass” + +Lighthouse removed its dedicated PWA category. Replace this acceptance criterion with: +- Chrome DevTools manifest/installability validation +- Production HTTPS installation test +- `beforeinstallprompt` test after engagement criteria +- Offline reload test +- Worker upgrade across two deployed builds +- Lighthouse performance/accessibility/best-practices checks + +Missing scope + +- Dockerfile/build-script and Cloudflare cache-rule changes. +- `sw.js` response headers and CSP implications. +- Next.js RSC/Flight exclusion tests. +- Two-version worker upgrade test, including an open playback modal. +- VidSrc behavior in installed iOS mode, including fullscreen, PiP fallback, backgrounding, and Media Session ownership. +- Trailer-specific native HLS/hls.js test matrix. +- Manifest screenshot/icon validation and maskable safe-zone checks. +- Consent-aware install analytics: `src/app/layout.jsx` defaults analytics storage to denied, so install events may intentionally not be transmitted. +- Provider failure UX when VidSrc is offline, blocked, or refuses iframe embedding. +oracle_rc=0 diff --git a/docs/pwa-plan.md b/docs/pwa-plan.md new file mode 100644 index 0000000..37007c8 --- /dev/null +++ b/docs/pwa-plan.md @@ -0,0 +1,109 @@ +# SkyStream PWA Plan — web-first installable app for Android + iOS + +Status: **Oracle-reviewed (APPROVE WITH CHANGES) — this doc is the Oracle-corrected +version of the original 4-PR plan.** All PRs stack bottom-up, each independently +deployable. Web-only forever; no native apps. + +## Why (current gaps) + +- manifest icons: only a 28x28 favicon; apple-touch-icon claims 180x180 on a 28px png +- no manifest id/scope/orientation/screenshots/shortcuts +- hand-rolled `sw.js`: static `CACHE_NAME` (hashed chunks accumulate forever), + network-first for a nonexistent `/api`, cross-origin (TMDB images, vidsrc) skipped, + 503-text offline "page", `skipWaiting`+`claim` hot-swap (stale-chunk risk), no update toast +- `viewport-fit=cover` + safe-area padding only in the player modal +- gtag must never be cached + +## PR stack (reordered per Oracle: icon work lives entirely in PR1) + +### PR1 — PWA foundation (icons, manifest, viewport) +- Generate real icon set from LOGO.png: 192x192, 512x512, maskable variants with + actual safe-zone padding, and a true 180x180 apple-touch-icon. (All icon work in + this PR; PR3 must not duplicate it.) +- Next 16: `export const viewport` (themeColor, `viewportFit: 'cover'`); + `metadata.icons` pointing at the real files. +- manifest.json: stable `id: '/'` (never change after users install), scope, + `display: 'standalone'` (drop `window-controls-overlay` — desktop-only, + creates title-bar layout obligations we don't want), categories, screenshots + (`sizes`+`type`+`form_factor`), shortcuts → Search `/` and Discover `/home` + (validate those URLs resolve). +- Baseline safe-area `env()` handling at layout boundaries only (Layout.css header/ + footer, BackToTop, body overscroll) — NOT a 53-file CSS sweep. + +### PR2 — Service worker rebuild (Serwist) + offline page +- **Serwist 10+ (configurator mode, InjectManifest-style)**, not next-pwa (dead) or + hand-rolled. Docker build runs: `next build` → `serwist build` → copy outputs. + Let Serwist own precache revisions/cleanup; do NOT also use a build-hash + CACHE_NAME for precache (only runtime caches get explicit names/versions). +- Routing policy (order matters, network-only exclusions first): + 1. gtag/analytics + vidsrc origins: NetworkOnly + 2. Next.js RSC/Flight requests (RSC header, Next-Router-State-Tree, + Accept: text/x-component): NetworkOnly — build-specific payloads must never + be served stale + 3. TMDB API calls: NetworkOnly + 4. `/_next/static/`: precache/cache-first (immutable) + 5. TMDB images (image.tmdb.org path pattern only): cache-first bounded by BOTH + `maxEntries` and `maxAgeSeconds`, cacheable statuses `[0, 200]` (opaque OK) + 6. Navigations: network with `/offline` fallback; do not retain arbitrary HTML + 7. Everything else: NetworkOnly unless deliberately classified +- Static `/offline` page (statically renderable, precached with its chunks/css); + no cached-catalog browsing — scope creep (Oracle concurs). +- Update protocol (exact): new worker installs → waits → client detects + `registration.waiting` → toast "New version available — Reload" → button posts + SKIP_WAITING → client waits for `controllerchange` → reload exactly once. + Never reload immediately after posting. Activation deletes only obsolete + SkyStream-named caches. +- Serve `/sw.js` with `Cache-Control: no-cache` and register with + `updateViaCache: 'none'` (matters behind Cloudflare). +- Migration: delete legacy `skystream-v1` cache on activate. + +### PR3 — iOS reality pass (testing-driven, not icon work) +- Target: iOS/iPadOS 26 Safari + Home Screen web apps (not "iOS 19"). +- Installed standalone apps are EXEMPT from WebKit 7-day script-storage eviction + (ordinary Safari tabs are not). Still treat storage as non-durable; optionally + `navigator.storage.persist()`; test cold launch + storage pressure. +- Playback is an iframe on VidSrc's origin: SkyStream cannot set Media Session + metadata, control play/pause/PiP, or guarantee inline playback. Add + `picture-in-picture` (and `mediasession` if provider supports) to the iframe + allow list — permission only, not capability. Tighten the existing + origin-checked postMessage handler: validate full message schema + numeric + ranges, don't trust every PLAYER_EVENT. +- iOS Home Screen PiP is known-broken even when `pictureInPictureEnabled` is true: + never promise PiP in installed mode; surface "Open in Safari" fallback. +- Test matrix: playsInline trailers, user-gesture playback, native HLS before + hls.js fallback, Wake Lock, backgrounding/audio interruption, fullscreen exit, + rotation, Dynamic Island/home-indicator overlap, lock-screen controls, + standalone vs Safari. Targeted CSS fixes from results (safe areas at + boundaries: player modal, Layout shell, trailer fullscreen, BackToTop). +- Do not rely on manifest `orientation` on iOS. + +### PR4 — Install UX + verification +- `beforeinstallprompt` capture + custom install button (Android); iOS gets the + custom A2HS instructions sheet (no BIP there). +- Analytics on install events — NOTE: default consent is denied, so events may + legitimately not transmit; make the event consent-aware rather than fighting it. +- Verification (replaces "Lighthouse PWA pass" — category removed upstream): + DevTools manifest/installability validation, real HTTPS install test, + BIP after engagement, offline reload test, **two-consecutive-deploy worker + upgrade test including with an open playback modal**, Lighthouse + perf/a11y/best-practices. +- Playwright mobile-emulation smoke; docs/AGENTS.md update. + +## Missing scope Oracle added (fold into the PRs above) +- Dockerfile/build-script changes + Cloudflare cache-rule/header changes for sw.js +- CSP implications of the worker + served headers +- RSC/Flight exclusion tests +- Two-version upgrade test with open playback modal +- VidSrc-in-installed-iOS behavior matrix (fullscreen, PiP fallback, backgrounding) +- Trailer native-HLS/hls.js test matrix +- Maskable safe-zone + screenshot validation +- Provider-failure UX when VidSrc is down/blocked (currently silent) + +## Deliberately out of scope +- Cached-catalog offline browsing +- Desktop window-controls-overlay +- Any native wrapper (Capacitor/etc.) +- Promising PiP/Media Session on iOS installed mode + +## Open questions (none blocking) +- Screenshots need device-frame captures; can generate from Playwright at build time later.