Status snapshot for a solo dev returning after a break. Synthesized from per-subsystem surveys. Honest, dense, file-cited.
- It's a polished, fully-interactive demo UI sitting on top of a real-but-disconnected, unpopulated backend. Every one of the 16 stx views renders and is clickable, but they all read from a single ~620-line client-side seed store (
resources/components/stores.stx,defineStore('tb')) persisted tolocalStorage(keywildloop-tb-v2) — not from the database. - The backend is NOT all stubs. There are 8 real DB-querying Actions (Auth ×2, Trail ×1, Territory ×5), real Stacks ORM models + 27–29 migrations, a genuine hand-rolled geometry engine (
resources/functions/geo.ts,gpx.ts), and a working OSM scraper command. All routes are wired inroutes/api.ts. The problem is connection and data, not absence. - The core territory-capture loop "works" — but only as a self-contained browser toy.
useRecorder.tsdoes real GPS (navigator.geolocation.watchPosition) + a simulate mode, and "capture" flips ownership in the in-memory store. It uses its OWNpointInPolygon+ a hardcoded "10 GPS pings inside an enemy polygon" rule — it never calls the real backend conquest engine, never persists, no multiplayer. - There are TWO incompatible capture designs that have never met. Backend = run a closed loop to claim empty land, then route-intersection splits enemy land (area-based,
ClaimTerritoryAction+ProcessActivityConquestAction). Client = accumulate 10 proximity pings (Paper.io-style). Wiring them is a game-design reconciliation, not just plumbing. - Only ONE endpoint is consumed by the UI (
GET /api/trailsviauseTrailCatalog.ts), and even that returns[]because every table has 0 rows — so the app falls back to seed data 100% of the time. A fully-written API client (resources/assets/scripts/api.ts, 287 lines, hasclaimTerritory/processConquest/giveKudos/etc.) exists but is dead code with zero importers. - Highest-leverage next move: make the run→activity→territory loop persist server-side end-to-end (populate DB → persist GPS to an Activity → invoke the already-built claim/conquest engine → render territories from
/territories/map). That single thread turns the "browser toy" into a real product and exercises the most-built, least-connected code.
Stack/runtime: Bun >= 1.3.0, SQLite >= 3.47.2. Stacks framework (v0.70.45) is vendored in-repo under storage/framework/** as workspaces (not npm-installed). stx ^0.2.66 (resolves 0.2.70). Maps via ts-maps (NOT leaflet).
First-run / clean checkout (critical):
./bootstrap # downloads pantry from github.com/home-lang/pantry,
# regenerates the GITIGNORED pantry/ dep tree,
# re-vendors bun-query-builder. REQUIRED on fresh checkout.
# (or) ./buddy installpantry/ (333 entries) is gitignored (.gitignore 43/56/61) and will not exist on a clean checkout. pantry/bun-query-builder/ is a vendored copy; root package.json does not declare it (HEAD commit: "bump vendored bun-query-builder to ^0.1.26 (not in root)"). Don't change bunfig.toml linker = 'hoisted' (required by better-dx convention).
Run:
./buddy dev # full Stacks dev
bun --watch serve.ts # stx-only frontend
bun run dev:frontend # same, alt
bun build.ts # build (buildApp from @stacksjs/stx)
bun preview.ts # previewPorts / URL (gotcha): config/ports.ts says frontend 3000 / api 3008 / db 3010, but .env PORT=6700 wins (serve.ts honors process.env.PORT). App is at wildloop.localhost on port 6700 — easy to look in the wrong place.
Database — three configs disagree; only one is real:
| Source | Says | Reality |
|---|---|---|
config/database.ts:14 |
default mysql, sqlite path database/stacks.sqlite |
misleading |
.env |
DB_CONNECTION=postgres, DB_DATABASE='bench_review' (5432) |
misleading (points at a leftover benchmark DB) |
config/query-builder.ts:5-7 |
dialect:'sqlite', database/stacks.sqlite |
THE config the ORM actually uses |
- All 27–29 migrations are SQLite-only DDL (
INTEGER PRIMARY KEY AUTOINCREMENT,TEXT/REAL) — they will NOT run on postgres/mysql as-is. But the alter migrations useADD CONSTRAINT ... FOREIGN KEY, which SQLite rejects — so the migration set is internally contradictory (see Caveats). - Every table is empty (0 rows) in
database/wildloop.sqlite;database/stacks.sqliteonly has framework tables. No seeders exist anywhere. - To get data:
buddy scrape:trails(hits live OSM Overpass — network-dependent) or manual insert.
Maps: public/js/ts-maps.mjs (336KB) + public/css/ts-maps.css are already built/committed, so maps render now. Rebuilding (bun run build:maps) requires the ts-maps sibling repo at ../../Libraries/ts-maps/packages/ts-maps, which is currently MISSING on this machine — build:maps would fail.
Working tree: clean, up to date with origin/main. Recent theme: "feat: add territory capture gameplay and Strava-style run UI" landed on top of a framework upgrade to v0.70.45.
Layout (Stacks conventions):
resources/views/— 16 stx page templates (file-based routing);resources/layouts/default.stx@includes the store + runsuseTrailCatalog.resources/components/— components + thetbstore (stores.stx).resources/composables/—useRecorder,useTrailMap,useTerritoryExplorer,useTrailCatalog,useRoutePreview.resources/functions/—geo.ts,gpx.ts,scraper/*.app/Actions/—Auth/,Trail/,Territory/, and an emptyScraper/(dead scaffolding; scraper actually lives inapp/Commands/ScrapeTrails.ts).app/Models/— 12 ORM models.routes/api.ts— routes (mostly framework boilerplate; wildloop routes at 18-19, 378, 381-392, 395-398).
Data layer. 12 Stacks ORM models with attributes, validation, faker factories, relations: User, Trail, Activity, Territory, TerritoryStats, TerritoryHistory, Kudos, Review (trail_reviews), SavedTrail, Achievement, UserAchievement, UserStats. Each has create + alter migrations and a uuid/timestamps trait. Relationship chain is end-to-end: User → Activity (belongsTo User+Trail) → Trail; capture path Activity → Territory (belongsTo User+Activity) → TerritoryHistory + TerritoryStats.
Geospatial approach — relational/JSON-in-TEXT, NOT PostGIS. Trail tracks in trails.geometry (TEXT), run tracks in activities.gpx_data (TEXT), territories store a GeoJSON Polygon string in territories.polygon_data + a CSV bounding_box (minLat,minLng,maxLat,maxLng) + center_lat/lng, area_size, perimeter (REAL). All spatial math is hand-rolled and genuinely implemented in resources/functions/geo.ts (503 lines: Haversine, Shoelace area with cos(lat) projection, Douglas-Peucker, ray-cast point-in-polygon, segment/route intersection, bbox overlap, centroid, GeoJSON round-trip, splitPolygonByRoute) and gpx.ts (regex GPX/GeoJSON/JSON parsing + validateGpsDataForClaim). No spatial index — conquest loads ALL active territories and filters in JS (O(n)).
How the frontend talks to the backend: mostly it doesn't. There is no resources/stores/ dir — all client state is the tb store. The ONLY live fetch in the whole app is useTrailCatalog.ts:28 → GET /api/trails?limit=500 → tb.hydrateTrailsFromApi (normalized by resources/assets/scripts/trail-data.ts, km→mi / m→ft). Because trails table is empty, the length guard fails and catalogSource stays 'seed'; the "live data" badge in trails.stx has effectively never lit up. Everything else (activities, territories, conquests, leaderboard, social) is seed-only.
The intended run→territory pipeline (built but disconnected):
record.stx → useRecorder (real GPS / simulate)
→ [INTENDED] POST gpx_data → create Activity
→ POST /territories/claim (ClaimTerritoryAction: closed-loop validate, area 1k–5M m², Territory.create + History + Stats)
→ POST /territories/process-conquest (ProcessActivityConquestAction: bbox prefilter → routeIntersectsPolygon → splitPolygonByRoute → child Territory + history + dual stats)
→ GET /territories/map | /leaderboard | /user/{id} → render
What actually happens today: useRecorder.stop() calls in-memory tb.addActivity() + tb.conquerTerritory() and never POSTs anything. The real engine has never processed a single run (and structurally couldn't: Activity.gpxData factory returns null, and both claim/conquest Actions hard-fail with "Activity has no GPS data").
Infra / build
- Stack pinned and runnable; framework vendored under
storage/framework/**;buddyCLI wrapper with rpx-resolution fix. - Maps prebuilt and committed (
public/js/ts-maps.mjs+.css); real ts-maps + OSM tile integration inuseTrailMap.ts(createTrailMap/drawTrailRoute/drawTerritoryPolygon/drawTrailMarker/createLiveRouteLine), used by record/trail/activity/trails pages.
Data / schema
- 12 ORM models fully defined (attributes, validation, factories, relations,
useApi,useSeedercounts). All 12 tables migrated and present (sqlite3 .tablesconfirms). - Geospatial schema designed;
geo.ts+gpx.tsmath implemented (real Haversine/Shoelace/Douglas-Peucker/point-in-polygon/route-intersection/polygon-split/GeoJSON).
API / backend
- All 8 Actions are REAL DB queries (no TODO/mock markers in
app/):Auth/LoginAction.ts(Auth.login → {token,user}),Auth/RegisterAction.ts(register + getUserFromToken).Trail/TrailIndexAction.ts—Trail.limit().get(), lat/lng mapping.Territory/ClaimTerritoryAction.ts— closed-loop claim, area bounds, Territory + History + Stats.Territory/ProcessActivityConquestAction.ts(~230 lines) — full polygon-split conquest, child territory, dual stat updates.Territory/{GetTerritoriesForMap,TerritoryLeaderboard,UserTerritories}Action.ts— real.where/.orderBy/.getqueries, GeoJSON FeatureCollection.
- All routes registered:
routes/api.ts18-19 (login/register), 378 (GET /trails), 381-392 (/territories/{claim,process-conquest,map,leaderboard,user/{id}}), 395-398 (/me,/logout).
Run / trail UI
- Real GPS capture (
useRecorder.tswatchPosition, haversine, high-accuracy, permission states). - record→save→view-activity loop works client-side end-to-end (
stop()→tb.addActivity()→feed/index/activity/[id]render it). - Trail + activity detail render real route geometry from seed
trailRoutes; feed SVG route-preview thumbnails (useRoutePreview.ts). GET /api/trailswired (the one live path) with graceful[]fallback.- OSM scraper is real and substantial:
app/Commands/ScrapeTrails.ts(buddy scrape:trails) +resources/functions/scraper/*(overpass-client, region-definitions, trail-normalizer ~235 lines, deduplication; ~511 lines total), writes viaTrail.create().
Territory UI
- All game surfaces render and are interactive:
territories,territory/[id],conquests,battles,challenges,leaderboard,stats— working filters/tabs, Leaflet maps (green=yours/orange=enemy/dashed=contested), conquest-history timelines. - In-browser capture mechanic visually functional:
applyCaptureSamplefills a meter,conquerTerritoryflips ownership + bumps stats/achievements/notifications; simulate mode replays seed routes.
Social / Auth
- Login/Register are the ONLY real DB-backed social flows: forms wired to
auth.login()/auth.register()with loading/error states, live password-strength + match logic. - Profile, notifications (filter/group/mark-read), client-side leaderboard aggregation all render correctly (over seed data).
Gameplay engine
- Backend geo engine is real and complete enough to claim + conquest-split (see Data/API above). Routes reachable. Models' field names match the Actions (
polygonData/boundingBox/parentTerritoryId/conquestCount;eventType/previousOwnershipDuration/newTerritoryId).
| Surface | What renders | The precise gap |
|---|---|---|
| Whole app data layer | 16 views via useStore('tb') |
Reads ~12 hardcoded seed arrays in stores.stx (lines ~209-417), persisted to localStorage. Only trails are API-replaceable. |
| Trails "live data" | trails.stx "live data" badge |
Trails table empty → /api/trails returns [] → guard fails → always seed. Live path has never fired. |
Territory map / detail (territories.stx, territory/[id].stx) |
Maps, nearby grid, leaders sidebar | useTerritoryExplorer.ts reads ONLY tb.territories()/users(); never calls /territories/map or /leaderboard (recomputes leaderboard from seed at L36-47). CTAs link to /record, not the claim flow. |
| In-game capture loop | record.stx capture meters, ownership flip | useRecorder.checkConquest (L165) uses its OWN pointInPolygon (L81, not geo.ts) + hardcoded CAPTURE_SAMPLES_NEEDED=10. No closed-loop, no area, no split — a proximity counter, mutating localStorage only. |
| conquests / battles | Battle log, "Under Attack", "Live" badges | All seed rows (seedConquests); "live" = status==='active'; period filters slice seed timestamps client-side. No websocket/poll/API. |
Leaderboard (leaderboard.stx) |
Podium + ranked list, period/metric tabs | Computed from seed; tabs are decorative — always sorts by b.totalDistance (L30) regardless of selection. Backend TerritoryLeaderboardAction unused; page doesn't even rank by territory. |
| stats.stx | Summary tiles, weekly bar chart, PRs | All from seedUserStats (single hardcoded object for user 1); weeklyHistory fixed 7-point array. No backend query. |
| Activity kudos / comments | activity/[id].stx toggle/submit work |
tb.toggleKudos/tb.addComment mutate the local array only; no POST, no Kudos/Comment endpoint. Feed kudos buttons (feed.stx:103-110) have NO @click — decoration. |
| Trail reviews | trail/[id].stx Reviews/Conditions tabs |
Display-only from seedReviews; no submission form, no review API. |
| Auth session / logout | onMount auth.user(); /me, /logout routes declared |
AuthUserAction/LogoutAction files don't exist → calls throw, swallowed by empty catch{}; redirect silently no-ops. No logout button anywhere. |
| Frontend API client | resources/assets/scripts/api.ts (287 lines) |
Full client (claimTerritory/processConquest/giveKudos/createActivity/fetchUserStats…) but zero importers — dead code; only attaches window.WildLoopAPI. |
splitPolygonByRoute |
conquest geometry | Self-labeled "simplified - for MVP" (geo.ts L378); boundary-walk math handles only clean 2-crossing convex case; >2 crossings / concave / self-intersecting routes produce wrong/degenerate polygons. Never run on real data. |
| Activity save fidelity | useRecorder.stop() payload |
splits:[], heartRateAvg/Max:null, cadence:null; no persisted link to triggered conquests (conqueredIds only used for the title string); real-GPS elevation stays 0 (sim fabricates via Math.random()*12). |
Backend endpoints that don't exist at all (models/tables exist, but no Action and no route):
- Activity / runs — no create/list/show. The recorder cannot persist a run.
activity/[id].stxrenders from seed. - Kudos — model + table exist; zero routes. Feed counts come from
seedActivities.kudos_count. - Reviews — model +
trail_reviewsexist; zero wildloop review routes (the/reviewsin api.ts are unrelated Commerce boilerplate). - Achievements / UserAchievements — models + tables; zero routes. No unlock engine; progress only changes as a side effect of
conquerTerritory. - UserStats public read — model + table; no endpoint (stats page uses seed).
- User-facing Leaderboard, Feed — views exist, demo-only.
Features with NO model, NO table, AND NO endpoint (pure frontend fictions):
- Clubs —
clubs.stxCreate/Join buttons have no handlers, no backend. - Challenges — full UI,
+ New Challengeis a no-op; no Challenge model/migration/Action. - Battles — seed rows filtered by status/date; no real-time.
- Segments, Notifications (as a backend), Follows/Friends — seed-only; no social-graph backend (faked via seed
membersarrays). - Social OAuth (Google/GitHub) — buttons render with brand SVGs, no
@click, no provider config. - Forgot-password / Terms / Privacy pages — linked but views don't exist (password-reset API routes exist at api.ts 101-105, no frontend).
Gameplay-engine gaps:
- Any frontend→backend wiring for the game. No view/composable calls a territory endpoint; the dead
api.tsis the only bridge. - GPS ever reaching the backend.
Activity.gpxDatafactory returnsnull;useRecorder.stop()never persistsgpx_data. Both claim/conquest Actions hard-fail without it. - Seeded/live game data. DB empty; no Database Seeder for any model; seeding helpers
generateSampleLoopGpx/generateLoopCoordinateshave zero callers. - Contest / defend path.
TerritoryHistoryhas a'defended'event type that nothing ever writes; "contested"/defendCountexist only in the seed store. - Scraper HTTP Action —
app/Actions/Scraper/is an empty dir (CLI-only ingestion). - Ranking job —
weeklyRank/allTimeRankare hardcoded to999on create (ClaimTerritoryAction.ts:117-118,ProcessActivityConquestAction.ts:211-212); no cron/recompute.
Environment / build
- DB config trap: trust
config/query-builder.ts(sqlite,stacks.sqlite), NOT.env(postgres/bench_review) orconfig/database.ts(mysql). The active DB file is even ambiguous:query-builder.tspoints atstacks.sqlite(framework tables only) while the schema-bearingwildloop.sqlite(90KB) is also present. - Frontend port is 6700, not 3000.
- Clean checkout needs
./bootstrapto regenerate gitignoredpantry/+ re-vendorbun-query-builder.bun.lockstill pins^0.1.21while vendored is^0.1.26. ts-mapssibling is missing →build:mapsfails; works only because output is committed. Maps silently fail (caught/logged) if the public chunks aren't served.
Migrations (will break a from-scratch migrate)
- Duplicate geometry column:
1780615824-add-trails-geometry-column.sqlAND1780619054-alter-trails-table.sqlbothALTER TABLE trails ADD COLUMN geometry TEXT→ "duplicate column name: geometry". (The 1780615824 file even contains it twice.) One must be removed/guarded. - 17 SQLite-invalid
ADD CONSTRAINTstatements across the1780615*alters — valid only on Postgres/MySQL. So the migration set is dialect-locked to Postgres/MySQL, yet the runtime is SQLite and the create migrations are SQLite syntax — internally contradictory. - Committed
.sqlitefiles are stale:wildloop.sqliteschema for trails/activities/territories LACKS the uuid/FK/geometry columns the models expect (only the create migrations ran; alters never applied).stacks.sqlitehas 0 tables. Don't trust either as source of truth.
Data modeling
- Type sloppiness: FK-ish ids and counts (
giver_id,parent_territory_id,review_count,kudos_count…) stored as REAL not INTEGER; "SQLite doesn't support ALTER COLUMN" so never corrected. - Denormalized counters drift:
activities.kudos_count,trails.rating/review_countare only set by factories; no code recomputes from related rows. No unique constraint preventing duplicate kudos. - One-directional relations:
Territory/Kudos/TerritoryHistorybelongsTo Activity, butActivitydeclares no inversehasMany. - No spatial index / PostGIS — conquest is O(n) full scan with per-territory string bbox parse; fine for demo, won't scale.
Security / correctness
- No auth on
/territories/*(only/me,/logoutguarded).ClaimTerritory/ProcessConquestreaduser_idfrom the request body — any caller could claim/conquer as any user once data exists. - Two divergent capture rule-sets (closed-loop+split vs. 10-ping proximity) that will never agree — reconcile the game design before wiring.
- Frontend/backend models diverged: Territory status enum is
['active','contested'](model) vs.'secure'|'contested'(store); backend haspolygon_data/bounding_box, store haslat/lng+ a separateterritoryPolygonsmap. Wiring needs a mapping layer.
UI / dead code
- Two parallel component trees (
resources/components/andresources/views/components/) with near-identical, drifting copies of ActivityCard/TrailCard/FeaturedTrailCard/DifficultyBadge/StarRating/Territory* — used by NO surveyed view (pages inline their markup). Several have broken stx interpolation (TrailCard.stx:59,FeaturedTrailCard.stx:55,DifficultyBadge.stx:23,InputGroup.stx:28) — latent, masked only because unused. - localStorage persistence masks the stub: key
wildloop-tb-v2— captures/kudos survive reloads (looks "real"), and seed edits won't appear until the key is cleared or bumped (thev2suffix is the manual-bump mechanism). - Fragile route-param extraction:
activity/[id].stxreadswindow.__routeParams?.id,trail/[id].stxreadswindow.stx._rp?.id ?? window.__stx_rp?.id— inconsistent undocumented globals; if the router changes, detail pages silently fall back to id 1/0. - Two divergent API clients: the unused
scripts/api.tsis the more complete one (conquest/kudos/stats); the useduseTrailCatalogis raw fetch. Standardize before extending.
The whole point of the app (territory capture) currently has no server backing. Everything below is needed to turn the browser toy into a real product. Tackle as one vertical thread.
-
Get the migrations to actually run + seed data. (scope: ~½ day)
- Fix the duplicate geometry migration (remove/guard
1780619054-alter-trails-table.sql; de-dupe the statement in1780615824). - Decide the dialect once. Easiest path: stay SQLite (matches
config/query-builder.ts+ create migrations) and rewrite the 17ADD CONSTRAINTalters as SQLite-compatible (recreate-table or drop the FK constraints, keep the columns). Then point at one DB file consistently. - Run
buddy scrape:trails(or write a minimaldatabase/seeders/for trails/users/territories so you're not network-dependent). - Touches:
database/migrations/*,config/query-builder.ts, newdatabase/seeders/,app/Commands/ScrapeTrails.ts. - Done when:
GET /api/trailsreturns rows andtrails.stxflips tocatalogSource='api'("live data" badge lights).
- Fix the duplicate geometry migration (remove/guard
-
Reconcile the two capture designs, then persist a run as an Activity. (scope: ~1 day)
- Pick ONE capture rule (recommend the backend's closed-loop-claim + route-intersection-split — it's the differentiator and already built). Make
useRecorderproduce a realgpx_datapayload. - Add the missing Activity endpoints:
ActivityStoreAction(+ index/show). WireuseRecorder.stop()to POST the GPS track → create an Activity row (fixActivity.gpxDataso it's persisted, notnull). - Touches: new
app/Actions/Activity/*,routes/api.ts,app/Models/Activity.ts,useRecorder.ts,resources/assets/scripts/api.ts(revivecreateActivity).
- Pick ONE capture rule (recommend the backend's closed-loop-claim + route-intersection-split — it's the differentiator and already built). Make
-
Wire the run → claim/conquest engine. (scope: ~1 day)
- From the saved Activity, call
POST /territories/claimandPOST /territories/process-conquest(the engine is already built inClaimTerritoryAction/ProcessActivityConquestAction). Revive the deadclaimTerritory/processConquestinscripts/api.tsand actually import them. - Add
authmiddleware to/territories/*and deriveuser_idfrom the session instead of the request body. - Touches:
useRecorder.ts,resources/assets/scripts/api.ts,routes/api.ts:381-392, the two Territory Actions. - Caveat to expect:
splitPolygonByRouteis MVP-quality — this is the first time it runs on real input; budget time to harden the >2-crossing/concave cases.
- From the saved Activity, call
-
Render territories + leaderboard from the DB. (scope: ~½ day)
- Replace
tb.territories()/territoryPolygons()reads inuseTerritoryExplorer.tswithGET /territories/map; replace the seed leaderboard withGET /territories/leaderboard. Add a mapping layer for the model↔store shape divergence (status enum, polygon_data vs lat/lng). - Touches:
useTerritoryExplorer.ts,territories.stx,territory/[id].stx,leaderboard.stx,stores.stx(hydration methods).
- Replace
Net P0 outcome: a single player can record a run, claim/conquer territory that persists to the DB, and see it on a map fed by the API — the real loop, end to end.
- Fix the broken Auth wiring: create
AuthUserAction+LogoutAction(routes already declared at api.ts 396-397); add a logout control tonav.stx; gate nav/pages on auth; feed the logged-in user into the store instead of hardwiredcurrentUserId:1. - Persist social interactions: Kudos and Review/Comment endpoints + wire
tb.toggleKudos/addCommentand the feed kudos buttons (feed.stx:103-110) to them. Add a unique constraint on kudos; recompute denormalized counters. - Fix the leaderboard sort bug (
leaderboard.stx:30ignores period/metric tabs) and make the ranking job populateweeklyRank/allTimeRankinstead of the hardcoded999. - UserStats / Achievements endpoints + a real unlock engine driven by activities/conquests (today progress only moves via
conquerTerritory).
- Spatial indexing (or at minimum a real bbox index) — conquest currently full-scans all active territories.
- Decide on Clubs / Challenges / Battles / Segments / Follows — these are pure UI with zero backend; either build models+endpoints or remove the dead UI.
- Real-time / multiplayer for battles/"under attack" (today entirely scripted seed).
- Tech-debt cleanup: delete one of the two component trees (fix the interpolation bugs or drop the files); standardize on ONE API client; bump the
tbstore persist key when the schema changes; unify route-param extraction across detail pages; correct REAL→INTEGER column types; remove the emptyapp/Actions/Scraper/.
- Confirm reality in 5 minutes:
sqlite3 database/stacks.sqlite ".tables"and… "SELECT COUNT(*) FROM trails"(expect 0), then loadwildloop.localhost:6700and note every page is seed. - Fix the duplicate geometry migration (
1780615824/1780619054) and make migrations run clean on SQLite (neutralize theADD CONSTRAINTalters). - Get ONE trail into the DB — run
buddy scrape:trails(or hand-insert a row) and watchGET /api/trailsreturn it andtrails.stxflip to the "live data" badge. That single green light proves the migration/seed/ORM/route/fetch chain is healthy and is the foundation every P0 step builds on.