Skip to content

Nearby search reliability + PWA nav gap fix + unified pin/icon design - #11

Merged
Osyna merged 3 commits into
mainfrom
feature/cartoon-ui-skin
Aug 10, 2026
Merged

Osyna merged 3 commits into
mainfrom
feature/cartoon-ui-skin

Conversation

@Osyna

@Osyna Osyna commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

This PR has grown across a few related fixes to the same session's UI/reliability work — see individual commits for full detail on each:

1. Nearby search: bound worst-case latency, avoid rate limits, fix precision

'Find in this area' could take 27s+ (or hang) before failing, and could show places outside the visible map. Root-caused with live measurements against the real public Overpass mirrors: tightened timeouts, added a hard response deadline, a concurrency limiter + per-mirror circuit breaker to avoid 429s, in-flight request coalescing, batched cache lookups, and a new refineToViewport() that trims results to the real requested bbox sorted by distance instead of an arbitrary truncation order.

2. Fix big empty gap below bottom nav on iOS PWA (standalone only)

Reported on iPhone 17 Pro Max, PWA-only. Root cause: the app's height: 100% full-screen chain is a known mismatch source once iOS runs the app standalone (edge-to-edge, no Safari chrome). Switched to 100dvh. Not pixel-verified on a physical device (none available here, Chromium can't emulate standalone mode) — please confirm on-device.

3. Unified pin/icon design across Search, Discover, and Map

