Safari, the iOS home screen, Android install and push notifications
still showed the old film-reel artwork beside the new theme colour. The
PNGs are now rendered from the SVG mark by `npm run icons` (Inkscape,
no image library in node_modules): the bare mark for the 32px favicon,
the mark on the stage colour for app icons, and a maskable variant with
the extra margin Android's adaptive crop needs.
Gold is the primary in both themes, Bricolage Grotesque carries the
wordmark, titles and the score, and Movie A / Movie B wear the blue and
magenta of three-strip Technicolor on their posters instead of colouring
whole rows. The loop closes on screen - connectors light, a gold line
draws back to Movie A, the row glows, then the result arrives - in place
of confetti, on every play mode, scaled to the chain's length. Every
page shares one header and hairline lists; copy is sentence case with
one verb per flow, and the Home hero shows a real finished loop.
Also clamps link counts at zero: an opponent who had not moved yet
showed "-1 links".
WP-17. Minor: push notifications become usable and nothing a player could
reach was removed. The admin console's jobs need backend 3.0.0.
Push (M35): the whole browser half - the service worker's handlers, the
subscribe/unsubscribe calls, the server's VAPID key - had shipped with
nothing calling it, so no browser ever held a subscription. The toggle
lives in Profile because the permission prompt has to come from a click.
The old isPushSubscribed awaited serviceWorker.ready, which never settles
with no worker registered; it uses getRegistration() now.
Admin: each job pages and the button becomes Continue with the server's
cursor; the recalculate card also stopped printing "undefined leaderboard
entries" for a field the server had not sent for several releases. The
three copy-pasted job cards are one component.
Consolidation: the four game screens share use-game-screen and one search
panel; the review page's static chain is ChainDisplay in a static mode
(21 rendered states verified byte-identical); formatTime and error
extraction exist once; Endless and quick play share a franchise-aware
picker instead of two divergent sort(() => Math.random() - 0.5) copies.
The dead api/index barrel and the fourteen functions only it referenced
are gone. noUncheckedIndexedAccess is on. Whiplash's TMDB id is corrected
and every preset title is checked against TMDB.
App.tsx imported all 24 pages statically, so a first-time visitor
downloaded the admin panel, three multiplayer modes and the preset-pair
table before the landing page could render - one 682 KB chunk. Every
page except Home is now React.lazy behind a Suspense boundary: 56
chunks, pages at 2-11 KB. Verified in a browser against the production
build - the landing page fetches only the app and vendor chunks, a
navigation fetches that route's chunks and renders, and a deep link into
an authenticated route lazy-loads it, redirects to sign-in, and
lazy-loads that too.
Vendor code is split into vendor-react, vendor-router, vendor-ui and
vendor-net via manualChunks. The main chunk was 95% dependencies, so the
first load is not much smaller - the dependency set is the load - but
the chunk that changes on every deploy is now 50 KB (14 KB gzipped)
rather than 682 KB. A returning visitor after a deploy re-downloads the
app, not React.
The favicon was 888 KB: PNG data under a .jpg name, 768x768, with a
fully opaque alpha channel, fetched on every page load. It is a 2.6 KB
32px PNG now, with a 180px apple-touch-icon, and manifest.json points at
real 192px and 512px PNGs with the correct type instead of declaring the
same mislabelled file at both sizes. The service worker's notification
icon pointed at a /favicon.ico that never existed.
The useGameStore selector item from M26 was already done in WP-10.
Addresses M26.
react-hooks/set-state-in-effect and react-hooks/purity had been demoted
to warnings "so CI passes", and CI ran without --max-warnings 0, so all
22 violations were invisible there. Both rules are back at error.
- Confetti is rolled once per mount instead of during render, where it
re-rolled on every re-render and made pieces visibly jump.
- Loading flags are derived from a "last settled request" key rather than
set synchronously inside an effect. That also adds the cancellation
these fetches were missing, so a resolved request can no longer write
into a unmounted or superseded view.
- Effect dependency arrays corrected; the two eslint-disable-line
comments in Profile.tsx are gone, and the split of its two effects
removes the guard that silently never re-ran.
- GameCompletionModal derives visibility from status instead of mirroring
it into state, with a ref so the sound and localStorage write fire once.
- ValidationFeedback adjusts state during render, the documented pattern,
instead of paying an extra committed render per link.
- The three react-refresh suppressions in ui/ are resolved properly by
extracting the variants into their own modules, so the component files
export only components. badgeVariants had no consumers and is no longer
exported.
- Drops a stale eslint-disable directive in sw.js.
The lint script now fails on warnings so this cannot quietly regress.