Clear boundary source data when:
- Clicking outside the marker circle (deselecting)
- Clicking a new place (before setting new boundary)
- Clicking empty map area (generic map click)
This ensures:
1. Re-clicking the same city after dismissing re-renders boundary
(selectedPlace is properly cleared, triggering fresh useEffect)
2. Clicking away doesn't leave stale boundary outline
(boundary source explicitly cleared to empty FeatureCollection)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes three critical bugs:
1. CRASH: Guard addSource/addLayer calls with existence checks to
prevent "Source already exists" errors in React strict mode
double-mount scenarios
2. BOUNDARY: Wrap boundary update logic in a function and properly
handle async style loading - check isStyleLoaded() and use
map.once('load') as fallback
3. FONTS: Use 'Noto Sans Regular' for highlight layers instead of
'Noto Sans Medium'/'Noto Sans Bold' which 404 on protomaps CDN
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace feature-state based highlighting with filter-based approach
using dedicated highlight layers. PMTiles don't have feature IDs,
causing setFeatureState to silently fail. The new approach:
- Creates hover-hl-* and selected-hl-* layers per source-layer
- Uses EMPTY_FILTER to hide layers by default
- Updates filter to match feature name when highlighting
- Preserves all existing functionality (zoom, boundary, place card)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add visual hover feedback on map labels (cities, POIs, regions)
using MapLibre feature-state paint property expressions
- Hover: cursor pointer + brighter text + subtle halo glow
- Click highlight: accent color text + accent glow halo
- Highlight persists until place card closed or different feature clicked
- Remove DOM overlay marker for feature mode (use native paint instead)
- Consolidate interactive layers into INTERACTIVE_LAYERS constant
- Re-apply styles on theme change
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove redundant "Read more (local)" links from WikiSummarySection
- Summary text now stands alone with population info
- LINKS section now shows Wikipedia/Wikivoyage/Wikidata with brand icons
- Use simple monochrome SVG icons for each wiki service
- Wikipedia and Wikivoyage show (local) indicator when served from Kiwix
- Falls back to wikipedia.org when wiki_url is not available
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When wiki_url is present from the API (has_kiwix_wiki enabled with local
coverage), use it instead of building a wikipedia.org link. Shows article
name with (local) indicator consistent with the Read More link.
Also adds wikivoyage_url link when available.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a place is selected without osm_type/osm_id (e.g., clicking
on a basemap label), trigger a reverse geocode to obtain the OSM
identifiers. This enables fetching place details including boundary,
wiki_summary, and other enriched data.
The existing placeDetails useEffect will then fire once the
osm_type/osm_id are available in the selectedPlace.raw object.
Co-Authored-By: Claude <noreply@anthropic.com>
When place API response includes wiki_summary, display:
- Summary text in PlaceCard and PlaceDetail
- Population with Users icon if wiki_population present
- "Read more" link to wiki_url (local Kiwix article)
- "Travel guide" link if wikivoyage_url present
- (local) indicator on wiki links to signal Kiwix-served content
Gate on has_kiwix_wiki feature flag - no changes when disabled
or when wiki_summary not present in response.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add whiteSpace: nowrap to Get Directions button to prevent text wrap
- Add ScaleControl (imperial units) to bottom-right of map
- Add dark theme styling for scale bar
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The load event only fires once during map init. When route state
changes before style is fully loaded, the once(load) handler
never executes because load already fired. idle fires after every
render cycle, ensuring the route draws once the style is ready.
B14: Comment out RadialMenu render — all 5 wedges show "coming soon"
toasts with no functional actions. Code preserved for when
actions are wired.
B5: Add detailed JSDoc to fetchAuthState explaining the
redirect:manual pattern and its dependencies on Caddy/Authentik
configuration.
B9: Investigated — user-select CSS errors come from MapLibre's own
maplibre-gl.css, not our code. WONTFIX (library issue).
Caddy changes (CT 101):
B6: Fixed recon.echo6.co header stripping bug — same pattern as navi
B8: Added /api/traffic/* to @authed_user (TomTom API must be authed)
The /outpost.goauthentik.io/sign_out path returns 404 on the
Authentik server - it is not a real endpoint. Changed logout
to redirect directly to auth.echo6.co/if/flow/default-invalidation-flow
with a next= param to return to navi.echo6.co after logout.
B1: Add ResizeObserver to MapView.jsx to handle layout settling.
The map canvas had 0 height at init because layout hadnt settled
when the useEffect fired. ResizeObserver calls map.resize() on
any container size change.
B11: Use native outpost start URL for login initiation:
/outpost.goauthentik.io/start?rd=%2F
This properly triggers auth flow and redirects to / after login.
Removed the Caddy /login handler that wasnt redirecting correctly.
The /api/auth/whoami endpoint returns JSON after auth, leaving
users on a raw JSON page. The new /login endpoint triggers
forward_auth and redirects to / after successful auth.
- MapView.jsx: extract addBoundaryLayer function, use getComputedStyle
for accent color (MapLibre rejects CSS vars in paint properties)
- PlaceCard.jsx: gate fetchNearbyContacts on auth.authenticated
- PlaceDetail.jsx: gate fetchNearbyContacts on auth.authenticated
- api.js: replace invalid timeout option with AbortSignal.timeout()
- RadialMenu.jsx: remove user-select from SVG style (Firefox rejects)
- Panel.jsx: add Cancel button for pending directions state
- 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.
When GPS is denied and user clicks Get Directions, StopList now shows:
- Empty origin slot (A) with dashed border and "Search for a starting point" prompt
- Destination slot (B) showing the place name from pendingDestination
Previously only showed small grey text, making it feel like the UI
showed "nothing" after the place card disappeared.
Symptom: clicking Get Directions with GPS denied closed the place card
and showed empty map. User had no UI to pick an origin.
Root cause: commit 9db8cec correctly stopped adding destination to
stops (fixing duplicate), but Panel.jsx only rendered StopList when
panelState was ROUTING/CALCULATED. With pendingDestination set but
stops empty, panelState was IDLE and StopList never mounted.
Fix: showRouteSection is now true when pendingDestination is truthy,
regardless of panelState. showEmptyState is false when pendingDestination
is set. StopList renders and displays its "Search for a starting point"
prompt.
Symptom: HAR capture showed /api/geocode requests with NO lat/lon/zoom
params despite map being centered on Twin Falls. Results returned
out-of-state addresses (Illinois, Iowa, Arkansas).
Root cause: SearchBar subscribed to mapCenter via React hook, but the
value was stale at search time due to render timing.
Fix: api.js searchGeocode now reads mapCenter directly from the store
via useStore.getState() at call time. This is the correct pattern for
non-component code. SearchBar no longer passes mapCenter as a param.
Bug: clicking Get Directions with GPS denied would add the destination
to stops AND set pendingDestination. Then when user selected an origin
via search, selectResult would add the origin AND re-add the pending
destination, resulting in 3 stops (A=dest, B=origin, C=dest again).
Fix: in startDirections GPS-denied path, only set pendingDestination
without adding to stops. The SearchBar selectResult handler already
adds both origin and pending destination when pending exists.
Fixes React error #185 (infinite re-render loop) caused by returning
object from Zustand selector without shallow comparison.
Changed usePanelState to return string states that encode both preview
and route status:
- PREVIEW_CALCULATED: preview + calculated route
- PREVIEW_ROUTING: preview + stops (no route yet)
- PREVIEW: preview only
- ROUTE_CALCULATED: calculated route only
- ROUTING: stops only
- IDLE: nothing
Panel.jsx updated to derive show flags from string states using
startsWith and includes checks.
Three regressions fixed:
1. mapCenter is now initialized on map 'load' event, not just 'moveend'.
Searches immediately after page load now correctly include viewport
bias instead of falling back to default Twin Falls coords.
2. setThemeOverride had stray code from startDirections that wiped
stops and added undefined place data. Toggling theme cleared the
active route. Restored setThemeOverride to its correct
theme-only implementation.
3. usePanelState returned ROUTE_CALCULATED before checking selectedPlace,
so preview cards could never appear alongside a calculated route.
Refactored to decouple preview state from route state - preview
renders whenever selectedPlace exists, independent of route state.
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
Three improvements:
1. When a place has a boundary polygon (from Nominatim), render its
outline on the map using a dashed accent line. Falls back to the
existing pulsing ring for places without polygons.
2. Selecting a feature now smoothly zooms the map to fit:
- With polygon: fitBounds to polygon bbox
- Without polygon: zoom level based on feature kind (city=11,
region=7, POI=16, etc.)
Terrain clicks do not change zoom.
3. Wikidata IDs render as styled 'View on Wikidata' links instead
of raw 'Wikidata: Qxxxxx' strings.
- Snap selection to feature geometry when clicking labeled places
- Add wikidata enrichment for basemap labels (population, description)
- Differentiate visual feedback: reticle marker vs pulsing highlight
- Clear previous feature highlight when selection changes
- Store selection mode (reticle/feature) and feature info in state
Frontend: MapView click handler, PlaceDetail wikidata fetch, CSS
Backend: /api/place/wikidata/<id> route for Wikidata API lookups
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Click handler now queries rendered features at click point and uses
the highest-priority labeled feature (POI > locality > region) as
the place identifier. Previously, clicks went straight to coord-based
reverse-geocode, causing 'Twin Falls' label clicks to resolve to
whatever feature was nearest (e.g., a radio tower).
Falls back to reverse-geocode when no labeled feature exists at the
click point. Two-click selection model and hover affordance unchanged.
POIs, city names, and other labeled features now show a pointer cursor
on hover, signaling that they are clickable. Matches the standard
map-app interaction pattern (Google Maps, etc.).
Implemented via MapLibre mouseenter/mouseleave handlers for interactive
layers: pois, places_locality, places_region, places_country,
places_subplace.
Replaces 'every click selects something' with a deliberate two-click
flow:
- First click drops marker (existing circle plus new precise center dot)
and opens place panel
- Second click INSIDE the marker circle opens the radial menu
- Second click OUTSIDE the circle deselects without selecting the new
spot — requires another click to select
The 4px filled center dot at exact click coordinates gives precise
visual feedback for GPS-coord readout. The existing circle's radius
defines the same-spot tolerance, visually showing the radial-trigger
hit area.
Right-click radial unchanged. Search-dropdown selection drops a marker
for consistency.
Grayed-out wedges shouldn't show the same hover feedback as enabled
wedges — the highlight contradicts the grayed-out 'not available'
signal. Auth-required wedges now stay visually muted on hover while
remaining clickable.
Lock icon overlay was visually busy and partially obscured the wedge
label. Replaced with a grayed-out treatment using reduced opacity for
auth-required wedges. Cleaner read, instantly recognizable as 'not
fully available'.
Left-click on the map already reverse-geocodes and opens the place
panel, making the 'What's here' wedge redundant. Radial drops from
6 to 5 wedges (72° each).
Remaining wedges (all stubs except they show toast):
- Drop pin
- Directions to here
- Save place (auth-gated)
- Add as stop
- Directions from here
Replace unreliable window event listener with transparent full-screen
backdrop element. Clicking anywhere outside the radial menu now properly
dismisses it. Also handles right-click on backdrop for dismiss.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements RadialMenu component (general-purpose, configurable wedges)
and useContextMenu hook (right-click on desktop, 450ms long-press with
8px movement threshold on touch).
First wired action: "What's here" — reverse-geocodes the trigger
location and opens the place panel for the result. Remaining wedges
(Drop pin, Directions from here, Directions to here, Add as stop,
Save place) render but stub to a toast — wiring deferred to follow-up
sessions.
Per design doc NAVI-DIRECTIONS-REDESIGN.md sections covering Phases a
and b of the implementation sequence.
- Add mapCenter state to store (lat/lon/zoom)
- Track map center on moveend in MapView
- Pass mapCenter to searchGeocode from SearchBar
- Update searchGeocode API call to include viewport params
Search results now prioritize locations near the current map view.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds a persistent zoom indicator pill in the bottom-left corner showing
current zoom level (e.g., "Z 11.4"). Updates live as user pans/zooms.
Also includes contour test layer support (blue color scheme) for
development verification of contour pipeline changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Adds contour PMTiles vector source (contours-na.pmtiles)
- Minor/intermediate/index tier rendering at z11+/z8+/z4+
- Elevation labels on index contours at z12+
- Dark theme opacity adjustment
- has_contours feature flag gated
Completes T pipeline integration (Phase 1).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>