Files
vngeoguessr/docs/development.md
T
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

7.6 KiB

Development Guidelines

Development Commands

  • npm run dev - Start development server with Turbopack
  • npm run dev:clean - Same, after clearing the .next build cache
  • npm run build - Build the application for production (writes .next)
  • npm run build:check - Same build for local verification, into .next-check
  • npm start - Start production server
  • npm run lint - Run ESLint
  • npm test - Run the test suite against the in-memory Redis fake
  • npm run test:watch - Re-run tests on change
  • npm run test:integration - Run the same suite against a local Redis
  • npm run redis:up / npm run redis:down - Start/stop that local Redis

Important Development Guidelines

JavaScript Only

  • This project uses JavaScript exclusively
  • Never create or suggest TypeScript files (.ts, .tsx)
  • All components and utilities should be .js or .jsx files

Function Parameters

  • All functions should use individual parameters instead of object destructuring
  • Use function(param1, param2) instead of function({param1, param2})
  • This applies to React components, utility functions, and API handlers

File Modification Policy

  • Only modify source code files, documentation (/docs), and plans (/plans)
  • Configuration changes (package.json, next.config.mjs, eslint.config.mjs, components.json, etc.) should be highlighted for manual processing
  • Environment files and build settings require manual review

Security Best Practices

  • Never expose or commit secret keys and sensitive information
  • Server-side session management prevents client-side coordinate access (Redis TTL 30 min)
  • All geographic calculations must be performed server-side
  • Session cleanup after guess submission for security

Environment Variables

Redis (required):

  • Either UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN (vanilla Upstash)
  • Or KV_REST_API_URL + KV_REST_API_TOKEN (Vercel Marketplace)
  • Optional: KEY_PREFIX (default: vngeoguessr:) for multi-tenant DB sharing

Mapillary (required):

  • MAPILLARY_ACCESS_TOKEN - Mapillary API token for image fetching

For local Redis without an Upstash account, see Running Upstash locally below.

Testing & Completion

Tests live in tests/ and cover the logic in src/lib/ (scoring and distance, the Upstash key adapter, game sessions, the region tree, the panorama indexes and the leaderboards), the API routes, and the leaderboard migration. Run npm test after changing anything under src/lib/ or src/data/.

The suite runs against two backing stores, from one set of test files:

  • npm test uses tests/fake-upstash-redis.js, an in-memory stand-in mocked in at the @upstash/redis boundary. No service, no Docker, well under a second. This is the default.
  • npm run test:integration runs the same files against a real Redis. Two of them skip: one asserts a response shape only an older SDK produces, and one fast-forwards half an hour to watch a session expire.

Running both is what keeps the fake honest. A behaviour the fake gets wrong shows up as a green unit run and a red integration run.

Everything above src/lib/ (UI, panorama viewer, map interaction, Mapillary calls) is still manual: inform the user when work is complete. Do NOT start development servers - user handles testing manually.

When the dev server needs restarting

Almost never. Measured against this project:

Change Behaviour
Components, pages Fast Refresh, ~200ms
API routes Hot reloaded, ~1.6s
Server libs under src/lib/ Hot reloaded, ~1.8s
Index data under src/data/ Hot reloaded, ~1.1s
next.config.mjs Next restarts itself
.env Reloaded in place

Do not run npm run build while a dev server is running. Both own the same output directory, so the build replaces manifests the dev server is still reading, and it logs a burst of ENOENT: no such file or directory errors for manifest files under that directory. Measured: 110 such errors from one overlapping build, and none when the two do not overlap. They are noise from the collision, not a fault in the code being edited.

Use npm run build:check instead. It builds into .next-check and leaves the dev server alone. The plain build script is unchanged, because that is what the deployment platform runs and it must keep writing the default directory.

If a change genuinely will not take effect, the build cache is usually inconsistent rather than the code being wrong. That happens after a build is killed part-way or a different Next version writes into .next, and it shows up as the dev server serving 500s for everything. npm run dev:clean clears the cache and starts fresh; a plain restart will not fix it.

Running Upstash locally

Upstash itself is cloud-only, but @upstash/redis speaks HTTP REST, so any server exposing that REST surface is indistinguishable to the app. docker-compose.yml runs the proxy Upstash's own docs recommend, SRH (hiett/serverless-redis-http), in front of a real Redis:

npm run redis:up     # redis + SRH on http://localhost:8079
npm run redis:down   # stop and discard the data

npm run test:integration points at it automatically. To use it for npm run dev as well, set in .env:

UPSTASH_REDIS_REST_URL=http://localhost:8079
UPSTASH_REDIS_REST_TOKEN=vngeoguessr-local-token

SRH accepts commands only as a JSON array POSTed to /, which is what the SDK sends; the path form Upstash also supports (GET /set/key/value) returns 404.

Rebuilding the generated region data

src/data/regions/, src/data/boundaries/ and src/data/panos/ are generated. Run the scripts in this order -- each reads what the previous one wrote:

node scripts/build-region-boundaries.mjs   # OSM/Nominatim -> boundaries + region tree
node scripts/build-pano-index.mjs          # Mapillary z14 tiles -> per-province pano index
node scripts/assign-pano-districts.mjs     # clip + partition panos by district; writes counts.js

Each script's header comment carries its flags and its cost; read it before running. Two things worth knowing up front:

  • build-region-boundaries.mjs --regenerate rebuilds the provinces, the barrel and the tree from what is already on disk, with no network calls. Use it after hand-editing a boundary.
  • build-pano-index.mjs spends Mapillary tile requests against a 50,000/day cap. assign-pano-districts.mjs spends none -- it only re-partitions indexes that already exist, and refuses to rewrite counts.js on a partial run.

Migrating leaderboards

scripts/migrate-leaderboards.mjs backfills the two boards whose region code changed (Da Lat into Lam Dong, Duc Hoa into Long An). It is dry-run by default and copies rather than moves, so the source boards survive.

Deploy first, then migrate. The new code writes to the new keys immediately; a migration run before the deploy would copy a board that is still being written to under its old name. Run the script's --help for the apply, verify and restore flags.

shadcn/ui Configuration

  • Style: "new-york"
  • Path aliases: Configured for @/components, @/lib, etc.
  • Components: Use JavaScript (.js) not TypeScript
  • CSS variables: Enabled for theming
  • Components location: src/components/ui/

Code Style Standards

  • Follow existing code patterns in the codebase
  • Use established libraries and utilities already present
  • Maintain consistent naming conventions
  • Write descriptive comments where the reason for the code is not obvious from the code -- this codebase leans on them heavily to record why a measured approach was chosen over the obvious one
  • Prefer editing existing files over creating new ones