The search-result preview pin (and its popup, and the Discover results list) still used the pre-cartoon flat-circle/dot design instead of the icon+color markers 'Find in this area' uses. Root cause was real duplication — four different hand-maintained icon/color tables had drifted apart (e.g. stores/friends.js had a stale, different-colored palette than stores/places.js, so friends' categories rendered in the wrong colors). Centralized on categoryIcons.js, added a shared CategoryIconBubble.vue component, migrated every affected spot, verified pixel-identical parity on existing UI and correct new rendering on the fixed spots.

All: build clean, 32/32 tests passing (11 new across this PR).

Osyna added 3 commits August 10, 2026 18:52
…sion

'Find in this area' could take 27s+ or hang before failing, and could
show places outside the visible map. Root-caused with live measurements
against the real public Overpass mirrors (not just code reading) and
fixed each concrete cause:

Timeouts
- Per-mirror fetch timeout 20s -> 6s (live testing showed a dead mirror
  hanging the full 20s, or a struggling one 504'ing after ~7-8s — no
  reason to wait that long before rotating)
- New hard deadline (14s) on the whole 'fetch missing cells' stage,
  independent of mirror count: whatever settles in time is returned,
  stragglers keep running in the background and still populate the
  cache for next time instead of holding the HTTP response open.
  /nearby now always answers promptly even during a full outage —
  verified via a real Playwright click-through that it never hangs,
  always resolving to a clean state
- Client-side AbortSignal.timeout(18s) in useNearby.js as a last-resort
  net for cases the server-side deadline can't cover

Rate limits (429s)
- Public Overpass instances allow ~2 concurrent slots per IP; the old
  code fired every missing cell (up to 12) fully in parallel at the
  same shared server IP — about the fastest way to get rate-limited.
  Added a process-wide concurrency limiter (3) so bursts queue instead
  of hammering upstream
- Per-mirror circuit breaker: a mirror that just 429'd/timed out is
  penalized for 45s and skipped, so only the first request pays its
  cost, not every subsequent one
- Healthy-mirror order is now shuffled per call — otherwise a burst of
  concurrent cell fetches all try the same 'looks fine so far' mirror
  before any single one has failed fast enough to teach the breaker
- In-flight request coalescing per cell key: concurrent requests
  (different users, or a fast double-click/retry) for the same missing
  cell now share one upstream call instead of firing duplicates
- Client-side in-flight de-dupe in useNearby.js: re-clicking while a
  request is already running reuses it instead of firing a second one

Speed
- Batched the per-cell cache lookup into one Prisma findMany instead of
  one findUnique per cell (was fully sequential — up to 12 DB round
  trips before any upstream call even started)

Precision
- Grid cells always fully cover the requested viewport and routinely
  extend past it, so results could include markers outside what the
  user actually asked about. New refineToViewport() trims the merged
  set back to the real bbox and sorts by distance to its center, so
  any truncation drops the farthest results first, not an arbitrary
  OSM-id-ordered tail. Verified: 0/59 out-of-bounds elements on a real
  Lyon query

A caught mistake: tried adding overpass.osm.ch as a third mirror after
it tested fast and responsive, but a follow-up global-coverage check
showed it's a *regional* Swiss instance — 120 real elements for Zurich,
0 for Lyon/Paris/Berlin. A fast wrong answer (silently 'no places
found') is worse than a slow right one, so it was dropped; the two
already-configured mirrors are the well-established global ones.
Also found and purged 10 GeoCache rows that got contaminated with
empty results from that mirror during testing, so they'll regenerate
correctly.

Tests: overpassGrid (12, +7 new for refineToViewport/distanceMeters),
geoProxy (8, +4 new: oversized bbox, viewport trimming, coalescing,
deadline-bounded response — the deadline test uses a test-only setter
to shrink 14s to 150ms so the suite doesn't actually sleep). 26/26
passing, build clean.
The app's full-screen height chain (html/body/#app) used 'height: 100%',
which resolves against the viewport under normal browsing but is a
well-documented source of mismatch once an iOS PWA runs standalone
(added to home screen) — no Safari chrome, edge-to-edge WKWebView,
viewport-fit=cover + apple-mobile-web-app-status-bar-style:
black-translucent only actually taking effect in that mode. The
result: the app's own containers can end up shorter than the real
visible screen, leaving empty page background showing below the
fixed bottom tab bar — reported as a ~2-button-tall gap on iPhone 17
Pro Max, PWA only (not in a regular Safari tab).

Switched to 100dvh (dynamic viewport height), the unit purpose-built
by browser vendors to track the actual visible viewport correctly
across exactly this class of mismatch, with the previous 100% kept
as a fallback for browsers predating dvh support.

Note: I don't have a physical iOS device in this environment and
Chromium can't emulate display-mode:standalone or iOS's viewport
handling, so this couldn't be pixel-verified end-to-end here — it's
the well-established fix for this exact, widely-documented symptom.
Verified the change doesn't regress normal rendering (simulated a
realistic --safe-bottom value, confirmed 0px gap, build clean).
The temp-pin shown when previewing a search result (Discover tab or the
map's own search bar) was still the pre-cartoon design: a flat colored
MapLibre circle with no icon, and its popup used a plain amber dot —
both inconsistent with the icon+color canvas markers 'Find in this
area' already uses for nearby results. Same story in the Discover
results list: each row showed a 10px flat dot (only 2 states: muted
grey or accent-orange) instead of a real per-type icon.

Root cause was duplication, not just a missed spot: 'icon+color per
category/type' was hand-maintained in FOUR different places that had
already drifted from each other — most concretely, stores/friends.js
carried a stale pre-cartoon Tailwind-ish palette (#ef4444, #a855f7...)
completely different from stores/places.js's real one, so a friend
without custom categories would show 'Restaurant' in a different red
than your own places.

Unification:
- categoryIcons.js is now the single source of truth: DEFAULT_CATEGORIES
  (moved from places.js unchanged) plus AMENITY_STYLE, which is DERIVED
  from it (not hand-duplicated) and extended to cover hotel/shop/museum/
  park/pharmacy — everything SearchView's filters need, not just food+drink
- stores/places.js and stores/friends.js both import DEFAULT_CATEGORIES
  from there now — fixes the real friend-category color-mismatch bug
- SearchView's PLACE_FILTERS chips derive icon+color via amenityStyle()
  instead of a third hand-duplicated copy (verified identical output —
  zero visual change, pure de-duplication)
- New CategoryIconBubble.vue: the shared 'icon in a tinted circular
  badge' component (icon+color always together — color is never the
  only signal, matching the map's own accessibility rule), used to fix
  the Discover result-list dots and migrate PlacesView's two existing
  hand-rolled bubble spots onto the same component (verified pixel-
  identical before/after via computed styles: 30x30/10px-radius and
  26x26/9px-radius variants both match exactly)
- MapView's temp-pin: replaced the flat circle layers with a symbol
  layer using the same dashed-ring icon-in-a-bubble style as nearby
  markers (registration is free — MapLibre's existing styleimagemissing
  handler already draws any nearby-marker|icon|color id on demand), and
  its popup now uses the same pp-cat-icon treatment the real nearby-
  marker popup already used, instead of the old flat amber dot
- App.vue's onSearchSelect now threads the search result's OSM type
  through to showTempPin so it can pick the right icon

Verified end-to-end live: Discover search results show real per-type
icon bubbles; selecting one shows a dashed-ring icon pin on the map
identical in style to a 'Find in this area' result, with a matching
popup; existing nearby markers and saved-place markers unaffected
(zero console errors). Build clean, 32/32 tests passing (6 new,
covering the derived-not-duplicated palette and the fallback path).
@Osyna Osyna changed the title Nearby search: bound worst-case latency, avoid rate limits, fix precision Nearby search reliability + PWA nav gap fix + unified pin/icon design Aug 10, 2026
@Osyna
Osyna merged commit 4dd3fa1 into main Aug 10, 2026
1 check passed
@Osyna
Osyna deleted the feature/cartoon-ui-skin branch August 10, 2026 17:45
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.

1 participant