Every Upstash command and Neon query now has a deadline (5 s and 8 s): the
Redis client retried a hung connection past the browser's fifteen-second
wait, and the Neon client had no limit. Usernames are NFC-normalised so a
tone mark typed as a combining character is accepted and does not make two
board members of one name. The debug key is compared in constant time. A
panorama whose district the tree does not know fails the draw before a
session is written, not /api/guess after it is consumed. The country branch
of countPanos and the never-triggered per-city cap are gone.
PGlite start-up alone takes about seven seconds on an ARM host, so the
10s beforeAll default failed whole files at random. The weekly backup
fails instead of uploading an empty export, and both workflows run with
read-only permissions; CI cancels superseded runs.
The daily challenge credited the main boards while its panorama is the
same all day and the answer is in the first guess response, which made
it a five-points-per-two-requests loop; it is now scored and counted but
credits nothing. The score fan-out settles per level and reports what
landed rather than calling a half-credited round unsaved. The daily
caches only its pick and resolves the image URL per request, so a dead
URL never breaks the day. The players HyperLogLog gets its TTL on its
own first write. DailyCard no longer computes the day number during
static render. Board rows use the Vietnamese name. Share buttons
announce their outcome and treat a cancelled sheet as nothing. The
backup artifact is encrypted before upload.
The debug routes returned panorama coordinates by id and by region to
anyone, which is the answer to a live round; in production they now
answer only a request carrying DEBUG_ACCESS_KEY, and the bbox tester
that proxied Mapillary's failing search is removed.
/api/guess checks the username against the one rule the name prompt
uses, rejects non-finite coordinates before consuming the session, and
names why a submit failed. Session ids that are not UUIDs are replaced
on new-game and rejected on skip.
Score boards are no longer trimmed to 200: trimming deleted the running
total of anyone below the cut, so once 200th place held more than one
round's points no new player could ever get on. Scores are added with
ZINCRBY so concurrent rounds under one name both count. The distance
record is best-effort after scoring, so a distance failure no longer
reports a scored round as unsaved.
Two Redis keys per day count rounds by picked level and score and
distinct players; npm run stats prints them.