12 Commits
Author SHA1 Message Date
tiennm99 11bfa59377 docs: codify the conventions the code follows, drop the unused tabs dependency
docs/development.md states the rules the code now enforces or keeps:
server-only modules and the import-graph test (which now also forbids
the daily pick and the region locator from client bundles), route
decides best-effort and the library never swallows, typed failure kinds
mapped to statuses, claim before write, one module per storage concern
through the guarded store, useEffectEvent for imperative callbacks,
unprefixed logical keys, three fixed region levels, and the response
contract. The parameter rule now says what it always meant: React
components destructure props. @radix-ui/react-tabs had no component
using it. Stale lines about tabs, pagination, a leaderboard migration
and a sub-second suite are corrected.
2026-09-21 16:25:53 +07:00
tiennm99 8450b75bce feat(daily): one panorama a day, the same for everyone, with a streak
/api/daily opens an ordinary session on a panorama picked
deterministically from the day and cached in Redis for two days, so
the Postgres draw and the Mapillary lookup happen once a day. Days
roll over at midnight Vietnam time. The round is scored by /api/guess
like any other and counted under its own level in the statistics.

There is no daily leaderboard: the only identity is a cookie and a
localStorage name, so a dated board would be won by whoever opened the
most private windows. The browser keeps the finished round and replays
it on a revisit, and tracks the streak of consecutive days. The home
page carries the challenge card and the result dialog shares the
outcome as a squares line.
2026-09-20 23:23:02 +07:00
tiennm99 e649259518 feat(game): explain a miss, share a round, and keep the phone header in reach
Below 3 points the result dialog says whether the guess had the right
district or province, located server-side from the boundaries. The
reveal links the spot on OpenStreetMap, and a Share button builds a
squares line with the region's URL. Region pages and the root carry
Open Graph metadata for link previews.

An expired round is worded as expired rather than as a failed save. A
skipped round gets a fresh session id so the unawaited delete cannot
kill the next round. Below sm the theme switch collapses to one cycling
button and the tally hides, so the mute control stays on a 360px
screen. The panorama viewer loads on demand, taking three.js out of
the first load.
2026-09-20 23:02:05 +07:00
tiennm99 a5c9ef5876 feat(maps): centralize tile provider with Geoapify support
Introduces map-tiles.js module to centralize tile provider logic, supporting Geoapify (paid, high quality) with API key fallback to OSM public server. Updates LeafletMap, ResultMap, and CoverageMap to use the new abstraction, improves maintainability and consistency across the codebase.
2026-09-01 09:43:19 +07:00
tiennm99 6569f095c9 feat(game): serve panorama index from Neon Postgres
- panoramas + pano_provinces tables replace 28MB bundled JSON; pano-index.js
  now draws via cached COUNT + ORDER BY id OFFSET with rejection sampling,
  composite (province,id)/(district,id) indexes support the skip
- scripts/seed-pano-db.mjs validates pipeline artifacts (the old real-data
  vitest invariants, extracted to scripts/lib/pano-artifacts.mjs), stages
  into panoramas_next, verifies, renames into place in one transaction,
  keeps panoramas_old as backup; --province reseeds in place; --check
  validates only
- pipeline writes gitignored data-build/panos/; pano barrel removed
- tests run against PGlite mocked in at the @neondatabase/serverless
  boundary, mirroring the fake-upstash pattern; fixtures replace real data
- infrastructure errors rethrow instead of reading as missing coverage;
  session ids via crypto.randomUUID (uuid package dropped)
2026-08-31 09:44:03 +07:00
tiennm99 c9a35dca9a docs: describe the region tree, and finish the coverage page migration
Reconcile every doc with the shipped code. All six described a flat
five-city model, and features.md, tech-stack.md, game-flow.md and
project-structure.md still documented the dart-throw over /images?bbox=
that the prebuilt panorama indexes replaced.

