Skip to content

CLAUDE.md — treeper

Travel-planner monorepo. Mobile-first app + web frontend (v2, ported from TripViz), a NestJS API, Python workers (scraping / LLM parse / geocode / media), Supabase, deployed on Coolify at *.itssatya.in.

PathWhatStack
apps/webWeb frontend (v2) — the TripViz map UIVite + React + TS + MapLibre + Tailwind + zustand
apps/mobileMobile app (v0)Flutter
apps/backendREST API /v1, Supabase-JWT authNestJS
apps/workersAI ingest + scraping + geocode + mediaPython FastAPI, instructor + LiteLLM
packages/contractsShared TS entity types (web consumes)TS (path alias @treeper/contracts)
infra/supabaseMigrations (migrations/NNNN_*.sql)Supabase CLI
infra/coolifyDEPLOY.md
specs/SDD: features/NNNN-*.md, adr/NNNN-*.md

Data model: trip → trip_destinations + trip_days → activities{kind: transport|lodging|food|sight|freeform, location_lat/lng, time_start, order_index}; activity_attachments (photos/files), activity_links (URLs). The web reuses these tables natively — no separate schema.

Workers are internal-only (X-Workers-Token); clients call only the NestJS API. The API forwards to workers for AI/geocode/media work.

Runbooks (follow these when asked to run/verify):

  • RUN_LOCAL.md — full local stack (Supabase + backend + workers + web + mobile-on-local).
  • RUN_SIM_PROD.md — mobile on an iOS simulator against the deployed APIs, driven via Maestro (no local backend).

Bring up the stack (each in its own shell). Use email/password auth for local verification.

Terminal window
# 1) Supabase — run the CLI from infra/ (NOT the repo root: root hits a stray 54321 project)
cd infra && supabase start # API :55321 DB :55322 Studio :55323 Mailpit :55324
# local has enable_confirmations=false
# 2) Backend (:3000)
cd apps/backend && npm i && npm run start:dev # reads .env.local
# 3) Workers (:8000)
cd apps/workers && uv run uvicorn treeper_workers.main:app --reload
# 4) Web (:5173) — PROD MODE BY DEFAULT
cd apps/web && npm i && npm run dev # = vite --mode prod (uses .env.prod)
# npm run dev:local → local stack (.env.local)

.env.local files are gitignored. Web needs VITE_API_BASE, VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY. client.ts normalises the API base to exactly one /v1.

Tests / typecheck (run before every commit)

Section titled “Tests / typecheck (run before every commit)”
Terminal window
cd apps/workers && uv run pytest -q # worker suite
cd apps/backend && npx tsc --noEmit # NB: piping to `tail` masks the exit code — check `echo $?`
cd apps/web && npx tsc --noEmit && npm run build

Bug fixes: Red → Green. Worker enrichment code is unit-tested with fakes (no network) because SearXNG/Nominatim aren’t reachable from the sandbox.

Coolify CLI v1.6.2, context personal-coolify (coolify.itssatya.in). Apps track the main branch, so deploy = merge → push → redeploy.

AppUUIDFQDN
backendo14cr95wpi4d3m76grtxhnjxapi-treeper.itssatya.in
workersr12jao9q6nzqxxujalfs0cj3workers-treeper.itssatya.in
web-appaol2ofb4kp60jq2vo02jkbkitreeper.itssatya.in
supabasesupabase-treeper.itssatya.in
Terminal window
git checkout main && git merge --no-ff <branch> && git push origin main
coolify app deploy <uuid> # returns a deployment UUID; builds are queued/serialized
coolify app env list <uuid> # values are masked; -s shows sensitive

Coolify CLI gotchas (learned the hard way)

