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.
Repo map
Section titled “Repo map”| Path | What | Stack |
|---|---|---|
apps/web | Web frontend (v2) — the TripViz map UI | Vite + React + TS + MapLibre + Tailwind + zustand |
apps/mobile | Mobile app (v0) | Flutter |
apps/backend | REST API /v1, Supabase-JWT auth | NestJS |
apps/workers | AI ingest + scraping + geocode + media | Python FastAPI, instructor + LiteLLM |
packages/contracts | Shared TS entity types (web consumes) | TS (path alias @treeper/contracts) |
infra/supabase | Migrations (migrations/NNNN_*.sql) | Supabase CLI |
infra/coolify | DEPLOY.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.
Local dev
Section titled “Local dev”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.
# 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 DEFAULTcd 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)”cd apps/workers && uv run pytest -q # worker suitecd apps/backend && npx tsc --noEmit # NB: piping to `tail` masks the exit code — check `echo $?`cd apps/web && npx tsc --noEmit && npm run buildBug fixes: Red → Green. Worker enrichment code is unit-tested with fakes (no network) because SearXNG/Nominatim aren’t reachable from the sandbox.
Deploy (Coolify)
Section titled “Deploy (Coolify)”Coolify CLI v1.6.2, context personal-coolify (coolify.itssatya.in). Apps track the main branch, so deploy = merge → push → redeploy.
| App | UUID | FQDN |
|---|---|---|
| backend | o14cr95wpi4d3m76grtxhnjx | api-treeper.itssatya.in |
| workers | r12jao9q6nzqxxujalfs0cj3 | workers-treeper.itssatya.in |
| web-app | aol2ofb4kp60jq2vo02jkbki | treeper.itssatya.in |
| supabase | — | supabase-treeper.itssatya.in |
git checkout main && git merge --no-ff <branch> && git push origin maincoolify app deploy <uuid> # returns a deployment UUID; builds are queued/serializedcoolify app env list <uuid> # values are masked; -s shows sensitiveCoolify CLI gotchas (learned the hard way)
Section titled “Coolify CLI gotchas (learned the hard way)”coolify deploy get <uuid>status is STALE/unreliable — it showedqueuedfor already-finisheddeploys. Don’t trust it.- True status leaks from
coolify deploy cancel <uuid>: it errors...Current status: finished|cancelled-by-user|.... (But a realyescancels a running deploy — don’t probe a live one withyes.) coolify deploy listis broken (JSON unmarshal error). Don’t use it.- Reliable readiness signals instead:
- New HTTP route → poll it. FastAPI workers expose
/openapi.jsonpublicly (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).
- New HTTP route → poll it. FastAPI workers expose
- 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 regenerateloadbalancer.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 uses127.0.0.1.
Sandbox / verification constraints
Section titled “Sandbox / verification constraints”- Shell can reach
api-treeper.itssatya.inandworkers-treeper.itssatya.in; cannot reachtreeper.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=passwordwith the anon key + the test creds; tokens expire — re-auth between long steps. - Claude-in-Chrome: a fresh MCP tab reports a
Placeholder WebUIfor ~3s afternavigatebefore the SPA loads — wait, then screenshot.
Enrichment architecture (apps/workers)
Section titled “Enrichment architecture (apps/workers)”The import pipeline parses a source → ItineraryDraft (instructor-forced schema), then runs best-effort, budgeted, non-terminal enrichment passes before persist:
- 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”. - Media (
media/) — per-activity photos (SearXNGimages) + one video link (SearXNGvideos), kind-aware (no media for transport; video only for sight/food/freeform). Stores external URLs (no rehosting). - Kind — the structuring/vision/video prompts classify each activity;
ai/kind_infer.pyis 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–70for hierarchy. Per-trip accents: indigo#6366f1, emerald#10b981, rose#f43f5e(max 3 trips). - Activity kind → icon/color (
lib/activityMeta.tsx): sight=sky#0ea5e9Landmark · food=red#ef4444Utensils · lodging=purple#8b5cf6BedDouble · transport=emerald#10b981Train · freeform=indigo#6366f1Sparkles. - Glass:
backdrop-blur-xl,bg-[#121a33]/80,border-white/10,shadow-2xl,rounded-3xlcards /rounded-2xlrows. - Route line (
map/RouteLayer.tsx): dual-layer glow — glowline-width 9, blur 8, opacity .3+ corewidth 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 towithoutOutliers()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-*).
Core principles
Section titled “Core principles”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.