Commit Graph
79 Commits
Author SHA1 Message Date
tiennm99 c2c1ced387 test: cover the lib layer against a fake and a real Redis
Scoring, the Upstash key adapter, sessions and the leaderboards had no
coverage, and they carry the anti-cheat model: server-held coordinates,
session TTLs, rank arithmetic.

One suite runs against two backing stores. By default @upstash/redis is
mocked with an in-memory fake, so `npm test` needs no service and
finishes well under a second. With TEST_REDIS=real the mock steps aside
and the same files run against a real Redis behind SRH, the proxy
Upstash recommends for local work, since the SDK speaks HTTP REST rather
than RESP and a plain Redis container cannot serve it alone.

Running both is what keeps the fake honest: a semantic it gets wrong
shows up as a green unit run and a red integration run. Two tests skip
against real Redis, one asserting a response shape only an older SDK
produces and one fast-forwarding half an hour to watch a session expire.

The suite was checked by mutation rather than by passing alone. Breaking
a score band, the key prefix and the session TTL each failed a test;
reversing the global trim window did not, because only the city
leaderboard was asserted, so that test now covers both scopes.

The compose stack doubles as a local Redis for `npm run dev`, which had
no credentials to reach one before.

package.json also loses the eight unused runtime dependencies removed in
the cleanup that follows; they cannot be split from the same file.
2026-08-30 09:26:02 +07:00
tiennm99 f214f4bd49 docs(plans): record the tech stack and mobile playability research
Reviews the stack against 2026 alternatives and finds no swap worth
making: Street View is priced out, MapLibre would fight the panorama
viewer for the GPU, Pannellum trades away the gestures the game runs on.

The findings that matter are mobile playability, panorama weight, and
the Turf monolith reaching the browser through a client-imported module.
Also records an unverified Mapillary bbox constraint that four of five
cities would sit exactly on.
2026-08-30 08:37:19 +07:00
tiennm99 0c977f7017 feat(game): rebuild the mobile game screen around a full-bleed panorama
The phone layout stacked a 45dvh panorama above a 400px map and the
action row, so the page ran well past one viewport. Players scrolled
between looking and guessing, page scroll fought every drag, and Submit
often sat below the fold.

The screen is now a fixed h-dvh surface. The panorama fills it, and the
guess map floats over it as a corner minimap that expands to near
fullscreen on tap. A cover intercepts touches while collapsed so a stray
finger cannot drop a pin too small to see, and Leaflet is told to
remeasure after each transition because it caches container size.

touchmoveTwoFingers is off: one finger has to rotate the panorama, since
looking around is the core verb and the screen no longer scrolls. The
viewer navbar hides below lg, where the action bar would bury it and
gestures already cover what it offered.

viewport-fit=cover lets the layout own the display, so the header pads
past the notch and the action bar past the home indicator. The result
dialog caps to the viewport, scrolls its body and pins its actions.

Desktop keeps the two-column split unchanged.

The header donate button also moves to the beer emoji here, because it
shares an import line with the icons this layout adds.
2026-08-30 08:37:11 +07:00
tiennm99 ac765857bc feat(donate): swap in the new QR image and rebrand the call to action
The donation QR is now zlp.jpg, a 768x1024 portrait rather than the old
square, so the intrinsic dimensions and the container width move with it
to keep the aspect ratio right and the dialog inside the viewport.

Coffee becomes beer across the donate surfaces. The emoji replaces the
lucide icon and is hidden from assistive tech, which reads the button
text instead.
2026-08-30 08:37:00 +07:00
tiennm99 6ab546af89 perf(mapillary): serve the 2048px panorama derivative
thumb_original_url returns the full-resolution equirectangular, commonly
4-8 MP and several megabytes. That download dominates round latency on
mobile data and strains WebGL texture memory on phones.

