Feature: Web frontend (apps/web)
ID: 0013Status: In progressOwner: @satyaCreated: 2026-06-07Updated: 2026-06-07Related ADRs: 0002 (Supabase + NestJS), 0003 (Python workers), 0009 (web frontend)1. Why
Section titled “1. Why”Treeper is mobile-only at v0, but planning and comparing trips is often a desktop/tablet task, and there’s a mature web map UI (TripViz) ready to reuse. A web frontend lets travellers view, import, discover, and compare trips on a big screen — reusing the existing API + workers.
2. Who it is for
Section titled “2. Who it is for”The Planner (desktop trip building/comparison) and the Traveller (glanceable map of a trip). Share-link viewers (no account) come in a later milestone.
3. Scope
Section titled “3. Scope”In scope
Section titled “In scope”F0013.1apps/web— Vite + React + TS app in the monorepo; adopts the backend data model natively; consumespackages/contracts. Retires TripViz’s standalone schema + Express server.F0013.2Auth — Supabase login (email/password + Google OAuth); session JWT attached to every API call; sign-out. (M1)F0013.3Render trips — fetch the user’s trips (GET /v1/trips) + days + activities; map markers byactivity.kind, swipeable day cards, per-day route line; multi-trip switch + compare overlay (color-coded). (M1)F0013.4Import — paste reel/URL or upload PDF/image →POST /v1/trips/:id/imports→ poll → review → commit (reuses the bottom-sheet import UI). (M2)F0013.5Discovery —POST /v1/itineraries/search(location + days) → render/compare results;clone-into-trip. (M3)F0013.6Edit — trip/day/activity CRUD with optimistic UI. (M4)F0013.7Media — attachment carousels + signed-URL uploads; links as chips. (M5)F0013.8Public view —GET /v1/s/:slugread-only shared trip. (M6)F0013.9Deploy — Coolify app atapp.itssatya.in; prod CORS origin. (M7)
Out of scope
Section titled “Out of scope”- AI/scraping logic itself (lives in workers, ADR-0003) — the web only calls the API.
- Offline-first / push notifications (mobile concerns).
- DB schema changes — the web reuses existing tables; no migrations.
4. User stories
Section titled “4. User stories”- As a Planner, I can sign in on the web and see my trips on a map, so that I can review plans on a big screen.
- As a Planner, I can switch between and overlay/compare up to 3 trips, so that I can weigh options.
- As a Traveller, I can swipe day-by-day through a trip’s activities on the map.
- (M2+) As a Planner, I can paste a reel/URL or upload a PDF and have it become a mapped plan.
5. UX notes
Section titled “5. UX notes”Full-screen MapLibre map; top trip-switcher chips (color-coded) + compare toggle + sign-out; bottom day card (Tinder-style swipe) on phones, side panel on wide screens. Markers colored by activity kind with a trip-color ring. Login is a centered card (email + Google).
6. Acceptance criteria (Milestone 1 — Auth + render)
Section titled “6. Acceptance criteria (Milestone 1 — Auth + render)”- AC1: With no session, the app shows the login gate; nothing else is reachable. ✅ (verified)
- AC2:
tsc --noEmitandvite buildpass forapps/web. ✅ (verified) - AC3: Email/password and Google sign-in obtain a Supabase session; sign-out clears it.
- AC4: After sign-in,
GET /v1/tripsis called withAuthorization: Bearer <jwt>; trips appear as switcher chips; the active trip’s days + activities render on the map with day cards + route. - AC5: An activity with null coords is listed in the card but not placed on the map.
- AC6: With ≥2 trips, Compare overlays all trips color-coded; tapping a chip/legend focuses one.
- AC7: The browser makes successful CORS calls from the web origin (no CORS errors).
AC1/AC2 verified in-sandbox. AC3 (email sign-in) verified against live Supabase from the sandbox browser. AC4/AC7 require a backend reachable from the browser with CORS deployed — as of last check the deployed backend returns no
access-control-allow-origin(OPTIONS preflight 404), soapps/backendmust be redeployed withenableCors+CORS_ORIGINS. AC5/AC6 verify once trips load.
6b. Acceptance criteria (Milestone 2 — Import)
Section titled “6b. Acceptance criteria (Milestone 2 — Import)”- AC8: An Import button (TopBar + empty state) opens a bottom sheet to paste a reel/TikTok/ YouTube/article link or notes, and/or attach PDF/image/video files, choosing a target (existing trip or New trip). ✅ (UI verified in-sandbox)
- AC9: Submitting creates the trip if needed, signs+uploads any files to
trip-attachments,POST /v1/trips/:id/imports(combined sources +user_context), then pollsGET /v1/imports/:iduntilready/failed. (requires reachable backend + workers) - AC10: On
ready, a review screen shows the parsed destinations + activities grouped by day with a confidence badge; Add commits all (POST /v1/imports/:id/commit) and the trip reloads on the map; Discard callsDELETE /v1/imports/:id. - AC11: For a New trip, the trip’s
end_dateis widened to fit the draft’s day span before commit soday_indexmapping isn’t clamped onto day 1. - AC12:
tsc --noEmit+vite buildpass with the import code. ✅ (verified)
6c. Geocoding accuracy (Milestone 2.1 — region-anchored pins)
Section titled “6c. Geocoding accuracy (Milestone 2.1 — region-anchored pins)”The first import pass geocoded each activity title against a global engine with no
country bias, landing pins in the wrong country (“Ella”→Switzerland, “Yala”→Thailand,
“Colombo Airport”→Vancouver) and missing descriptive titles entirely (42/55 null on the
Sri Lanka test trip). Fixed in apps/workers/.../geocode:
- AC13: Enrichment is region-anchored — destinations are geocoded first to derive the trip’s dominant country + bounding box; activities are then resolved inside that region (title-as-POI → city label → destination fallback), and out-of-region matches are rejected.
- AC14: A committed trip can be re-geocoded in place via
POST /v1/trips/:tripId/reenrich(ownership-gated; forwards to the workers-token surfacePOST /ai/trips/reenrich). The web TopBar exposes a “Fix pins” action that calls it and reloads the trip. - AC15: The web map frames to the outlier-trimmed point set, so a single stray pin can’t blow the viewport out to the whole globe. ✅ (unit-verified worker + web build)
7. Endpoint surface (consumed; no new endpoints)
Section titled “7. Endpoint surface (consumed; no new endpoints)”- M1:
GET /v1/trips,GET /v1/trips/:id/days,GET /v1/trips/:tripId/activities. - M2:
POST /v1/trips(create target),PATCH /v1/trips/:id(widen dates),POST /v1/trips/:tripId/imports/sign-upload,POST /v1/trips/:tripId/imports,GET /v1/imports/:id(poll),POST /v1/imports/:id/commit,DELETE /v1/imports/:id. File upload uses Supabase storageuploadToSignedUrl. Per-user import rate limit is 20/day (429). - Backend change: enable CORS (
CORS_ORIGINS) inapps/backend/src/main.ts. (No DB migrations.)
8. Data model
Section titled “8. Data model”Reuses existing tables (trips, trip_destinations, trip_days, activities, activity_attachments,
activity_links). Shared TS mirror in packages/contracts/src/index.ts.
9. Testing plan
Section titled “9. Testing plan”- Build/typecheck in CI. Manual: sign in → trips render (Preview MCP, mobile + tablet). Negative: logged-out gate, expired-token refresh, null-coord activity, empty-trips state.
10. Open questions / follow-ups
Section titled “10. Open questions / follow-ups”- Extract more DTOs into
packages/contracts; backend consumes it. - Deep-link
?trip=/?trips=to preselect; share-link route (M6). - Code-split MapLibre.