feat: store normalized Chattanooga routes in Payload - #115
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…te-topology # Conflicts: # docs/guides/chattanooga-gis-import.md # src/components/Map.tsx # src/data/geo_data.ts # src/utils/map.ts
…te-topology # Conflicts: # src/migrations/index.ts
🤖
What this changes
This moves the Casual route catalog into Payload/Postgres and makes the database the source of truth for which routes exist, how they are presented, and where their geometry comes from.
It adds Routes as a first-class collection under Payload's Map content admin section, alongside Trails. Every published Route appears in the Casual tab. A Trail does not appear there automatically; an editor exposes one by creating a Route whose geometry source is Existing trail and selecting that Trail. This keeps Casual as one ordered, editable list without duplicating Trail geometry or making the client merge two collections.
Generated normalized GIS is not committed. Importers normalize verified source data and write the result, measurements, and provenance directly to Postgres.
Route vs. Trail
A Trail is the canonical trail record used by the trail experience: trail complex, rating/type, steward, geometry, and elevation data.
A Route is the curated item shown in Casual mode. It owns the public label, description, styling, direction controls, and stable route ID. Its line can come from one of three explicit sources:
There is no implicit Studio fallback. If a Route is database-backed (imported or trail), its same-named Studio layer is hidden even if the route API is unavailable. A Studio layer is shown only when a published Route explicitly declares Studio as its geometry source.
End-to-end data flow
Imported route
The Route owns its geometry, display bounds/measurements, and import provenance: source path, SHA-256, and source feature count.
Trail-backed route
The Route stores only the relationship. The read path resolves the Trail's current geometry, distance, and bounds, so later Trail edits automatically flow through to Casual. On first save, a blank Route name and route ID derive from the Trail name and slug. Its kind is forced to trail.
Studio-backed route
The database still controls whether the item exists in Casual and supplies its card metadata. The geometry stays in Studio until a verified current source is available. Studio-backed Routes are intentionally omitted from /api/map/routes.
Public app behavior
The server resolves the active city from the request hostname, reads published Routes through the Payload Local API, and passes that list to the client. A valid
?city=bendor?city=chattanoogaquery overrides hostname and environment selection across the map, About, embed, export, and SVG redirect flows; invalid ids fall back to the normal resolution path. The same database-backed list drives:The route API returns a FeatureCollection containing only drawable imported and trail-backed Routes. Runtime code renders those from one shared GeoJSON source and renders Studio-backed Routes from their explicitly configured Studio layers.
This removes the checked-in TypeScript route arrays as the runtime catalog: an absent or unpublished database Route is not silently restored from static metadata.
Admin behavior and validation
Routes are versioned Payload documents with drafts and up to 50 revisions. Publishing enforces source-specific integrity:
The Trail relationship picker is filtered to published Trails in the Route's selected city. Display fields include kind (ride, greenway, path, or trail), description, color, width, opacity, distance, route bounds, arrow controls, and optional reverse-arrow bounds.
Database schema
The schema is introduced by migration 20260919_215743 and expanded by 20260919_224403.
Payload also creates _routes_v, which mirrors the Route fields as version-prefixed columns and links each revision back to routes.id. It includes indexes for parent, route ID, status, timestamps, latest revision, and the city/route-ID pair. The new routes relationship in payload_locked_documents_rels supports Payload document locking.
Several SQL columns remain nullable because Payload must be able to save drafts. The publish hook supplies the conditional invariants that SQL alone cannot express—for example, requiring geometry for imported Routes, a source Trail for trail-backed Routes, and a known layer ID for Studio-backed Routes.
Current city rollout
Bend
The production seed upserts all eight existing Casual routes as imported, using the already committed public/data/bend/bike-network.geojson as its source. The redundant generated public/data/bend/routes.geojson is removed.
The existing Deschutes River Trail Casual Route remains imported for now because its current Casual line covers a different section than the MTB Trail record. An editor can deliberately switch it, or any future Casual entry, to an existing Trail in admin.
Chattanooga
The Riverwalk importer reads the checksum-verified shapefile from the archived PR #67 GIS bundle, transforms it from NAD83 / UTM zone 16N to WGS84, normalizes segment direction/topology in memory, and upserts the result into Payload:
pnpm db:import:chattanooga-routes "/path/to/GIS/Uncompressed files"The normalized output is written directly to the database and is not added to Git. Riverwalk is treated as runtime-owned even before that manual import, so the older same-named Studio line cannot reappear as a fallback.
The other five Chattanooga Casual routes have newer Studio geometry than the archived source files. The production seed therefore creates explicit Studio Route records for their metadata and visibility. It preserves any route an editor has already migrated to imported or trail-backed geometry.
Deploy and operations
The production build pipeline runs, in order:
Vercel preview/development builds skip migrations and seeds because they share the production database. Non-Vercel builds with DATABASE_URL keep the same migrate-then-seed behavior.
Both seeds are idempotent and skip unchanged records. They do not overwrite intentional source migrations. The Chattanooga Riverwalk GIS import remains a separate manual operation because its external source archive is not committed and therefore is not available during deploy.
Validation