8.4 KiB
Stations (Gas/Fuel) Feature — Dispatchable Change Plan
This document is written as an execution plan that can be handed to multiple AI agents to implement in parallel.
Repo Constraints (Must Follow)
- Docker-first workflow (production builds): validate changes via
make rebuildand container logs. - Mobile + Desktop requirement: every UI change must be validated on both
frontend/src/features/stations/pages/StationsPage.tsx(desktop) andfrontend/src/features/stations/mobile/StationsMobileScreen.tsx(mobile). - Never expose the Google Maps API key to the browser or logs.
Scope
- Fix broken station photo rendering on stations UI after the “hide Google API key” change.
- Add navigation links for saved/favorite stations:
- “Navigate in Google” (Google Maps)
- “Navigate in Apple Maps” (Apple Maps)
- “Navigate in Waze” (Waze)
Bug: Station Photos Not Displaying
Current Implementation (What Exists Today)
- Frontend cards render an
<img>via MUICardMediawhenstation.photoReferenceis present:frontend/src/features/stations/components/StationCard.tsx- URL generation:
frontend/src/features/stations/utils/photo-utils.ts→/api/stations/photo/:reference
- Backend exposes a proxy endpoint that fetches the Google Places photo (server-side, using the secret key):
- Route:
GET /api/stations/photo/:reference backend/src/features/stations/api/stations.routes.tsbackend/src/features/stations/api/stations.controller.ts- Google client:
backend/src/features/stations/external/google-maps/google-maps.client.ts(fetchPhoto)
- Route:
Likely Root Cause (Agents Must Confirm)
The photo endpoint is protected by fastify.authenticate, but <img src="..."> requests do not include the Authorization header. This results in 401 Unauthorized responses and broken images.
Second thing to confirm while debugging:
- Verify what
Station.photoReferencecontains at runtime:- expected: Google
photo_referencetoken - risk: code/docs mismatch where
photoReferencebecame a URL like/api/stations/photo/{reference}, causing double-encoding bygetStationPhotoUrl().
- expected: Google
Repro Checklist (Fast Confirmation)
- Open stations page and observe broken images in browser devtools Network:
GET /api/stations/photo/...should show401if auth-header issue is the cause.
- Confirm backend logs show JWT auth failure for photo requests.
Decision: Image Strategy (Selected)
Selected: Option A1 (keep images; authenticated blob fetch in frontend; photo endpoint remains JWT-protected).
Option A (Keep Images): Fix Auth Mismatch Without Exposing API Key
Option A1 (Recommended): Fetch Photo as Blob via Authenticated XHR
Why: Keeps /api/stations/photo/:reference protected (prevents public key abuse), avoids putting JWT in query params, and avoids exposing the Google API key.
Implementation outline:
- Frontend: replace direct
<img src="/api/stations/photo/...">usage with an authenticated fetch that includes the JWT (via existing AxiosapiClientinterceptors), then render viablob:object URL.- Add a small component like
StationPhotoused byStationCard:apiClient.get('/stations/photo/:reference', { responseType: 'blob' })URL.createObjectURL(blob)for displayURL.revokeObjectURLcleanup on unmount / reference change- graceful fallback (hide image) on 401/500
- Add a small component like
- Backend: no route auth changes required.
Tradeoffs:
- Slightly more frontend code, but minimal security risk.
- Must ensure caching behavior is acceptable (browser cache won’t cache
blob:URLs; rely on backend caching headers + client-side memoization).
Option B (Remove Images): Simplify Cards
Why: If image delivery adds too much complexity or risk, remove images from station cards.
Implementation outline:
- Frontend: remove
CardMediaphoto block fromStationCardand any other station photo rendering. - Leave
photoReferencein API/types untouched for now (or remove later as a cleanup task, separate PR). - Update any tests that assert on image presence.
Tradeoffs:
- Reduced UX polish, but simplest and most robust.
Feature: Navigation Links on Saved/Favorite Stations
UX Requirements
- On saved station UI (desktop + mobile), provide 3 explicit navigation options:
- Google Maps
- Apple Maps
- Waze
- “Saved/favorite” is interpreted as “stations in the Saved list”; favorites are a subset.
URL Construction (Preferred)
Use coordinates if available; fall back to address query if not.
- Google Maps:
- Preferred:
https://www.google.com/maps/dir/?api=1&destination=LAT,LNG&destination_place_id=PLACE_ID - Fallback:
https://www.google.com/maps/search/?api=1&query=ENCODED_QUERY
- Preferred:
- Apple Maps:
- Preferred:
https://maps.apple.com/?daddr=LAT,LNG - Fallback:
https://maps.apple.com/?q=ENCODED_QUERY
- Preferred:
- Waze:
- Preferred:
https://waze.com/ul?ll=LAT,LNG&navigate=yes - Fallback:
https://waze.com/ul?q=ENCODED_QUERY&navigate=yes
- Preferred:
Important: some saved stations may have latitude/longitude = 0 if cache miss; treat (0,0) as “no coordinates”.
UI Placement Recommendation
- Desktop saved list: add a “Navigate” icon button that opens a small menu with the 3 links (cleaner than inline links inside
ListItemText).- File:
frontend/src/features/stations/components/SavedStationsList.tsx
- File:
- Mobile bottom sheet (station details): add a “Navigate” section with the same 3 links as buttons.
- File:
frontend/src/features/stations/mobile/StationsMobileScreen.tsx
- File:
Work Breakdown for Multiple Agents
Agent 1 — Confirm Root Cause + Backend Adjustments (If Needed)
Deliverables:
- Confirm whether photo requests return
401due to missing Authorization. - Confirm whether
photoReferenceis a raw reference token vs a URL string. - Implement backend changes only if Option A2 is chosen.
Files likely touched (Option A2 only):
backend/src/features/stations/api/stations.routes.ts(remove auth preHandler on photo route)backend/src/features/stations/api/stations.controller.ts(add stricter validation; keep cache headers)backend/src/features/stations/docs/API.md(update auth expectations for photo endpoint)
Agent 2 — Frontend Photo Fix (Option A1) OR Photo Removal (Option B)
Deliverables:
- Option A1: implement authenticated blob photo loading for station cards.
- Option B: remove station photos from cards cleanly (no layout regressions).
Files likely touched:
frontend/src/features/stations/components/StationCard.tsx- Option A1:
- Add
frontend/src/features/stations/components/StationPhoto.tsx(or similar) - Potentially update
frontend/src/features/stations/utils/photo-utils.ts - Add unit tests under
frontend/src/features/stations/__tests__/
- Add
Agent 3 — Navigation Links for Saved Stations (Desktop + Mobile)
Deliverables:
- Create a single URL-builder utility with tests.
- Add a “Navigate” menu/section in saved stations UI (desktop + mobile).
Files likely touched:
frontend/src/features/stations/utils/(newnavigation-links.ts)frontend/src/features/stations/components/SavedStationsList.tsxfrontend/src/features/stations/mobile/StationsMobileScreen.tsx- Optional: reuse in
frontend/src/features/stations/components/StationCard.tsx(only if product wants it outside Saved)
Agent 4 — Tests + QA Pass (Update What Breaks)
Deliverables:
- Update/extend tests to cover:
- navigation menu/links present for saved stations
- photo rendering behavior per chosen option
- Ensure both desktop and mobile flows still pass basic E2E checks.
Files likely touched:
frontend/cypress/e2e/stations.cy.tsfrontend/src/features/stations/__tests__/components/StationCard.test.tsx- New tests for
navigation-links.ts
Acceptance Criteria
- Station photos render on station cards via Option A1 without exposing Google API key (no
401responses for photo requests in Network). - Saved stations show 3 navigation options (Google, Apple, Waze) on both desktop and mobile.
- No lint/test regressions; container build succeeds.
Validation (Container-First)
- Rebuild and watch logs:
make rebuildthenmake logs - Optional focused logs:
make logs-frontendandmake logs-backend - Run feature tests where available (prefer container exec):
- Backend:
docker compose exec mvp-backend npm test -- features/stations - Frontend:
docker compose exec mvp-frontend npm test -- stations - E2E:
docker compose exec mvp-frontend npm run e2e
- Backend: