- Add /api/auth/whoami endpoint check on app load
- Store auth state in Zustand (authenticated, username, loaded)
- Hide Contacts tab when unauthenticated
- Gate fetchNearbyContacts calls on auth.authenticated
- Replace Save button with Log in affordance when unauthenticated
- Add Login/Logout buttons to panel header
- Prevent any /api/contacts/* requests from firing when unauthenticated
Public functionality (search, routing, place details) remains
fully functional for unauthenticated users.
Major refactor consolidating two-panel layout (Routes/Contacts + floating
PlaceDetail) into one 400px left column with state-driven content.
Architecture:
- New PlaceCard component for preview and stop cards (collapsible)
- Panel states: IDLE, PREVIEW, ROUTING, PREVIEW_ROUTING, ROUTE_CALCULATED
- usePanelState selector in store.js derives state from selectedPlace/stops/route
- StopList now renders stops as PlaceCard with variant=stop
- PlaceDetail.jsx removed from App.jsx (content moved to PlaceCard)
UX refinements:
- Panel width 400px (was 360px) to fit buttons on one line
- Map zoom padding updated to 420px for wider panel
- Body text bumped to text-sm (14px) for readability
- Get Directions button hidden when 2+ stops (route auto-calculates)
- PlaceCard title prefers feature name (raw.name) over formatted address
- Preview card shows above route during PREVIEW_ROUTING state
- Directions flow no longer shows toast when GPS denied
GPS permission:
- Remove silent mount-time getCurrentPosition calls that cause iOS Safari
to cache a "denied" state without ever prompting the user
- LocateButton always retries getCurrentPosition on tap (user gesture)
- Only show "denied" toast on PERMISSION_DENIED (code 1), not timeout
- MapView watchPosition now starts only after confirmed grant, not unconditionally
Traffic toggle:
- Fix isStyleLoaded() race in LayerControl — if style not loaded when
toggle fires, defer to map.once("style.load") instead of silently bailing
- Change outside-click handler from mousedown to pointerdown for mobile
Mobile UX (from prior session):
- Add LocateButton component (crosshair GPS locate/re-center)
- Reposition layer control + locate button to top-right on mobile
(below MapLibre nav controls, above bottom sheet)
- ModeSelector: add min-w-0 to prevent flex overflow at 390px
- StopItem: remove button visible on touch (60% opacity vs hover-only)
- Panel: overflow-x-hidden + safe-area-inset-bottom on mobile sheet
- Body overflow-x guard on mobile viewports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New LayerControl component with popover toggles for hillshade/traffic
- MapView: add/remove hillshade raster-dem and traffic raster layers
- Overlay layers persist in localStorage, survive theme swaps
- Hillshade defaults ON, traffic defaults OFF when available
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds synthetic "Your location" stop A when GPS granted; place
detail panel slides in on search result click with Directions /
Add stop / Save (stub) / Share actions; elevation via Valhalla
/height; react-hot-toast for feedback; pendingDestination state
for GPS-denied Directions flow.
Phase 3 Step 5 C1 of Navi.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
layersWithCustomTheme() expects a Theme object as its second argument,
not a string key. Passing "dark" (string) caused all layer paint
properties to be empty objects — rendering tiles invisibly on black
background. Switched to layers(source, namedTheme("dark"), {lang:"en"})
which is the canonical v4 API. Added v4 sprite URL for POI icons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Minimal Idaho basemap via local PMTiles extract from Protomaps
daily build (20260419). Deployed to /mnt/nav/frontend/ via rsync,
served by nginx on :8440 as navi.echo6.co.
Phase 3 Step 1 of Navi frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>