Skip to content

feat(games): add game status pills and personal score on the game page - #55

Merged
kris1027 merged 15 commits into
mainfrom
feat/game-status-rating
Oct 24, 2026
Merged

kris1027 merged 15 commits into
mainfrom
feat/game-status-rating

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

Signed in users can now set a game's status on /games/{id} (Want to play, Playing, Finished) and give it a personal score from 1 to 10. Clicking the pill that is already on removes the game. Removing a game, or losing a score by moving back to Want to play, shows a toast with Undo.

Why

Implements spec 0026, scope feature G4 in docs/scope/games.md. It builds on the user_game_state table from spec 0024 and the game page from spec 0025. Options and reasoning are in rationale.md.

Changes

  • Database: a new migration 20261024120000_game_tracking_functions.sql adds five security invoker functions: set_game_status, rate_game, remove_game, restore_game_status and restore_removed_game. Each is granted to authenticated only and writes auth.uid() itself, so RLS still applies. set_game_status locks the row before it reads the old values, so two tabs never get the same Undo payload. The restores refuse (P0002) outside a 10 minute window. A score on a game with no row, or on Want to play, is refused with a new code, BS004. There is no new table or column.
  • Server Actions (app/games/actions.ts): setGameStatus, setGameRating, removeGame, restoreGameStatus and restoreRemovedGame. They share one order: Zod parse, then the session check, then the IGDB check and the release check, then the write under the user's JWT, then refresh(). None of them throws or redirects. IGDB is only called when a write could move a game forward. Removing, clearing a score and both restores skip IGDB, so they still work during an IGDB outage, on a hidden game, or before release. Clearing a score only touches a row that has a score.
  • Release gate: before a game's first release (UTC day), Playing, Finished and scoring are disabled on the page, and the actions also refuse them with not_released. A pressed pill on an unreleased game can still be clicked to remove it.
  • UI: GameTrackingSlot and GameStatusControl go in the hero's tracking slot, shaped like the movie pills. The control updates optimistically through useOptimistic and the pure reducer applyGameIntent. Moving into Finished opens the score picker when the game has no score yet. Failures roll back and show the matching toast, including a session expired toast with Sign in. Visitors see nothing in the slot, and the page shell stays prerendered.
  • Shared tracking code: game types, schemas, copy, error classification (classifyGameTrackingError) and game_tracking.* log events in lib/tracking/. The release helpers live in lib/catalog/game-page.ts.
  • Tests: pgTAP 190-game-tracking-functions and new grant checks in 020-security-configuration. Vitest covers the actions, the reducer, schemas, errors, the control, the slot and the release helpers. Every new action is registered in private-actions-boundary.test.ts.

How to test / verify

  • Start the local stack with supabase db reset, then pnpm dev:docker. Sign in as user-a@example.test. The seed has 1942 as Playing with score 8.
  • Open /games/1942. The pills show Playing pressed with score 8, and the state doesn't flicker on load.
  • Click Finished: the score stays. Click Want to play: the score disappears, and the toast offers Undo, which brings back Finished, 8 and the exact dates.
  • Click the pressed pill: the game is removed, and Undo brings it all back.
  • On an unreleased game, you see Releases {date} or Release date to be announced, and Playing and Finished are disabled.
  • Commands: pnpm test:db, pnpm db:types:check, pnpm typecheck, pnpm lint, pnpm test and pnpm build. The full manual list is in verify.md, and every step is checked off there.

Risk & rollout

  • One additive migration (functions only). Per the verify record, it has already been applied to the cloud project with supabase db push, as docs/deploy.md asks before merge.
  • To roll back, revert the PR. The functions can stay, because nothing else calls them.
  • No feature flags. The GAMES tab stays hidden until G5, so only someone who opens /games/{id} directly sees this.
  • Review: approve with nits. The three minor findings are fixed in 60e1de9: Undo rows are typed from the generated types, clearing only touches a scored row, and the spec now matches the migration. Two nits are open on purpose: a comment on why opening the picker outside the transition is safe, and a separate log outcome for errors in readGame that don't come from IGDB.

Notes for reviewers

  • Worth a close look: the row lock in set_game_status, and how bestats.restore_game is turned back off on every path, including before a raise (the migration).
  • The page decides the release line with requestTodayUtc(), and the action uses todayUtc(new Date()). Around UTC midnight they can disagree, so an enabled pill can still return not_released. The spec accepts this (AC-9).

🤖 Generated with Claude Code

kris1027 and others added 10 commits October 24, 2026 11:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y a scored game

The set_game_status and remove_game rows were cast to hand written shapes,
so a renamed SQL column compiled and the Undo silently disappeared. They now
widen the generated row, which keeps a rename a compile error.

Clearing a score now matches only a scored row: any update moves updated_at,
which restore_game_status measures its 10 minute window from, so a stale tab
could extend an old Undo. The spec now lists five functions and the restore
parameters in the migration's order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds tests for the actions, the status control and tracking slot, the
game state read, the release helpers, and the game cases of the schemas,
messages and error classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ticks the verify steps that ran, marks Verify, Test and Review done for G4
in the games scope, and adds the fresh model review findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ating

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added docs Documentation additions and updates feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components database Schemas, migrations, queries, and row-level security labels Oct 24, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStats Oct 24, 2026
@kris1027 kris1027 self-assigned this Oct 24, 2026
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bestats Ready Ready Preview Oct 24, 2026 4:30pm UTC

kris1027 and others added 5 commits October 24, 2026 18:27
Moves the session, catalog check, write, classify and refresh order out of
app/movies/actions.ts into lib/tracking/write.ts, so the game actions can
use the same copy instead of repeating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game actions now use runTrackingWrite and parseTrackingInput, read the
prev_* and removed_* columns through one readRestoreState, and call the
hidden-inclusive read readGameIncludingHidden so its name says what it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e place

GAME_STATUSES comes from the generated enum, so the Zod schema follows SQL,
and canHoldScore is the one TypeScript copy of "only Playing or Finished
holds a score". The game input schemas get their doc lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control reads GAME_STATUSES and canHoldScore, both toast sites name the
failure toast through gameTrackingToastId, showRestored replaces the
restoring thunk factory, and the release gate cites AGENTS.md Games.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names readGameIncludingHidden as the IGDB read, records the rating is not
null filter on a cleared score and why, states that a hidden game can be
scored, and describes the status pills as a fieldset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit b37c56e into main Oct 24, 2026
4 checks passed
@kris1027
kris1027 deleted the feat/game-status-rating branch October 24, 2026 16:32
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStats Oct 24, 2026

This branch was successfully deployed

1 active deployment
Preview — eab3197b Deployed Oct 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

database Schemas, migrations, queries, and row-level security docs Documentation additions and updates feature New user-facing functionality test Automated tests and test infrastructure ui Layouts, styling, and shared interface components

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant