docs: record fresh session ids, the daily pick rules and the new components

This commit is contained in:
tiennm99 committed 2026-09-28 15:01:41 +07:00
1 parent 4d25a82448
commit b8b4ffbbcb
3 files changed
+24 -9

No files matched your search

+2 -1
View File
@@ -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.
+10 -6
View File
@@ -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
+12 -2
View File
@@ -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