Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions docs/pwa-plan-oracle-review.txt
Original file line number Diff line number Diff line change
@@ -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
109 changes: 109 additions & 0 deletions docs/pwa-plan.md
Original file line number Diff line number Diff line change
@@ -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.
Loading