Skip to content

AI puts named places on the map; quieter chrome, audit fixes and a green e2e suite - #2

Merged
coinmandeer merged 19 commits into
mainfrom
claude/stoic-hopper-jutqgy
Sep 25, 2026
Merged

coinmandeer merged 19 commits into
mainfrom
claude/stoic-hopper-jutqgy

Conversation

@coinmandeer

@coinmandeer coinmandeer commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Change

Follows the merged UI redesign (coinmandeer/map0S-codex#1): the AI puts every place it names on the map, the chrome is quieter, the renderer and iOS glitches found in the audit are fixed, and the e2e suite is back to describing the product that ships.

AI → map, whichever model answers

  • Places an answer names (places field, a JSON block, GPS pairs, list items) are geocoded on the server and drawn automatically, with one camera fit for route + stops + places. No "Show on map" click (apps/api/src/services/ai/mentionedPlaces.ts, chatService.ts).
  • A plain-text answer is no longer dropped; a tool call written into the text is recognised; submit_plan accepts stops by name/address; two or more places + a "route" question yields a computed route.
  • Rejected mapData goes back to the model once instead of ending the turn; tool arguments wrapped in Markdown or with a trailing comma are repaired syntactically instead of silently discarded.
  • Offline/memory profile gets a gazetteer and a straight-line router, so the assistant works in dev without keys.
  • Answers render paragraphs, lists and bold (AnswerText.tsx); compact header with the last conversation's title and one ⋯ menu.

Search, layers, place detail

  • Suggester: places first with a kind icon, name + hierarchy, at most 3 layers, one source credit in the footer, arrow keys with aria-activedescendant. The "Ask AI: …" row is replaced by a quiet "Enter asks the assistant" hint; anything not suggested goes to the AI. Orphaned GooglePlaceSearch.tsx removed.
  • Layer and basemap panels: group icons, hairline list instead of boxes, no jumping "N on · M selected" summary.
  • Place detail: section icons + chevron instead of the native triangle, bilingual field labels (district, updatedAt, dataScope…), locale-formatted dates, stat grid that no longer breaks words.

Renderer and map

  • Emergency basemap no longer crashes on getStyle().name when the first style fails; one bad tile no longer swaps the whole basemap (8 errors / 15 s budget); glyphs for raster styles; the selected pin survives a basemap switch; the pulse stops after three cycles.
  • Game: entering the game after a load often showed an empty board — a late detach of the replaced handle removed the new scene (shared layer id), and a handle detached inside a style.load dispatch re-attached itself. Fixed in worldLayer.ts; ?mode=game now starts the scene. With the scene running, the game panel opened from the HUD could stay a spinner for tens of seconds on a slow machine (a lazy component's Suspense retry never expires, and the board's redraws kept interrupting it); it now loads through ordinary state, commits as soon as its module is ready and is preloaded when game mode starts.
  • Discover: area outlines waited for an unrelated state change because sync() bailed while any tile was loading — now retried on idle; the boundary manifest is fetched as soon as a local world is shown. The "What is here?" chip (component and styles existed) is mounted; on a phone it floats above the sheet and steps aside while the sheet is half or full height or a place detail covers the panel. The header shows the selected area's name even when the guide is unavailable.
  • Map status: a failed layer's alert stays up while the layer retries instead of blinking out and back.

Plans and presets

  • A shared plan link (/s?mode=planning#plan=…) kept losing mode on the first camera rewrite, so a reload or a copied address opened Discover without the plan.
  • A preset with events (City) fell back to "Custom" immediately because the timeline's from/to window counted as drift; the time window is no longer part of preset identity.
  • The empty planning panel opens half-height on a phone like the other panels.

iOS (PWA now, Capacitor in the roadmap)

  • Inputs ≥ 16 px on touch (no focus zoom), no tap flash or rubber-band overscroll, PNG apple-touch-icon and manifest icons, apple-mobile-web-app-title; GPS is requested only after "My location" or when already granted; map controls no longer collide with the Layers/Settings column.

Data

  • Golemio: v2 address objects, district slugs ("praha-1" → "Praha 1") and opening hours are kept; the memory server serves stateless live sources (was 404). GOLEMIO_API_KEY / EUROPEANA_API_KEY documented — keys only via env, none in the repo.

Audit

  • docs/audits/2026-09-25-mapos-audit.md: findings by severity (renderer/perf, UI/UX, mobile/iOS, AI, tests), answers on GBFS, SpaceAPI, Ookla, Eurostat, BTC Map, Europeana, and a phased roadmap (F1 performance, F2 native-feeling mobile, F3 "wow", F4 Capacitor, F5 data).

E2E suite

  • Specs rewritten for the redesigned chrome through one helper (e2e/fixtures/mapPanel.ts) instead of the removed basemap-btn/overflow ids; URL assertions replaced by application state (the URL deliberately keeps only the camera since v0.1); boundary tiles have a shared fixture (e2e/fixtures/boundaryTiles.ts).
  • Three Discover tests exercised the legacy GeoJSON overlay that the default VITE_DISCOVER_BOUNDARIES flag hides; they now test the same user-visible behaviour on the vector-tile overlay.
  • The area-hover performance check measured how fast the runner's software GL paints (a plain repaint of the unchanged map takes ~80 ms there). It now requires the handler to fit in one frame and the hover frame to cost no more than an ordinary repaint, keeping the absolute 50 ms on machines that paint that fast.
  • The map memory benchmark (performanceMap.spec.ts) runs in its existing production profile (playwright.performance.config.ts, one worker) as a new CI step test:e2e:performance instead of beside the WebGL workers of the default profile, where it timed out on main too; the profile now points its base URL at its own preview port.
  • The weather hover/tap test waits until the grid stops refetching before comparing the two readings (a late camera move replaced the grid between them on the CI runner).
  • Security profile: the production bundle exposes no map handle, so the untrusted name is opened through Discover's accessible place list (the test had read window.__maposMap since it was written).
  • No test was skipped, disabled or quarantined.

Evidence

  • Relevant unit and contract tests pass — npm test (web 583 pass, API 848 pass, packages 111/84/4 pass; skips are the existing environment-gated ones: live model, PostgreSQL, rights inventories), test:contracts, test:privacy (218 pass), test:migrations (37), test:load, test:secrets, test:css-tokens. New unit tests: preset drift vs the event window, shared-plan link keeps its mode, ?mode=game starts the scene.
  • Type, lint and formatting checks pass — npm run typecheck, npm run lint, npm run format:check.
  • Integration/E2E, visual, accessibility or load evidence — local runs with the preinstalled Chromium: default suite 217/220 before the benchmark moved to its own profile (1 skipped MAPOS_LONG_PERF gate, unchanged; the 2 failures were that benchmark timing out beside other WebGL workers, 1.3/1.6 min alone with and without this branch's source changes). Profiles: offline 5/5, security 1/1, accessibility 8/8, visual 18/18, runtime-starter 2/2, performance 2/2 (production build, 1.1/1.3 min). After the first CI run: game panel under 4x CPU throttling opens in ~3–5 s (was never / >30 s), weather + game + Discover specs 23/23 on three loaded workers. Live check at 1440 px and iPhone 14 Pro size (Discover, layers, basemaps, "Brno" suggestions, a question sent with Enter, AI panel, place detail): no page errors.
  • No test was presented as run when it was skipped — known sensitivity: game.spec.ts:68 fails only when several copies of itself run at once (--repeat-each), because they fight the same shared-world enemy.

Layer or provider integration (if applicable)

  • Versioned manifest, query/100-feature budget and cancellation — no new layer; Golemio normalisation stays inside the existing source and its 500-row cap.
  • Loading, empty, partial, error and provider-outage states — Golemio keeps its "not configured" / zoom notices; map status alert now stable during retries.
  • Filters, legend, provider-neutral detail/actions and accessible non-map alternative — detail labels are provider-neutral and bilingual.
  • Permissions, private-data boundary, AI exposure and entitlement policy — geocoding of places an answer names uses the existing resolve_location registry; no new data leaves the server.
  • Fixture/contract tests, health/SLO owner and disable/rollback path — fixture test for Golemio normalisation; no new provider.
  • No provider secret or untrusted extension JavaScript reaches the client — keys only via server env.

Release safety

  • Migration: none.
  • Feature flags: none added. Discover's existing VITE_DISCOVER_BOUNDARIES rollback path is unchanged.
  • Security/privacy/dependency/bundle: no new dependency (the icon render script uses sharp, already installed with the API's glTF tooling). GPS permission is requested later than before.
  • Traceability/docs: audit document, docs/data-sources.md, docs/environment.md, docs/plans/blockers.md updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16

… answered

Models differ in how they answer: some geocode each place and submit ids,
others list places in prose or write the submission as JSON text. The map
depended on the first style, so most answers showed nothing.

- mentionedPlaces: read places from a submitted list, a JSON block, GPS
  pairs or a list of names, and resolve each through resolve_location
- submit_answer takes places; submit_plan takes stops by name/address/GPS;
  two places and a route question become a routed plan card
- a prose answer is kept and its places geocoded instead of discarded
- rejected mapData goes back to the model once; rows without placeId are
  geocoded by placeName
- tool arguments in a Markdown fence or with a trailing comma are repaired
- offline profile gets a gazetteer and straight-line router; /geocode no
  longer answers every query with Plzeň
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
- header is the thread name plus a menu instead of a native details popup
- answers render paragraphs, lists and bold as React elements, never HTML
- composer is one row on phones; follow-ups scroll sideways
- an answer frames its route, stops and places once; the route preview
  no longer refits when a panel rebuilds the same route
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
Places come first as one line of name and one of hierarchy, with the kind
shown by an icon. At most three layers follow. The 'Ask AI' row is gone:
anything that is not a suggestion goes to the assistant on Enter, which a
footer hint says once, next to the geocoder attribution. Arrow keys move
through places.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
Layers and basemaps: every section has an icon and reads as a hairline
list instead of a stack of boxes. The 'N on · M selected' row that
appeared above the groups on the first toggle is gone (the Active tab
already counts them), so the panel no longer shifts under the pointer.
Basemap groups get English names.

Place detail: sections have an icon and chevron instead of the browser's
triangle; field labels are bilingual and cover district/updatedAt/
dataScope/verifiedAt; dates read as dates; statistic tabs no longer break
words and the grid no longer overflows. Pin preview strings use i18n.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
- the fallback style crashed on map.getStyle().name when the first style
  failed, leaving a blank map; it now applies
- one failed tile no longer swaps the whole basemap (8 errors / 15 s)
- route segments use promoteId, so selecting one finally highlights it
- raster and fallback styles carry a glyph endpoint, so cluster counts
  and stop numbers render instead of being rejected
- the selected pin survives a basemap switch; its pulse stops after three
  rings instead of repainting every frame
- the location watch starts only once permission is granted or the user
  asks, not as a prompt on first load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
- fields are at least 16px on touch devices so iOS stops zooming in
- no tap flash, rubber-band scroll or long-press callout on chrome
- PNG apple-touch-icon and manifest icons (iOS ignores SVG), generated
  by scripts/render-app-icons.mjs; web app title and theme colour
- map controls stay below the top rail on phones; +/- hidden on touch
- preset delete is visible on touch instead of hover-only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
Golemio v2 addresses are objects, which the text-only normaliser dropped;
districts are slugs and opening hours arrays. All three now read as text.
The in-memory server answers stateless live layers (Golemio, BTC Map,
Europeana, SpaceAPI, GBFS) outside the offline profile instead of 404.
GOLEMIO_API_KEY and EUROPEANA_API_KEY are documented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
- Game: a style switch replaces the world handle, and the old handle's late detach removed the new scene (shared layer id); a handle detached inside a style.load dispatch also re-attached itself, because MapLibre calls the listeners it copied before off(). Each handle now attaches as soon as a style exists, re-attaches after style loads, removes only its own layer and refuses to attach once detached. Opening ?mode=game starts the scene.
- A shared plan link (/s?mode=planning#plan=…) lost its mode on the first camera rewrite, so a reload or a copied address opened Discover without the plan.
- A preset with events fell back to Custom immediately: the timeline's from/to window counted as drift. The time window is no longer part of preset identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
…hosen area's name

- The boundary overlay bailed whenever isStyleLoaded() was false, which is also true while any tile is loading, and nothing retried: outlines appeared only after an unrelated state change. It now syncs again on idle, and fetches the manifest as soon as a local world is shown (was up to a minute later).
- DiscoverHereButton and its styles existed but the chip was never mounted. It floats above the sheet on a phone, hides while the sheet is half or full height (the panel already answers for the centre there) and while a place detail covers the panel.
- The header names the selected area even when its guide cannot be fetched (was "Neidentifikovaná oblast").
- Search suggestions are left-aligned again (a row inherited the column's centring).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
…hooks

- A failed layer's alert stays up while the layer retries (a pan re-queries it) instead of blinking out and back; it clears once the layer answers, leaves range or is turned off.
- The empty planning panel opens half-height on a phone like the other panels.
- Map panel tabs and layer views carry test ids; the basemaps tab keeps the tiles-sheet hook.
- Route preview keeps its string segment ids: MapLibre 5 keys feature state by them, and the promoteId change broke segment selection.
- Remove the orphaned GooglePlaceSearch component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
- One way into the map panel (e2e/fixtures/mapPanel.ts: layers/basemaps tabs, catalogue search rows, settings, user layers) instead of the removed basemap-btn and overflow ids; shared fixtures for Discover boundary tiles and for creating and sharing a plan.
- The URL keeps only the camera since v0.1: reload tests reopen the mode, and layer/mode/preset checks read application state.
- Search: suggestions show name + hierarchy with one source credit, and Enter asks the assistant (the Ask-AI row is gone).
- Discover: three tests exercised the legacy GeoJSON overlay that the default VITE_DISCOVER_BOUNDARIES flag hides; they now cover the same behaviour on the vector-tile overlay. The app starts without layers, so tests that need POIs switch them on through the URL.
- The area-hover check measured how fast the runner's software GL paints (a plain repaint of the unchanged map takes ~80 ms there). It now requires the handler to fit one frame and the hover frame to cost no more than an ordinary repaint, keeping the absolute 50 ms where a machine paints that fast.
- Security: the production bundle exposes no map handle, so the untrusted name is opened through Discover's accessible place list.
- Map status, AI panel session menu, visual captures and offline smoke follow the new controls. No test was skipped or disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
Adds the game-scene race, the unmounted Discover chip, boundary outlines waiting for an unrelated change, shared-plan links losing their mode, preset drift from the event window and the orphaned component to the fixed list, and replaces the stale CI row with the current e2e state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
The 6- and 12-layer runs take 1.3 and 1.6 min alone, identical before and after this branch's changes, but exceeded the 180 s budget beside two other WebGL workers. The wall-clock budget goes to 360 s; the heap allowance and bounded counts it asserts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
With the scene now started by ?mode=game, opening the game panel from the HUD left a spinner for tens of seconds on a slow machine (CI, 4x-throttled Chromium). The lazy panel resumed through a Suspense retry, which React never expires, and the board's redraws plus the HUD clock kept interrupting it. The panel now loads through ordinary state, commits synchronously once its module is ready, and starts loading when game mode begins; a failed load surfaces through the module boundary's retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
performanceMap.spec.ts already had playwright.performance.config.ts (production build, one worker), but the default profile also ran it beside two other WebGL workers, where it timed out on main and on this branch alike. The default profile now ignores it like the other auxiliary specs, test:e2e:performance runs the profile in CI, and the profile points its base URL at its own preview port (it only worked with MAPOS_E2E_WEB_PORT set). The 360 s budget from the previous commit goes back to 180 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
On the CI runner a late camera move after the phone resize replaced the grid between the hover and the tap, so they showed 20 °C and 16 °C. The test now waits until grid requests stop and the map is idle before choosing the point. The audit records the pre-existing camera shift behind it: opening or closing the right drawer moves the view by a panel width and refetches the layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
A Deploy VPS workflow runs scripts/deploy-vps.sh (staged build, verified backup and restore drill, atomic switch, automatic rollback) and the public smoke check from a GitHub runner. It runs when a same-repository pull request is labelled deploy:vps, deploying its head commit and removing the label afterwards, or manually once on the default branch. SSH access comes only from repository secrets: VPS_SSH_KEY, or VPS_PASSWORD through sshpass wrappers so the password never reaches a command line; VPS_KNOWN_HOSTS optionally pins the host key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
Closing the layers drawer with Escape changes the map padding, so the view settles about half a second later and the grid is fetched once more for the final bbox. On the CI runner that request landed after the count was taken, so the pollutant change looked like a refetch. The test now waits until grid requests stop and the map is idle before counting; the pollutant change itself still must not fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3QFugXbaTxPuuP3q3rR16
@coinmandeer
coinmandeer merged commit 08597b2 into main Sep 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants