Repository navigation
Nearby search reliability + PWA nav gap fix + unified pin/icon design - #11
Merged
Merged
Conversation
…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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 to100dvh. 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.jshad a stale, different-colored palette thanstores/places.js, so friends' categories rendered in the wrong colors). Centralized oncategoryIcons.js, added a sharedCategoryIconBubble.vuecomponent, 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).