frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 7s
frontend-ci / fs-scan (push) Successful in 14s
frontend-ci / typecheck (push) Successful in 24s
frontend-ci / lint (push) Successful in 26s
frontend-ci / test (push) Successful in 32s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Skipped
frontend-ci / lint (pull_request) Successful in 24s
frontend-ci / test (pull_request) Successful in 31s
frontend-ci / secrets-scan (pull_request) Successful in 5s
frontend-ci / sast (pull_request) Successful in 8s
frontend-ci / fs-scan (pull_request) Successful in 15s
frontend-ci / typecheck (pull_request) Successful in 14s
frontend-ci / build (pull_request) Successful in 38s
frontend-ci / push (pull_request) Skipped
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.
560 lines
28 KiB
Markdown
560 lines
28 KiB
Markdown
# Changelog
|
|
|
|
All notable changes to this project will be documented in this file.
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
> History prior to 1.1.0 has not yet been backfilled from git.
|
|
|
|
## [Unreleased]
|
|
|
|
## [2.4.0] - 2026-09-12
|
|
|
|
### Added
|
|
|
|
- The app icon: Safari, the iOS home screen, Android install and push
|
|
notifications now show the Movie Loop mark on the stage colour instead of
|
|
the old film-reel artwork, with a maskable variant so Android no longer
|
|
letterboxes it. `npm run icons` re-renders every PNG from the SVG sources
|
|
with Inkscape.
|
|
|
|
## [2.3.0] - 2026-09-12
|
|
|
|
A look-and-feel pass. The app now has one visual identity - Movie Loop - with
|
|
the closed loop as its only bold element.
|
|
|
|
### Added
|
|
|
|
- A display typeface (Bricolage Grotesque) for the wordmark, page titles, the
|
|
puzzle statement and the score, alongside Geist for everything else, and a
|
|
declared type scale with a 12px floor.
|
|
- A logo mark - an open ring with the missing link in the gap - beside the
|
|
wordmark, on the sign-in and 404 pages, in the completion dialog, and as an
|
|
SVG favicon.
|
|
- The loop closes on screen: connectors light up top to bottom, a gold line
|
|
draws from the last row back to Movie A, the closing row glows, and the
|
|
result arrives after - in solo, versus, async and game-night play alike. The
|
|
sequence scales with the chain's length. Under reduced motion the finished
|
|
loop simply appears and the result opens at once.
|
|
- The Home page shows a real finished loop (Speed to Apocalypse Now and back)
|
|
in place of the how-to-play card; the ? in the header keeps the full rules.
|
|
- Posters and photos fall back to placeholder art if TMDB no longer serves
|
|
them, instead of showing a broken image.
|
|
- `PageHeader`, `MovieMarker` and `LogoMark` components; difficulty badges now
|
|
carry ordinal pips.
|
|
|
|
### Changed
|
|
|
|
- Gold is the primary colour in both themes; it no longer flips to indigo in
|
|
light mode. The dark background is a true indigo-slate rather than near-black.
|
|
- Movie A and Movie B are identified by the poster's ring and an A/B chip, in
|
|
the blue and magenta of three-strip Technicolor, instead of colouring whole
|
|
rows. Green is reserved for online/ready states and no longer means "correct".
|
|
- The timer, difficulty badges, trophies, win/lose states and score lines use
|
|
the palette tokens instead of stock red/green/yellow.
|
|
- The game screen states the puzzle in a sentence ("Toy Story to Cast Away and
|
|
back.") with the timer, link count and next objective on one line; the two
|
|
movie cards that repeated the top of the chain are gone. Posters in the chain
|
|
are larger.
|
|
- Home, Daily, Leaderboard, Profile, Achievements, Endless, Custom, Login, Game
|
|
review, Versus, Game night, Friends and Admin share one left-aligned page
|
|
header, hairline lists instead of card grids, and sentence-case copy. Profile
|
|
shows the streak large and the rest of the record as a ledger, with account
|
|
settings last.
|
|
- Copy: "Quick Play" is "Play now", "Ranks" is "Leaderboard", "Passed Movie B"
|
|
is a changing objective ("Next: reach …", "Next: back to …", "Loop closed"),
|
|
"Connected!" is "Linked", "Congratulations!" is "Loop closed.", and empty
|
|
states say what to do next.
|
|
- The daily page shows the "not generated yet" state as an ordinary notice
|
|
rather than red error text.
|
|
|
|
### Fixed
|
|
|
|
- An opponent who has not moved yet showed "-1 links"; an empty chain now
|
|
counts as zero.
|
|
|
|
### Removed
|
|
|
|
- The confetti overlay; the loop-closing sequence replaces it.
|
|
- Stock Tailwind palette colours from game components, uppercase table headers
|
|
and labels, and the `text-7xl`/`text-8xl` sizes.
|
|
|
|
## [2.2.3] - 2026-09-12
|
|
|
|
The first production deploy of the push opt-in.
|
|
|
|
### Fixed
|
|
|
|
- **Enabling push notifications no longer fails with "Validation failed".**
|
|
The browser's `PushSubscription.toJSON()` includes `expirationTime`, and it
|
|
was being sent verbatim to a server schema that is deliberately strict about
|
|
unknown fields. The client now sends exactly what the server accepts -
|
|
`endpoint` and `keys` - and refuses an incomplete subscription rather than
|
|
sending it. (The server also accepts the field now; either side alone fixes
|
|
it.) Nothing had ever called this path before 2.2.0, and the test doubles
|
|
were shaped like the schema rather than like a browser.
|
|
|
|
### Changed
|
|
|
|
- **The CSP hash for the inline theme script is computed at build time** from
|
|
the `index.html` that is actually shipped, copied into the image and read at
|
|
start, instead of being hardcoded in the container's start-up script behind
|
|
a comment asking for it to be regenerated by hand. The bytes are what the
|
|
browser hashes, so an edit to the script - or a checkout with different line
|
|
endings - would have silently blocked the page's own theme script. The
|
|
container now refuses to start without the file rather than serve a policy
|
|
that blocks its own page. Verified against the built image: the header's
|
|
hash equals the hash of the served script. (Investigating a CSP console
|
|
message in production is what surfaced this; the served page turned out to
|
|
be correct, and that message came from a browser extension's injected
|
|
script, which the policy is right to refuse.)
|
|
- Every text file is checked out with LF on every platform (`.gitattributes`).
|
|
|
|
## [2.2.2] - 2026-09-12
|
|
|
|
Found by playing the app end to end against a clean-install development stack
|
|
before the first push - a Quick Play, a Custom Game loop closed and scored, a
|
|
hint, an undo, and every anonymous route.
|
|
|
|
### Fixed
|
|
|
|
- **No more stale results after picking an actor or movie.** Selecting an
|
|
actor clears the input and flips the search to movie mode, but the 300ms
|
|
debounce still held the actor's name, so a *movie* search for "Daniel Craig"
|
|
ran and opened a listbox of results under an empty box. An empty input now
|
|
means nothing to search, whatever the debounce holds.
|
|
- **The hint button says what a hint costs.** It read "-150 pts"; the server
|
|
charges 200 (150 is the duplicate penalty).
|
|
- **A day with no daily challenges yet reads as what it is.** The API answers
|
|
404 before the midnight job has run - a fresh install, or a failed job - and
|
|
the page called that "Failed to load today's challenges", which reads as an
|
|
outage. It now says they have not been generated yet and when they will be.
|
|
- Requests the page abandoned on purpose (navigating away mid-fetch aborts
|
|
them) are no longer logged as `[API Error] canceled` - two per page change,
|
|
burying the real ones.
|
|
|
|
## [2.2.1] - 2026-09-12
|
|
|
|
The frontend test foundation (WP-19: the frontend half of L1), and the defects
|
|
it turned up.
|
|
|
|
### Fixed
|
|
|
|
- **Joining an open Game Night room that refuses you shows why.** The error was
|
|
set but only rendered inside the password prompt, which a passwordless join
|
|
never opens - WP-10 fixed the password path and left this one silent.
|
|
- **The remove-friend button has an accessible name** ("Remove <name> as a
|
|
friend"). It rendered only an icon, the one icon-only control the
|
|
accessibility pass missed; a screen reader announced "button" next to a
|
|
destructive action.
|
|
- **Signing out from Profile lands on the home page**, as the button intends.
|
|
The page's own auth guard re-ran the moment the user cleared and sent them
|
|
to the sign-in screen instead.
|
|
- **The sign-in page redirects someone who is already signed in** to their
|
|
profile, and **says so when the session expired**: the API client has sent
|
|
expired sessions to `/login?expired=1` since WP-10, and nothing read it, so
|
|
being signed out by an expired token looked identical to signing out.
|
|
- **Starting a game reports failure instead of losing it.** Quick Play and
|
|
Endless awaited the TMDB pick with no catch - a failed request, or a page
|
|
with fewer than two usable films, was an unhandled rejection with nothing on
|
|
screen - and the button only disabled itself during `startGame`, so a second
|
|
click during the pick started a second game. Every start page (Quick Play,
|
|
Endless, Custom, Daily) also navigated to `/game` whether or not the game
|
|
had started; a failure bounced off that page's idle redirect with the
|
|
store's message never displayed. `startGame` now reports `'started'` or
|
|
`'failed'`, and each page shows the reason and stays put.
|
|
|
|
### Changed
|
|
|
|
- `npm run test:cov` is what CI runs: the 90% thresholds codified in WP-14 are
|
|
enforced, having been cleared - 14% to 99% lines, 12% to 97% branches, 105
|
|
to 1,112 tests across 93 files. Every source file is above the bar on its
|
|
own; there are no per-file exceptions.
|
|
- The pair picker shuffles by random key instead of an in-place Fisher-Yates,
|
|
removing three index guards that could not be reached.
|
|
|
|
## [2.2.0] - 2026-09-11
|
|
|
|
Repository hygiene (WP-17: L2, L4, M9, M35, the frontend half of the audit's
|
|
section 3 prune list). Minor: push notifications become usable, and nothing a
|
|
player could reach was removed. The admin console's maintenance jobs need
|
|
backend 3.0.0.
|
|
|
|
### Added
|
|
|
|
- **Push notifications can be turned on.** Profile → Notifications has the
|
|
toggle. The whole browser half of the push stack - the service worker's
|
|
`push` and `notificationclick` handlers, the subscribe/unsubscribe calls,
|
|
the server's VAPID key - had shipped for months with nothing calling it, so
|
|
no browser ever held a subscription (M35). The prompt is asked from a click,
|
|
as browsers require, and a refusal or a server without VAPID keys is
|
|
explained in place rather than failing silently. If a subscription cannot be
|
|
registered with the server, the browser-side one is rolled back so the two
|
|
never disagree.
|
|
- The admin console's "Backfill Game Owners" card, for the job that re-attaches
|
|
legacy games to their players. It reports how many were ambiguous - more
|
|
than one player matched, so the server made no guess - alongside the count
|
|
it re-attached.
|
|
- Tests for the push library, the subscription hook, the notifications
|
|
section and the pair picker (45 tests; 105 in total).
|
|
|
|
### Changed
|
|
|
|
- Admin maintenance jobs continue where they left off. Each one processes a
|
|
page and, when more remains, the button becomes **Continue** and passes the
|
|
server's cursor back. The three near-identical job cards are one component.
|
|
The recalculate card also stopped printing "undefined leaderboard entries":
|
|
its type declared a field the server had not sent for several releases.
|
|
- Endless mode and quick play pick pairs through one franchise-aware picker.
|
|
Endless had its own copy with no franchise check, so it could serve *The
|
|
Fellowship of the Ring* against *The Two Towers*; both copies also shuffled
|
|
with `sort(() => Math.random() - 0.5)`, which is neither uniform nor a valid
|
|
comparator.
|
|
- The four game screens share one hook for the store wiring and label
|
|
derivation they had each copied, and one search panel; the review page's
|
|
read-only chain display is the live `ChainDisplay` in a static mode rather
|
|
than a second component that had drifted from it. Verified byte-identical
|
|
DOM across 21 rendered states before and after.
|
|
- `formatTime` and error-message extraction each exist once (`lib/format`,
|
|
`lib/error`) instead of five and three times.
|
|
- `noUncheckedIndexedAccess` is on. Indexing now types as possibly undefined
|
|
and the code handles it, with no non-null assertions added. Along the way,
|
|
taking an initial with `[0]` became `.charAt(0)`, which also stops an empty
|
|
display name from throwing.
|
|
- The package is named `movie-loop-frontend`; it had kept the scaffold's
|
|
`frontend-temp`.
|
|
- CI refuses to release a version whose `v<version>` git tag already exists on
|
|
origin, before it builds anything, for the same reason the backend does.
|
|
- The stock Vite README is replaced with one that describes this app.
|
|
|
|
### Fixed
|
|
|
|
- *Whiplash* in the preset pairs is TMDB id 244786; the table had 274870, a
|
|
different film. Every preset title is now checked against what TMDB serves
|
|
for its id.
|
|
- Checking whether push is subscribed no longer awaits
|
|
`navigator.serviceWorker.ready`, which never settles when no worker is
|
|
registered - the check hung on any page load before registration.
|
|
- `getErrorMessage` returns its fallback for a server error carrying an empty
|
|
message array, rather than `undefined`.
|
|
|
|
### Removed
|
|
|
|
- The dead `src/api/index.ts` barrel and the fourteen API functions it alone
|
|
referenced, three orphaned types, the Vite scaffold SVGs, the unused
|
|
`scroll-area` primitive and four unused dialog/sheet exports, and the
|
|
`_onGameStart` escape hatch that two stores read but nothing ever set.
|
|
Nothing a user could reach.
|
|
|
|
## [2.1.0] - 2026-09-11
|
|
|
|
Accessibility (WP-16: audit section 2.8).
|
|
|
|
### Added
|
|
|
|
- `eslint-plugin-jsx-a11y` (recommended rules) in the lint step, so an orphaned
|
|
label, a clickable `<div>` or a missing ARIA attribute fails CI instead of
|
|
shipping. It found 19 violations on arrival; all are fixed, none suppressed.
|
|
- A "Skip to content" link on every page, visually hidden until focused, that
|
|
moves focus to the main region.
|
|
- Live regions where state used to change silently: link validation announces
|
|
"Validating", a rejection (as an alert) and "Connected"; the versus and
|
|
game-night lobby countdowns are assertive timers, so a screen-reader user is
|
|
no longer dropped into a running match with no warning; the game-night
|
|
player list announces players finishing or forfeiting.
|
|
- Tests for the notification popover's keyboard contract (9 tests).
|
|
|
|
### Changed
|
|
|
|
- **The notification bell is a real popover.** It was a `<div>` with
|
|
`<div onClick>` rows: nothing in it was focusable, nothing announced, no way
|
|
to close it from the keyboard. The trigger now reports its state and the
|
|
unread count in its name (`Notifications, 3 unread`), the panel is a labelled
|
|
dialog the trigger controls, every notification is a button, the per-row
|
|
controls have real names rather than tooltips, Escape closes and returns
|
|
focus to the trigger, and the unread dot has text behind it.
|
|
- **The Challenge Friend overlay is the UI kit's `Dialog`.** It was a fixed
|
|
`<div>` with no role, no focus trap and no Escape - Tab went straight through
|
|
it into the page behind. Base UI supplies the dialog role, `aria-modal`,
|
|
focus trapping, Escape and focus restoration.
|
|
- The search combobox follows the ARIA 1.2 pattern: `role="combobox"` with
|
|
`aria-expanded`/`aria-controls` on the input itself, rather than the 1.1
|
|
wrapper-`div`-with-`aria-owns` form that current screen readers announce
|
|
inconsistently. Verified live: the state flips and `aria-controls` resolves
|
|
to the listbox.
|
|
- Every form control has a label. Sign-in/sign-up, the three change-password
|
|
fields (which had only placeholders, and placeholders vanish once you type),
|
|
the admin challenge form, custom game, game night's room form (including a
|
|
join-password field that had nothing at all), the versus lobby form, the
|
|
friend search and the chat message box. Labels above groups of buttons
|
|
("Difficulty", "Time Limit", "Movie A") are group labels via
|
|
`role="group"`/`aria-labelledby` rather than a faked `htmlFor`.
|
|
- Every icon-only button has a name that says what it does: chat send, back,
|
|
pagination, display-name save/cancel, notification actions, and three versus
|
|
buttons whose content collapsed to a bare spinner while loading.
|
|
- The game header's "Passed Movie B" read the same text in both states - only
|
|
the icon and colour changed - so a screen reader heard the wrong thing until
|
|
B was reached. It now announces "Movie B reached" or "not yet reached", and
|
|
the online indicator on the friends list has text behind the green dot.
|
|
- `autoFocus` is gone from the friend search. It skipped screen-reader users
|
|
past the page heading; the field is one Tab away.
|
|
- **Reduced motion is respected globally.** A single stylesheet rule collapses
|
|
every animation and transition when the OS preference is set; the confetti
|
|
overlay does not render at all under it, since a still frame of confetti has
|
|
no meaning.
|
|
|
|
|
|
## [2.0.1] - 2026-09-11
|
|
|
|
Bundle and asset budget (WP-15: M26).
|
|
|
|
### Added
|
|
|
|
- A test suite, where there was none: vitest with `@testing-library/react` and
|
|
jsdom, run by `npm test`, with coverage via `npm run test:cov`. The first
|
|
tests are the regression net for the audit's C1 - every multiplayer mode
|
|
used to leave the completion modal on "Calculating score..." forever because
|
|
no backend session was ever created - and for the chain-validation state
|
|
machine in both its server-validated and anonymous client-side forms. 51
|
|
tests. Coverage thresholds of 90% (lines, branches, functions, statements)
|
|
are codified in `vite.config.ts`; the suite does not meet them yet, so
|
|
`test:cov` fails honestly while CI runs the plain `npm test`. Tests intercept
|
|
the axios client rather than the API modules, because a second concurrent
|
|
dynamic `import()` of a mocked aliased module resolved to the real file under
|
|
vitest - measured, and it reached the development backend once.
|
|
- A `test` job in CI.
|
|
|
|
### Changed
|
|
|
|
- **Every page except Home loads on demand.** `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. Routes are now `React.lazy` behind a `Suspense`
|
|
boundary, giving 56 chunks with pages at 2-11 KB each. Verified in a browser
|
|
against the production build: the landing page fetches only the app chunk and
|
|
vendor chunks; navigating to a route fetches that route's chunks and renders;
|
|
a deep link into an authenticated route lazy-loads it, redirects to sign-in,
|
|
and lazy-loads that too.
|
|
- **Vendor code is split into separately cached chunks** (`vendor-react`,
|
|
`vendor-router`, `vendor-ui`, `vendor-net`) via `manualChunks`. The first
|
|
load is not much smaller - the main chunk was 95% dependencies, and 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, so a returning visitor after a
|
|
deploy re-downloads the app, not React. Vite's 500 KB chunk warning no longer
|
|
fires.
|
|
|
|
### Fixed
|
|
|
|
- The favicon was an 888 KB image - PNG data under a `.jpg` name, 768x768 with
|
|
a fully opaque alpha channel - fetched on every page load. It is now a 2.6 KB
|
|
32px PNG, with a 180px `apple-touch-icon` for iOS home screens.
|
|
- `manifest.json` declared that same file as both its 192x192 and 512x512 icon,
|
|
typed `image/jpeg`. It now points at real 192px and 512px PNGs with the
|
|
correct type, so installs get a correctly sized icon instead of a mislabelled
|
|
one the browser has to rescale.
|
|
- The service worker's push notifications referenced `/favicon.ico`, which never
|
|
existed; a push with no icon in its payload rendered blank. They use the 192px
|
|
icon.
|
|
|
|
### Known issues
|
|
|
|
- The `useGameStore()` selector item from M26 was already resolved in WP-10;
|
|
the chain-validation hook subscribes per field.
|
|
- Not done, recorded for later: `socket.io-client` (~90 KB of `vendor-net`)
|
|
ships to anonymous visitors because the page chrome's notification bell
|
|
reaches the notification store, which imports it statically. Loading the
|
|
socket client on demand would mean an async `connect()` in four stores - a
|
|
change in its own right, not a bundle setting.
|
|
|
|
## [2.0.0] - 2026-09-10
|
|
|
|
Container and response-header hardening (WP-13: M31, M34).
|
|
|
|
### ⚠ Breaking
|
|
|
|
- **The production container now listens on 8080, not 80.** nginx runs as an
|
|
unprivileged user, and a non-root process cannot bind a privileged port without
|
|
`CAP_NET_BIND_SERVICE`, which compose drops. Anything routing to this container
|
|
must be repointed: in the homelab Traefik config that means
|
|
`traefik.http.services.movieloop.loadbalancer.server.port` changes from `80` to
|
|
`8080`. The published mapping in `docker-compose.prod.yml` is updated here, but
|
|
the Traefik label lives outside this repo and must be changed by hand.
|
|
|
|
### Fixed
|
|
|
|
- Security headers are no longer dropped from most responses. nginx's
|
|
`add_header` is not additive across levels: a block declaring any `add_header`
|
|
of its own discards every header inherited from its parent. `location /assets/`
|
|
and `location = /config.js` each declared one, so `X-Frame-Options`,
|
|
`X-Content-Type-Options` and `Referrer-Policy` were silently absent from every
|
|
JavaScript and CSS response - which is most of what a browser loads. The
|
|
headers now come from a snippet included in each location. Verified against the
|
|
built image: 6 of 6 headers present on `/`, on a hashed `/assets/*.js` bundle,
|
|
and on `/config.js`.
|
|
- `config.js` no longer interpolates `API_URL` unescaped. This used `envsubst`,
|
|
which does no escaping, so a value containing a double quote closed the
|
|
JavaScript string literal and everything after it became executable script on
|
|
every page load. The value is now escaped when rendered. Operator-supplied, so
|
|
not a likely attack - but the page hands its auth token to whatever origin that
|
|
value resolves to, and a malformed value is now inert rather than executable.
|
|
|
|
### Added
|
|
|
|
- A Content-Security-Policy, plus `Strict-Transport-Security` and
|
|
`Permissions-Policy`. With the JWT in `localStorage`, CSP is the main
|
|
compensating control against injected script and there was none. `connect-src`
|
|
is derived at container start from `API_URL`, because the API is not
|
|
same-origin in production - the app is served from one subdomain and the API
|
|
from another, so naming it explicitly is required or every request and
|
|
websocket would be blocked. The inline theme script in `index.html` runs before
|
|
first paint to avoid a flash of the wrong theme and so cannot move to a file;
|
|
it is allowed by SHA-256 hash rather than by `unsafe-inline`, which would have
|
|
bought almost nothing. **If that script is edited its hash must be regenerated
|
|
in `docker/40-render-config.sh` or the page will not apply its theme.**
|
|
|
|
### Changed
|
|
|
|
- The production image is based on `nginxinc/nginx-unprivileged:alpine` and runs
|
|
as uid 101 rather than root. It serves static files and never needed root.
|
|
Verified in the built image: `uid=101(nginx)`.
|
|
- `envsubst` is no longer installed, since the entrypoint renders `config.js`
|
|
itself in order to escape the value.
|
|
|
|
## [1.3.0] - 2026-09-10
|
|
|
|
Session handling and effect correctness.
|
|
|
|
### Fixed
|
|
|
|
- An expired or revoked session no longer leaves the app half signed in. The
|
|
response interceptor only wrote to the console, so the nav still showed your
|
|
name while every request quietly failed. A 401 now clears the token and
|
|
returns you to sign-in. Signing in with the wrong password is unaffected -
|
|
that is an answer, not an expired session.
|
|
- Signing out ends the whole session. It disconnected the notifications socket
|
|
and nothing else, leaving the chat, versus and game night sockets open with a
|
|
token that had just been removed, and the friends and game stores populated -
|
|
so signing in as someone else on the same tab showed the previous person's
|
|
friend list and pending-request badge.
|
|
- Leaving a match no longer carries its result into the next one. Disconnecting
|
|
cleared only the socket, so the match id, result and score survived and the
|
|
next match opened showing the previous match's completion modal.
|
|
- Reconnecting no longer duplicates everything. All four sockets tested whether
|
|
the existing connection was *connected* rather than whether it existed, so one
|
|
that was mid-reconnect got replaced without being closed; the abandoned socket
|
|
kept its own handlers and delivered every event a second time.
|
|
- Game night spectators are detected again. The check decoded the session token
|
|
with `atob`, which rejects the base64url encoding tokens actually use, so it
|
|
threw inside a socket handler and the spectator flag was never set.
|
|
- Reloading your profile no longer discards a game in progress. Effects depended
|
|
on the whole user object, which is rebuilt on every profile load, so they
|
|
re-ran when nothing had changed - and in an async match the re-run reset the
|
|
board mid-game.
|
|
- Undo is applied in order and no longer drifts from the server. Rapid undos
|
|
fired overlapping requests at a server that removes one link per call, and a
|
|
failure was only logged while the local chain had already been shortened.
|
|
- Leaving a page cancels its in-flight requests instead of letting them land
|
|
afterwards.
|
|
- The friends page no longer removes other components' socket listeners when it
|
|
unmounts.
|
|
- Copying a result no longer leaves a timer running after the page closes.
|
|
- Share links point at the deployment that produced them, rather than always at
|
|
production.
|
|
|
|
### Security
|
|
|
|
- The runtime API setting is escaped when written into the page. It was
|
|
substituted raw, so a value containing a quote ended the string and the rest
|
|
became executable JavaScript on every page load - and the page hands its auth
|
|
token to whatever that value resolves to. A malformed value is now also
|
|
rejected on read, falling back to the default.
|
|
|
|
### Removed
|
|
|
|
- The difficulty selector in Endless mode. It was never applied to anything:
|
|
difficulty is derived from cast analysis on the server, and Endless builds its
|
|
pairs in the browser, so the control silently did nothing. The genre and
|
|
decade filters are unaffected.
|
|
|
|
## [1.2.3] - 2026-09-10
|
|
|
|
### Fixed
|
|
|
|
- The async challenge leaderboard no longer shows an empty heading to anyone
|
|
who has not played that match. The backend now withholds the movie pair from
|
|
non-participants - the pair is the puzzle - so the two titles arrive absent
|
|
rather than empty, and the page rendered a bare separator between nothing.
|
|
It now says the movies are hidden until you play, and the type reflects that
|
|
they are optional.
|
|
|
|
## [1.2.2] - 2026-09-09
|
|
|
|
### Fixed
|
|
|
|
- Shell scripts and Dockerfiles are pinned to LF so a Windows checkout no longer
|
|
copies CRLF line endings into the container image. `docker/40-render-config.sh`
|
|
was affected; nginx sources it rather than executing it, so nothing was visibly
|
|
broken, but the file was wrong in the image.
|
|
|
|
## [1.2.1] - 2026-09-09
|
|
|
|
### Fixed
|
|
|
|
- All high-severity dependency advisories cleared (`npm audit`: 23 -> 0),
|
|
entirely within existing semver ranges.
|
|
- Five further `set-state-in-effect` violations caught by the updated
|
|
react-hooks plugin, including search state that was cleared on every
|
|
keystroke below the search threshold, and an async submit guard that re-ran
|
|
its own effect.
|
|
|
|
### Changed
|
|
|
|
- In-range dependency updates, notably react 19.2 -> 19.3 and
|
|
`@base-ui/react` 1.2 -> 1.8.
|
|
- `shadcn`, `tailwindcss`, `@tailwindcss/vite` and `tw-animate-css` moved to
|
|
devDependencies. They are build-time only - `shadcn` is a scaffolding CLI
|
|
with no runtime imports - but as runtime dependencies they pulled `msw`,
|
|
`@babel/core` and `ts-morph` into the production dependency tree that
|
|
vulnerability scanning reads.
|
|
|
|
## [1.2.0] - 2026-09-09
|
|
|
|
### Changed
|
|
|
|
- Game completion, hints and all versus submissions follow the server's
|
|
authoritative scoring contract: completion posts no payload, hints go through
|
|
the server (which also records them), and match submissions send a session id
|
|
rather than a score.
|
|
|
|
### Fixed
|
|
|
|
- Stale hints are cleared when the chain changes; the previous movie's cast
|
|
used to stay on screen after picking an actor another way.
|
|
|
|
## [1.1.0] - 2026-09-09
|
|
|
|
### Fixed
|
|
|
|
- Versus, Game Night and Async Versus can complete a match again. None of the
|
|
three ever created a backend game session, so no score was produced, nothing
|
|
was submitted, the opponent was never notified, and the completion modal had
|
|
no way out.
|
|
- Chain links may be reused. Repeating an actor or movie was rejected outright
|
|
instead of being allowed and penalised, which could strand a player mid-chain.
|
|
Reuse now warns that it costs points, and the penalty appears in the score
|
|
breakdown.
|
|
- A failed async score submission is no longer retried on every render.
|
|
- Confetti no longer re-randomises mid-flight.
|
|
- Page data fetches are cancelled on unmount, so a late response cannot write
|
|
into a view the player has left.
|
|
- Profile stats and history load independently, fixing a guard that could stop
|
|
the stats request from ever re-running.
|
|
|
|
### Changed
|
|
|
|
- The dev container starts the Vite server correctly and now picks up source
|
|
edits, so hot reload works.
|
|
- The lint CI job fails on warnings, and the two React hook rules that had been
|
|
demoted to warnings are back at error.
|