Section titled “Coolify CLI gotchas (learned the hard way)”
  • coolify deploy get <uuid> status is STALE/unreliable — it showed queued for already-finished deploys. Don’t trust it.
  • True status leaks from coolify deploy cancel <uuid>: it errors ...Current status: finished|cancelled-by-user|.... (But a real yes cancels a running deploy — don’t probe a live one with yes.)
  • coolify deploy list is broken (JSON unmarshal error). Don’t use it.
  • Reliable readiness signals instead:
    • New HTTP route → poll it. FastAPI workers expose /openapi.json publicly (lists routes regardless of auth); a new backend route returns 200 instead of 404.
    • After deploy, exercise the actual change (e.g. re-enrich response includes new fields only when the new worker is live).
  • Traefik port labels don’t auto-regenerate when you change ports_exposes. Symptom: 502. Fix: re-set the domain (coolify app update <uuid> --domains https://...) to regenerate loadbalancer.server.port, then restart.
  • Web is a Dockerfile build (node→nginx, repo-root context for packages/contracts). VITE_* are build args (inlined at build time). nginx listens on 80 (IPv4+IPv6); healthcheck uses 127.0.0.1.
  • Shell can reach api-treeper.itssatya.in and workers-treeper.itssatya.in; cannot reach treeper.itssatya.in (cloudflared tunnel) — verify the web UI via the user’s browser (Claude-in-Chrome) or by checking the API data the map renders.
  • Nominatim is blocked in the sandbox; SearXNG is internal-only (geocode + media verify on prod, not locally).
  • JWT for live API checks: POST supabase-treeper.itssatya.in/auth/v1/token?grant_type=password with the anon key + the test creds; tokens expire — re-auth between long steps.
  • Claude-in-Chrome: a fresh MCP tab reports a Placeholder WebUI for ~3s after navigate before the SPA loads — wait, then screenshot.

The import pipeline parses a source → ItineraryDraft (instructor-forced schema), then runs best-effort, budgeted, non-terminal enrichment passes before persist:

  1. Geocode (geocode/) — region-anchored: geocode destinations first, derive the dominant country + bbox, then resolve activities inside that region (title-as-POI → city label → destination-coord fallback), rejecting out-of-region matches. Stops “Ella → Switzerland”.
  2. Media (media/) — per-activity photos (SearXNG images) + one video link (SearXNG videos), kind-aware (no media for transport; video only for sight/food/freeform). Stores external URLs (no rehosting).
  3. Kind — the structuring/vision/video prompts classify each activity; ai/kind_infer.py is the keyword repair classifier for existing trips.

Re-enrich (geocode/reenrich.py) repairs an already-committed trip in place: re-geocode + re-classify kind + back-fill media (deduped, 8-image cap). Exposed as POST /v1/trips/:tripId/reenrich (ownership-gated) → worker POST /ai/trips/reenrich. The web TopBar “Fix pins” button calls it.

Commit (apps/backend/.../imports.service.ts) persists draft media → activity_attachments (kind image, url; storage_path mirrors the URL since it’s NOT NULL) + activity_links. Web hydrates a trip in 2 bulk calls: GET /v1/trips/:id/attachments + /links.

Design guidelines (web — TripViz visual system)

Section titled “Design guidelines (web — TripViz visual system)”

Keep the web UI matching this system; don’t regress to plain.

  • Palette: bg #0b1020, card #121a33/80, text #e6e9f2 + white/40–70 for hierarchy. Per-trip accents: indigo #6366f1, emerald #10b981, rose #f43f5e (max 3 trips).
  • Activity kind → icon/color (lib/activityMeta.tsx): sight=sky #0ea5e9 Landmark · food=red #ef4444 Utensils · lodging=purple #8b5cf6 BedDouble · transport=emerald #10b981 Train · freeform=indigo #6366f1 Sparkles.
  • Glass: backdrop-blur-xl, bg-[#121a33]/80, border-white/10, shadow-2xl, rounded-3xl cards / rounded-2xl rows.
  • Route line (map/RouteLayer.tsx): dual-layer glow — glow line-width 9, blur 8, opacity .3 + core width 3.2, opacity .95, round caps; animated draw (~1100ms cubic ease-out).
  • Markers (map/ActivityMarkers.tsx): photo-thumbnail marker when the activity has a photo (48px rounded image, white ring + trip-color outer ring, kind-icon badge); else a kind-colored pill. Spring entrance (stiffness 420, damping 24, staggered), active scale 1.12. Compact (compare) = small dots. Map frames to withoutOutliers() points so one stray pin can’t blow out the viewport.
  • Media (cards/MediaCarousel.tsx): horizontal snap carousel (h-28, rounded-xl) of photos + video play-cards (thumbnail + play badge + platform icon); non-video links as chips. Dot indicators (active 14px / idle 5px).
  • Motion: prefer framer-motion spring over linear; day deck swipe (90px / 450px·s⁻¹ threshold), stiffness 320, damping 32.
  • Mobile-first: day card bottom on phones, right side panel on wide; env(safe-area-inset-*).

Correctness over cleverness · small diffs · evidence over assumption · verify with proof (Red→Green for fixes; live API/visual check after deploy) · never mark work done without focused proof.