From 2ab6575e4babd7c1a8725aa0a7c9337f7ff164a8 Mon Sep 17 00:00:00 2001 From: Mischa Date: Thu, 23 Jul 2026 20:39:30 +0200 Subject: [PATCH] =?UTF-8?q?docs(working):=20spec=20=E2=80=94=20post=20form?= =?UTF-8?q?=20location=20override=20(search=20+=20map=20+=20drag=20pin)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the actual root cause behind the Denmark 2026 corrupted-coordinate bug: there was no supported way to set an entry's location to somewhere other than the current GPS position, forcing hand-typed/pasted raw coordinates through Admin2's fragile text field. Backend sanitization (cache-on-save) already guards against silent corruption; this spec adds a frontend way to avoid needing that path at all. --- ...7-23-post-form-location-override-design.md | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/working/specs/2026-07-23-post-form-location-override-design.md diff --git a/docs/working/specs/2026-07-23-post-form-location-override-design.md b/docs/working/specs/2026-07-23-post-form-location-override-design.md new file mode 100644 index 0000000..7953e06 --- /dev/null +++ b/docs/working/specs/2026-07-23-post-form-location-override-design.md @@ -0,0 +1,75 @@ +# Post form: location override (search + map + drag) + +**Status:** πŸ“‹ Not started + +## Problem + +The post form's `lat`/`lng` fields exist in the blueprint (`user/pages/02.post/post-form.md`) as plain `type: text` fields, but a theme CSS rule (`user/themes/intotheeast/css/style.css:893-895`) hides them, and the only way to populate them is the `πŸ“ Get Location` button, which reads the browser's live GPS position via `navigator.geolocation`. + +This breaks down whenever an entry describes a place the traveller isn't physically standing in when they write it up β€” the common case for journal entries written at the end of a day, from a shelter/hostel/train, about somewhere visited earlier. There is currently no supported way to set a coordinate for anywhere other than "here, right now." + +The only workaround has been logging into Admin2 and hand-typing/pasting raw decimal coordinates directly into the page's frontmatter field. This is what produced the Denmark 2026 bug: a coordinate pasted from an external source carried an invisible Unicode bidi mark (U+200E), which PHP's `(float)` cast silently coerced to `0.0`, placing the entry's map marker at `(0, 0)` with no error or warning anywhere in the pipeline. + +Backend sanitization has already been added (`user/plugins/cache-on-save/cache-on-save.php`: `cleanCoordinate()`, wired into both `onFormValidationProcessed` for the public form and `onAdminSave` for Admin2/API saves) to strip invisible characters and range-validate lat/lng before they ever reach a page's frontmatter. That fix is necessary but not sufficient: it prevents *silent corruption of whatever gets typed*, but does nothing to prevent the underlying problem β€” a fragile, invisible-to-the-eye, paste-prone raw text field is still the only way to set an arbitrary location, and there's no way to visually confirm the result before submitting. This spec addresses that gap directly, on the frontend post form, so the Admin2 round-trip is no longer needed for this at all. + +## Goals + +- Give the traveller a reliable, visual way to set an entry's coordinates for a location other than their current GPS position, without touching Admin2. +- Let any coordinate-setting mistake be caught *before* submit, via a live map preview, rather than relying solely on backend validation to catch it after the fact. +- Keep the common case (GPS, writing about where you currently are) exactly as fast and simple as it is today β€” no added friction for the πŸ“ Get Location button. + +## Non-goals + +- No changes to Admin2 or the `api` plugin. The backend sanitization already shipped there stays as-is, as defense-in-depth for the Admin2 edit path (which this spec doesn't touch). +- No change to how coordinates are stored (still plain `lat`/`lng` floats in frontmatter). +- No offline/self-hosted geocoding β€” this reuses free, no-key, CORS-enabled public APIs, consistent with the form's existing BigDataCloud (reverse geocode) and Open-Meteo (weather) integrations. + +## Design + +### Placement + +- **πŸ“ Get Location** (GPS): unchanged. Stays in its current top-level `.form-action-row`, primary/always-visible action for "I'm posting from where I am right now." +- **City / Country**: unchanged position and behavior in the main field flow (still plain, always-visible text fields, still auto-filled by GPS reverse-geocode only when blank). +- **New "More location details" disclosure**, placed directly below the City/Country fields (a separate `
` block from the existing "More options" advanced-fields disclosure, which stays scoped to the unrelated `published`/`force_connect`/`featured` toggles). Closed by default. Contains: + - A **"πŸ” Look up coordinates"** button. + - A small MapLibre preview map with a single, draggable marker. + - The raw `lat`/`lng` text fields, relocated here from their current CSS-hidden position in the main flow (the `display: none !important` rule in `style.css` is removed; the fields simply live inside this disclosure instead). + +### Search mechanics + +- The lookup button geocodes the **current values of the City and Country fields** (e.g. `"Jerup, Denmark"`) via Open-Meteo's free geocoding endpoint (`https://geocoding-api.open-meteo.com/v1/search?name=...&count=5&language=en&format=json`) β€” same provider the form already trusts for weather (`api.open-meteo.com`), no API key required. +- Explicit click, not live-as-you-type β€” matches the deliberate, single-action feel of the existing GPS button. +- **The lookup only reads City/Country β€” it never writes back to them.** A geocode result sets `lat`/`lng` and moves the pin only. This avoids the earlier concern of an ambiguous or slightly-off match silently overwriting a name the traveller deliberately typed. +- Multiple matches β†’ rendered as a small clickable list (place name, admin region, country) so the traveller can disambiguate (e.g. "Paris, Île-de-France, France" vs "Paris, Texas, United States"). Clicking an entry sets `lat`/`lng` and moves the pin; the list is not shown again until the next lookup. +- No matches β†’ inline hint: try adding a country, or drag the pin manually. +- Network failure β†’ degrades the same way the existing reverse-geocode/weather calls do: silent-ish failure, fields untouched, traveller can still fall back to manual entry or the pin. + +### Map preview + sync + +- Single MapLibre GL map instance, reusing the site's existing style (`https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json` β€” same as `maplibre-utils.js`, no new API key), with one draggable marker. +- `maplibre-gl` is dynamically imported (`import('maplibre-gl')`) only when the "More location details" `
` is opened for the first time — mirrors the existing HEIC-conversion lazy-chunk pattern in `post-form.js`, so the ~200KB library is never fetched for ordinary GPS-only submissions. +- Four ways to set a coordinate, all kept in sync with each other: + 1. **GPS button** (main flow) — if "More location details" is later opened, the map/pin reflect whatever `lat`/`lng` currently hold. + 2. **Search-result click** — sets fields, moves/creates pin. + 3. **Dragging the pin** — on `dragend`, reads the marker's `lngLat`, writes back into the `lat`/`lng` text fields (rounded to 6 decimal places, matching the GPS button's existing precision). + 4. **Typing directly into lat/lng** — on blur/debounced input, if both values parse as valid finite numbers within range, move (or create) the pin. Invalid/unparseable input just leaves the pin where it was — this is a visual aid, not a blocking validator; final enforcement stays server-side in `cleanCoordinate()`. +- If the map is opened with no `lat`/`lng` set yet, no pin is shown until one of the four paths above sets a value. + +### Error handling + +- No search results: inline message under the search box, map/pin untouched. +- Search network failure: silent-ish degrade (consistent with existing weather/reverse-geocode error handling in `post-form.js`), fields untouched. +- Invalid manual `lat`/`lng` text: no client-side hard block (the map preview and eventual server-side `cleanCoordinate()` are the safety nets); this UI's whole point is to make that failure mode rare in practice, not to duplicate the backend validator client-side. +- Geolocation permission denied: unchanged existing behavior (`#location-status` error message). + +## Out of scope / explicitly deferred + +- No changes to `user/plugins/admin2/` or `user/plugins/api/` — confirmed and intentional. +- No removal of the existing backend `cleanCoordinate()` sanitization (`onFormValidationProcessed` + `onAdminSave` in `cache-on-save.php`) — it remains as defense-in-depth, especially for the still-possible Admin2 edit path. +- Automated Playwright coverage for the new search→pin→submit flow is desirable but currently blocked by a pre-existing, unrelated `make test-account` Makefile quoting bug — flagged as a follow-up, not a blocker for shipping this feature. Manual in-browser QA (per CLAUDE.md's UI-change testing guidance) is required before considering this done. + +## Testing plan + +- Manual QA in the dev browser: open `/post`, expand "More location details," exercise all four coordinate-setting paths (GPS, search + pick a result, drag the pin, type raw numbers) and confirm the pin and fields stay in sync in both directions. Submit and confirm the saved entry's frontmatter has the expected `lat`/`lng`. +- Exercise the ambiguous-search case (a place name with multiple matches) and the no-match case. +- Exercise the "type garbage into lat/lng" case and confirm the map simply doesn't move the pin (no crash), while a submit still round-trips through the existing backend `cleanCoordinate()` validation.