Skip to content

Feature: Web frontend (apps/web)

ID: 0013
Status: In progress
Owner: @satya
Created: 2026-06-07
Updated: 2026-06-07
Related ADRs: 0002 (Supabase + NestJS), 0003 (Python workers), 0009 (web frontend)

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.

The Planner (desktop trip building/comparison) and the Traveller (glanceable map of a trip). Share-link viewers (no account) come in a later milestone.

  • F0013.1 apps/web — Vite + React + TS app in the monorepo; adopts the backend data model natively; consumes packages/contracts. Retires TripViz’s standalone schema + Express server.
  • F0013.2 Auth — Supabase login (email/password + Google OAuth); session JWT attached to every API call; sign-out. (M1)
  • F0013.3 Render trips — fetch the user’s trips (GET /v1/trips) + days + activities; map markers by activity.kind, swipeable day cards, per-day route line; multi-trip switch + compare overlay (color-coded). (M1)
  • F0013.4 Import — paste reel/URL or upload PDF/image → POST /v1/trips/:id/imports → poll → review → commit (reuses the bottom-sheet import UI). (M2)
  • F0013.5 DiscoveryPOST /v1/itineraries/search (location + days) → render/compare results; clone-into-trip. (M3)
  • F0013.6 Edit — trip/day/activity CRUD with optimistic UI. (M4)
  • F0013.7 Media — attachment carousels + signed-URL uploads; links as chips. (M5)
  • F0013.8 Public viewGET /v1/s/:slug read-only shared trip. (M6)
  • F0013.9 Deploy — Coolify app at app.itssatya.in; prod CORS origin. (M7)
  • 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.
  • 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.

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 --noEmit and vite build pass for apps/web. ✅ (verified)
  • AC3: Email/password and Google sign-in obtain a Supabase session; sign-out clears it.
  • AC4: After sign-in, GET /v1/trips is called with Authorization: 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), so apps/backend must be redeployed with enableCors + 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 polls GET /v1/imports/:id until ready/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 calls DELETE /v1/imports/:id.
  • AC11: For a New trip, the trip’s end_date is widened to fit the draft’s day span before commit so day_index mapping isn’t clamped onto day 1.
  • AC12: tsc --noEmit + vite build pass 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 surface POST /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 storage uploadToSignedUrl. Per-user import rate limit is 20/day (429).
  • Backend change: enable CORS (CORS_ORIGINS) in apps/backend/src/main.ts. (No DB migrations.)

Reuses existing tables (trips, trip_destinations, trip_days, activities, activity_attachments, activity_links). Shared TS mirror in packages/contracts/src/index.ts.

  • 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.
  • Extract more DTOs into packages/contracts; backend consumes it.
  • Deep-link ?trip=/?trips= to preselect; share-link route (M6).
  • Code-split MapLibre.