From b8b4ffbbcbaf68eaac703ca49ca25642cdf3ee16 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 28 Sep 2026 15:01:41 +0700 Subject: [PATCH] docs: record fresh session ids, the daily pick rules and the new components --- docs/development.md | 3 ++- docs/features.md | 16 ++++++++++------ docs/project-structure.md | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/development.md b/docs/development.md index 1a75056..bf55c4f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -51,7 +51,8 @@ are the shape the code has and new code should keep. board is written, and the fan-out after it settles per level rather than all-or-nothing, because nothing after the claim can be retried. - **One module per browser-storage concern**, all reading through - `src/lib/storage.js` (never throws, notifies watchers) and rendered through + `src/lib/storage.js` (never throws, keeps a write the browser refuses in + memory for the visit, notifies watchers) and rendered through `useStoredValue` in `src/lib/use-stored-value.js`. No component seeds state from storage in an effect; the `react-hooks/set-state-in-effect` rule is an error. diff --git a/docs/features.md b/docs/features.md index 2e1527b..e1c1dd0 100644 --- a/docs/features.md +++ b/docs/features.md @@ -78,8 +78,9 @@ the name prompt and by `/api/guess`; a colon is excluded because the distance boards pack `username:distance:timestamp` into one member. Guess coordinates must be finite numbers, checked before the session is consumed so a malformed - submit costs nothing. A `sessionId` that is not a UUID is replaced on - `/api/new-game` and rejected on `/api/skip`; only server-minted ids reach the + submit costs nothing. `/api/new-game` mints a fresh session id every round + and ignores any the client offers; a `sessionId` that is not a UUID is + rejected on `/api/guess` and `/api/skip`. Only server-minted ids reach the keyspace - **Server-resolved region**: the district a panorama sits in is decided at session creation and never sent to the client; a `regionCode` in the guess @@ -95,9 +96,9 @@ `invalid-guess`, `invalid-username`, `invalid-request`) and the result dialog words each one differently, so an expired round is not reported as a failed write -- **Skipped rounds get a fresh id**: the skip request deletes the old session - without being awaited, so the next round never reuses that id -- a late - delete used to land after the new round's write and kill it +- **Every round gets a fresh id**: a reused id let a late skip delete, or a + guess claiming the previous round, land on the new round's session and kill + it - **Server-side Calculations**: all distance and scoring computed server-side using Turf.js @@ -188,7 +189,10 @@ renaming it would orphan every score already recorded under it. for 48 hours, so the Postgres draw happens once a day. The image URL is resolved from Mapillary on every request, as every round does, so a signed URL that stops working never breaks the day; a pick deleted upstream is - forgotten and the next seeded candidate takes over + forgotten and the next seeded candidate takes over. A timeout or 5xx is not + proof of deletion and keeps the pick (the request fails with a 502), so a + Mapillary blip cannot give one day two panoramas. The pick is written with + `SET NX`, so two instances that draw differently still serve the first one - **Days roll over at midnight Vietnam time** (`src/lib/daily-calendar.js`), numbered from 2026-09-20 as #1 - **Scored, counted, never credited**: `/api/daily` opens an ordinary session diff --git a/docs/project-structure.md b/docs/project-structure.md index 62199b1..ce6b9d5 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -80,7 +80,13 @@ Next.js 16 App Router structure: - `FirstRoundHint.js` - One-time how-to-play banner, rendered in flow into the panorama pane's top row via `PanoramaViewer`'s `topBarSlot` - `DonateQRModal.js` - Donation QR code modal -- `ThemeToggle.js` - Light/dark switch +- `ThemeToggle.js` - Light/dark switch; only writes the stored choice +- `ThemeSync.js` - Applies the stored theme (and OS flips while it is + 'system') to the document. Renders nothing; mounted once in the root layout +- `ShareButton.js` - Share-sheet/clipboard button with its outcome label and + screen-reader status, used by the result dialog and the daily card +- `PlayLink.js` - A game link whose plain click the home page can intercept + for the name prompt - `SoundToggle.js` - Music and sound-effect switches; `compact` renders one mute-everything button for the game header below `sm` - `MusicPlayer.js` - The background loop. Renders nothing and is mounted in the @@ -140,6 +146,8 @@ Neon Postgres, which is what the app queries at runtime. - `mapillary.js` - Mapillary lookup by image id - `player-id.js` - **Server-side only.** The anonymous `vng_pid` cookie that identifies a browser for repeat-avoidance, and nothing else +- `cookies.js` - Cookie-header parsing for plain `Request`s, shared by + `player-id.js` and `debug-access.js` - `pano-history.js` - **Server-side only.** The last 50 panoramas a player was shown, in Redis with a rolling 3-day expiry - `session.js` - Redis-based session management with 30-min expiry @@ -192,7 +200,9 @@ and `debug/pano` routes have no dedicated test file; their underlying `wait-for-srh.js` are the shared harness that lets the same files run against either the in-memory fake or a real Redis. `fake-neon.js`, `mock-neon.js` and `pano-fixtures.js` are the equivalent for the panorama store: PGlite behind the -Neon SDK boundary, loaded with small synthetic rows. +Neon SDK boundary, loaded with small synthetic rows. `mapillary-stub.js` +stubs the Mapillary Graph API around every test of a file that calls +`stubMapillary()`. `tests/e2e/` holds the Playwright smoke specs (`*.spec.js`, so vitest never collects them): the homepage picker, the username modal, one full round, and