Request thumb_2048_url alongside it and prefer it, falling back when
Mapillary has no 2048 derivative for an image.
2026-08-30 08:36:52 +07:00
tiennm99 577cb8f599 feat(ui): semantic colour tokens, OS dark mode, and accessibility pass
Every surface hardcoded its own grey and red, so the dark palette already
defined in globals.css could never take effect and muted text failed
contrast. Route colour through tokens instead: --brand and --surface join
the shadcn set, the Vietnamese flag red stays the brand, and a
prefers-color-scheme block supplies the dark values.

The dark: variant now matches both the media query and a .dark ancestor.
It previously matched only the class, so dark: utilities compiled to rules
that could never apply under OS-driven theming.

Contrast, measured against WCAG AA rather than eyeballed:
text-gray-400 on the page background was 2.41:1, the blue and purple rank
labels 2.34:1 and 2.46:1, white on amber-500 2.15:1. All twelve token
pairs now sit between 4.7:1 and 19.8:1 in both themes.

Also: emoji and text glyphs used as icons give way to lucide SVGs with
accessible names, score and rank stop relying on colour alone, loading and
result regions announce via role=status, interactive targets reach 44px,
100vh becomes dvh so mobile browser chrome stops clipping the game, the
leaderboard tab count follows the city list, and numeric columns use
tabular figures.
2026-08-29 15:16:19 +07:00
tiennm99 9645a355c6 fix(new-game): repair session lookup and log Mapillary failures
The POST handler read from gameSessions, an in-memory Map removed in
the Redis migration, so every call threw a ReferenceError and returned
an opaque 500. Read through getGameSession instead, and answer 400 on a
missing sessionId, 404 when the session is gone, 500 on a store error.
Parsing the body no longer throws out of the handler on malformed JSON.

Log the underlying Mapillary error before returning the generic
coverage message, so an API outage is not silently reported as a city
without street view.
2026-08-29 14:44:02 +07:00
tiennm99 31dd477080 fix(mapillary): budget API errors separately from empty windows
Racing 8 windows per round made the retry budget correlated: a single
transient 5xx burst failed all 8 at once and burned 40% of it, so a few
seconds of Mapillary trouble exhausted the loop and surfaced to players
as "this city may not have sufficient coverage". Sequential attempts
had spread the same budget over ~20s and rode such blips out.

Count only genuine misses against the 20-window budget. Rounds that
lose windows to 5xx or network errors get their own smaller allowance
with a widening backoff, so a burst is not answered with another burst.

