fix(review): land cleanCoordinate server-side guard; surface geocode failures

Two code-review follow-ups.

cleanCoordinate() — the server-side coordinate sanitizer the design doc and
plan both describe as already shipped — had never actually been committed; it
existed only as uncommitted work in another checkout, so this branch had no
server-side validation of lat/lng at all (the blueprint fields are plain
`type: text` with no `validate:` key). Landing it here makes the spec's stated
safety net real. Also corrected its onAdminSave comment, which justified that
hook by saying the public form's lat/lng inputs are CSS-hidden and GPS-filled
— true before this feature, inverted by it. Both hooks are needed: this branch
makes /post the primary hand-entry path, not Admin2.

The geocode lookup swallowed every failure and reset the button, leaving the
DOM byte-identical to the pre-click state — a traveller on flaky mobile data
could not distinguish a failed lookup from a broken button. It now shows a
distinct hint, and checks r.ok first so a 4xx/5xx body no longer parses as
"no results" and tells the traveller their city does not exist. R8's actual
guarantee (fields untouched on failure) is preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 21:48:20 +02:00
co-authored by Claude Opus 5
parent a5993b2091
commit e873a9cb23
3 changed files with 127 additions and 30 deletions
File diff suppressed because one or more lines are too long
+15 -3
View File
@@ -719,7 +719,14 @@ function initLocationDetails() {
// button stuck disabled on "Searching…" forever.
var controller = new AbortController();
var timeoutId = setTimeout(function () { controller.abort(); }, 10000);
fetch(url, { signal: controller.signal }).then(function (r) { return r.json(); }).then(function (data) {
fetch(url, { signal: controller.signal }).then(function (r) {
// Without this a 4xx/5xx body (rate limit, upstream error) parses as
// JSON with no `results` key and the traveller is told their city
// does not exist — sending them off to hunt a spelling mistake that
// isn't there. Route real failures to the catch instead.
if (!r.ok) throw new Error('geocode http ' + r.status);
return r.json();
}).then(function (data) {
var list = (data && data.results) || [];
if (!list.length) {
setHint('No matches — try adding a country, or drag the pin on the map.');
@@ -746,8 +753,13 @@ function initLocationDetails() {
}
showResults(list);
}).catch(function () {
// R8: network failure (including our own timeout abort) degrades
// silently — fields untouched.
// R8 revised: the fields stay untouched on failure (that part of R8
// is the actual guarantee), but the failure is no longer invisible.
// Silence left the DOM byte-identical to the pre-click state — empty
// hint, enabled button — so a traveller on flaky mobile data could
// not tell "lookup failed" from "the button is broken". Distinct
// from the no-match message above, which means the service answered.
setHint('Couldnt reach the lookup service — check your connection and try again, or drag the pin on the map.');
}).then(function () {
clearTimeout(timeoutId);
lookupBtn.disabled = false;