project-overview.md gains a Coverage note that classifies absent coverage
into its three causes -- not yet added, no street imagery, missing from
the boundary -- because a note that only says "partial" teaches
maintainers to ignore real gaps. Cu Chi is named as the one instance of
the third, which is the only one that is a defect.

A context hook had been denying access to src/app/debug/coverage/page.js,
so this also lands the two items earlier phases recorded as
undeliverable: the api/debug/city-coverage -> region-coverage rename, and
the page's migration from a flat city list to RegionSelect. With its last
caller gone, game.js drops CITIES, cities, cityNames, cityCenters and
cityBboxes; getCityIndex, indexedCities and fetchCityPanorama take names
that match what they now take.

Selecting a district with no boundary returned a 400 and left the
previous region's panorama count and outline on screen beside the error,
so Ho Chi Minh's 184,938 read as Cu Chi's. The error path now clears
everything derived from the previous region, and a null boundary removes
the outline rather than skipping the redraw.

Two plan-time claims did not survive contact with the code and the docs
follow the code: VN scores like any other node rather than being a
zero-scoring exploration mode, and the fan-out credits two levels when a
panorama falls outside every district outline.
2026-08-30 19:46:13 +07:00
tiennm99 b69aab5205 chore: drop unused assets, primitives and dead functions
The Next.js starter left five SVGs in public/ that nothing references.
Eight shadcn primitives were vendored in but rendered nowhere: avatar,
form, popover, progress, separator, sheet, table and tooltip. Removing
them frees five Radix packages plus react-hook-form, its resolvers and
zod, none of which the app had wired up.

getAccumulatedScoreMessage was imported by GameClient and never called.
getRandomCityLocationFromBbox had no caller at all, so it goes with its
tests rather than leaving coverage pointed at unreachable code.

The docs claimed a form and validation stack the project never used, and
listed components that are no longer vendored.
2026-08-30 09:26:11 +07:00
tiennm99 1c62e3e1dd perf(mapillary): race dart-throw windows concurrently
Each Mapillary window query costs ~1-5s and most windows hold no
panoramas, so throwing them one at a time made /api/new-game wait for
the sum of every miss. Measured in production: median 17.8s, max 90.5s
over 18 samples.

Throw 8 windows per round instead, race them with Promise.any and abort
the losers once one yields panoramas. Wall time becomes one round-trip
in the common case. Same 20-window budget, same per-city delta, same
401 short-circuit.

After the change, over 10 samples: median 5.6s, max 20.3s.
2026-08-29 14:27:42 +07:00
tiennm99 17b1b46e0c docs: reflect Upstash REST migration, Mapillary dart-throw, cron removal 2026-05-10 03:03:15 +07:00
tiennm99 53ee7957e7 refactor(storage): migrate to Upstash REST SDK with KEY_PREFIX
Replace node-redis with @upstash/redis REST client and route all keys through
a centralized adapter that prepends KEY_PREFIX (default 'vngeoguessr:'). The
new Upstash DB can now be safely shared with other Vercel projects without
key collisions.

- src/lib/upstash.js: new adapter with getJson/putJson/del + zAdd/zRange/
  zScore/zRank/zRevRank/zRemRangeByRank helpers; reads UPSTASH_REDIS_REST_URL
  or KV_REST_API_URL aliases
- src/lib/session.js, src/lib/leaderboard.js: use the adapter; public APIs
  unchanged so api routes stay untouched
- src/lib/redis.js: removed
- scripts/migrate-upstash.js: one-shot copy from old TCP DB to new REST DB,
  prepending the prefix; idempotent, with --dry-run flag
- docs/tech-stack.md: reflect the new SDK and prefix model
2026-05-10 00:00:47 +07:00
tiennm99 2b5359e3cb feat: update docs 2025-08-16 16:47:46 +07:00
tiennm99 1a27e2d596 feat: stash 2025-08-16 08:30:26 +07:00