Restore the per-attempt error logging dropped in the previous commit:
a round losing windows to errors now logs the status and count instead
of the same benign "no panos yet" line an empty round produces.
2026-08-29 14:44:02 +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 493f29621e build: move from pnpm to npm
Replace pnpm-lock.yaml with package-lock.json. The security overrides move to
package.json#overrides with their ranges intact, including the bounded
'>=x <y' pins whose upper bounds keep the next major out. allowBuilds becomes
package.json#allowScripts for the packages actually present in the tree.
2026-08-17 13:28:24 +07:00
tiennm99 e3fb09c4d9 chore: remove dependabot version-update config 2026-07-25 14:15:31 +07:00
tiennm99 f614ce9057 chore(deps): bump dependencies and remediate js-yaml/postcss advisories
Bump the minor-and-patch dependency group (react 19.2.7, radix-ui,
react-hook-form 7.81.0, tailwindcss 4.3.3, and others), pin eslint to
the Next-compatible 9.x range since eslint 10 breaks next lint, and add
pnpm overrides forcing js-yaml >=4.2.0 and postcss >=8.5.10 to clear
open Dependabot alerts.
2026-07-17 18:05:42 +07:00
dependabot[bot] 836c89ee7f chore(deps-dev): bump eslint from 9.39.4 to 10.4.0 (#23)
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.4 to 10.4.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.4.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:02:35 +07:00
dependabot[bot] 5b5e8be188 chore(deps): bump next from 15.5.15 to 15.5.18 (#18)
Bumps [next](https://github.com/vercel/next.js) from 15.5.15 to 15.5.18.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.15...v15.5.18)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.18
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 18:01:04 +07:00
tiennm99 45b2f5a0a6 chore: drop inapplicable dependabot ecosystems (#27) 2026-05-23 11:09:04 +07:00
tiennm99 44253a6f4f chore: add dependabot config (#19) 2026-05-23 10:49:02 +07:00
tiennm99 1613a44b8e chore(node): set Node 24 as default via engines.node 2026-05-13 17:12:35 +07:00
tiennm99 130d13e0cf fix(pnpm): allow native build scripts for pnpm 11 strictDepBuilds
Set allowBuilds.sharp and allowBuilds.unrs-resolver to true in
pnpm-workspace.yaml so pnpm 11 frozen-lockfile installs succeed on Vercel.
2026-05-13 16:44:16 +07:00
tiennm99 dfb74f1ad1 chore: migrate from npm to pnpm 2026-05-13 10:16:36 +07:00
tiennm99 17b1b46e0c docs: reflect Upstash REST migration, Mapillary dart-throw, cron removal 2026-05-10 03:03:15 +07:00
tiennm99 b54e13ea5f chore: remove decorative cron job and its vercel.json config 2026-05-10 02:43:45 +07:00
tiennm99 10cb776ecc chore(plans): remove completed migration & mapillary plans 2026-05-10 02:33:01 +07:00
tiennm99 1e16bd8d42 chore(storage): drop redis dep and migration script post-Upstash cutover
The Upstash REST migration is verified in production; node-redis is no longer
needed and the one-shot migration script has done its job.

- Remove `redis` from package.json
- Delete scripts/migrate-upstash.js (recoverable from commit 53ee795)
- Add cleanup plan; close out migration plan Phase 3
2026-05-10 02:13:12 +07:00
tiennm99 c435a618ac fix(mapillary): bump retries 10 -> 20 to better cover HN pano-sparse blocks 2026-05-10 01:46:19 +07:00
tiennm99 f12fef855d fix(mapillary): port viguessr random-point + per-city delta strategy
Replaces adaptive shrink/grow bbox loop with viguessr's proven approach:
random point inside city bbox → fixed-size sub-bbox of side 2*delta → re-roll
position on empty/error. Small fixed bbox keeps Mapillary query cost under
their 'reduce data' cap; dart-throw eventually lands on a pano-rich window.

- src/lib/game.js: add per-city mapillaryDelta (HN=0.003, others=0.005)
  and export cityDeltas alongside existing cityBboxes
- src/lib/mapillary.js: rewrite fetchMapillaryImages(bbox, delta) with
  10-attempt position-only retry; revert to thumb_original_url; limit=3
- src/app/api/new-game/route.js: pass cityDeltas[cityCode] to the fetcher

Source: https://github.com/luuvanduc1999/viguessr (assets/js/game.js)
2026-05-10 01:38:48 +07:00
tiennm99 a3a0667774 fix(mapillary): adaptive bbox - shrink on 500, grow on empty across 15 retries 2026-05-10 01:12:55 +07:00
tiennm99 3d954cbbdc fix(mapillary): bump bbox area to 0.002 sq deg (~5km) keeping limit=1 2026-05-10 01:04:04 +07:00
tiennm99 9e91be0572 fix(mapillary): limit=1 + smaller bbox; trim is_pano field (filter handles it) 2026-05-10 00:52:53 +07:00
tiennm99 5c4bf4d609 fix(mapillary): bump sub-bbox area to 0.001 sq deg (~3km) for better pano coverage 2026-05-10 00:46:24 +07:00
tiennm99 e47b7fe28b fix(mapillary): always sub-sample small bbox + retry on transient errors 2026-05-10 00:40:47 +07:00
tiennm99 963175886f fix(mapillary): keep is_pano=true filter (panoramic only required) 2026-05-10 00:33:13 +07:00
tiennm99 9618e3c260 fix(mapillary): drop is_pano filter, lower bbox area + limit to dodge Mapillary 500 2026-05-10 00:29:50 +07:00
tiennm99 34959449c7 fix(mapillary): use thumb_2048_url to avoid Mapillary 500 'reduce data' error 2026-05-10 00:25:48 +07:00
tiennm99 5a416a2962 debug: log Mapillary error body and redacted URL 2026-05-10 00:19:48 +07:00
tiennm99 5fe770121e docs(plans): add Upstash migration plan with KEY_PREFIX
Three-phase plan: adapter refactor, data migration, cutover/cleanup.
2026-05-10 00:00:57 +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
dependabot[bot] f0f19508c4 chore(deps): bump next from 15.5.14 to 15.5.15 (#16)
Bumps [next](https://github.com/vercel/next.js) from 15.5.14 to 15.5.15.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.14...v15.5.15)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.15
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 13:33:17 +07:00
dependabot[bot] b462c7b2db chore(deps): bump uuid from 11.1.0 to 14.0.0 (#17)
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.1.0 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.1.0...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 13:33:14 +07:00
dependabot[bot] 70e96ddd66 chore(deps): bump minimatch (#10)
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:13:27 +07:00
dependabot[bot] 9a2432dcb8 chore(deps-dev): bump tar from 7.4.3 to 7.5.11 (#12)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.4.3 to 7.5.11.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.4.3...v7.5.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:13:23 +07:00
dependabot[bot] 0d32ef520f chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 (#13)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:13:20 +07:00
dependabot[bot] 99e2ec9c6a chore(deps): bump picomatch (#15)
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:13:17 +07:00
dependabot[bot] af2455fc1c chore(deps): bump next from 15.5.9 to 15.5.14 (#14)
Bumps [next](https://github.com/vercel/next.js) from 15.5.9 to 15.5.14.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.9...v15.5.14)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.14
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 17:13:09 +07:00
tiennm99 99d0ab6d56 feat(ui): replace gradient glassmorphism with soft clean design
Swap dark animated gradient background for light solid bg, glassmorphism
cards for clean white cards, and remove pulse/glow animations. Keep
Vietnamese red as accent color.
2026-04-04 10:12:14 +07:00
tiennm99 9279439f8c add claude settings with Vercel plugin enabled 2026-04-04 09:20:59 +07:00
tiennm99 f58832796a remove settings.local.json from git tracking 2026-04-04 09:18:04 +07:00
tiennm99 5c2064dfca fix(mapillary): subdivide large bboxes to fit API area limit
Mapillary API rejects bboxes exceeding 0.01 sq degrees with HTTP 500.
City bboxes (e.g. TPHCM=0.016) exceeded this limit. Now picks random
sub-bboxes within the city area and retries up to 5 times to find coverage.
2026-04-04 09:10:20 +07:00
tiennm99 9bee409f52 feat(ui): Vietnamese-themed redesign with glassmorphism and improved components
- Add Vietnamese-themed CSS with gradient backgrounds, animations, and brand color variables
- Redesign homepage with gradient hero, glassmorphism cards, and leaderboard component
- Improve game page loading fallback with spinner
- Enhance GameClient with compact header, polished result modal, and retry logic
- Extract LeaderboardList as reusable component
- Make DonateQRModal responsive instead of hardcoded dimensions
- Polish UsernameModal visuals
2026-04-04 00:23:41 +07:00
tiennm99 95dfe57913 Update package-lock.json 2026-01-22 15:47:28 +07:00
Tien Nguyen Minh ecadfceffd Merge pull request #4 from tiennm99/dependabot/npm_and_yarn/next-15.5.9
chore(deps): bump next from 15.5.2 to 15.5.9
2026-01-09 10:20:17 +07:00