Cartos — Comprehensive Guide

Sovereign geospatial vertical-AI platform. Last updated 2026-08-01 — two updates, most recent first. Map legibility + the GFM arc's close. (1) Catalog layers now render risk as risk — any layer whose features carry risk_score + risk_tier is automatically drawn as severity (tier colour, tier-graduated size, pulsing target rings, optional name labels), and every feature popup is rebuilt from a 20-row key: value dump into a shaped card with a risk hero band, score-driver bars and grouped, unit-formatted attributes. Both are auto-detected from the data, not configured per layer, so Guardian's 50 strike targets and its 2 500 collateral-risk POIs are one code path and any future layer adopting the same field names inherits it. ⚠ The work uncovered a silent renderer bug — a label layer without an explicit text-font falls back to a font OpenFreeMap does not serve, drawing nothing at all with no error — found only by executing the real layers through a headless MapLibre harness after tsc and vite build were both already clean. Live on prod (d4805bc); see §9.12. (2) The GFM arc completed 2026-07-30 → 08-01 across nine PRs (#136–#148) for under $2 000 of model spend: a frozen open encoder + embedding index + verified operators free multispectral fusion measured at +0.067 accuracy and stored beside the vector rather than concatenated into it, dataset supersession so a re-embed hides its predecessor instead of double-serving it, and a cost gate that is live but deliberately inert (§9.11). ⚠ Stage 1 was measured and declined — the $15–35k continued-pretraining step had a documented falsifier that nobody had ever tested, so the frozen baseline was measured on CPU for nothing; it fired, and the money was not spent. Those sections were written at the time; this changelog entry is the header catching up to them.

Earlier: 2026-07-29 — five updates, most recent first. Security + operability pass, all shipped to main and deployed to prod (verified). (1) Migration 0087 closes an RLS gap on the three Cartos Harvest tablescrop_calendars, field_parcels and farmer_registry each described themselves as tenant-scoped but carried no row-level-security policy, leaving only the app-layer WHERE filter as the boundary; farmer_registry holds PII (name / national_id / phone). The fix pairs ENABLE + FORCE + tenant_team_isolation with a cartos-data/src/harvest.rs change so all nine set_tenant_session call sites forward the caller's real groups instead of &[] — ⚠ the two halves must ship together (see §7). Verified on prod: relrowsecurity and relforcerowsecurity true on all three, zero RLS-violation lines in the API log. (2) The Africa cadastre ingest is now a saved, credentialed, re-syncable connector in Admin → Integrations (migration 0088, connectors/parcel_fabric.rs on the existing RealConnector seam) — credentials stay server-side and the idempotent-upsert + human-review-preservation guarantees hold across re-syncs, turning a one-shot demo call into an operational feed (see §8.4). (3) Admin console usability — module/feature filters on the Modules and Features tabs, and a Test button with a coloured status badge on Catalog and Integrations (see §7.4). (4) Build-pipeline hygiene — the CI gate no longer duplicates itself on every push to main, no longer rebuilds on documentation- or website-only commits, and the mobile iOS build is now run on demand rather than on every mobile push. Earlier today: Cartos Africa's africa-parcel module is wired to a real cadastral store — merged to main and deployed + verified on prod (migration 0086 applied; live-ingest tested end-to-end). africa-parcel went from a surface-only stub to a source-agnostic, LADM-aligned (ISO 19152) parcel + party store (africa_parcels + africa_parcel_parties) that two ArcGIS ingest adapters map INTO, so the same review/scope/export path serves either source: parcel_fabric maps Esri's documented Parcel Fabric information model (Name/StatedArea/ CreatedByRecord/RetiredByRecord/IsSeed/GlobalID — ready to point at a real ArcGIS Enterprise fabric, though no public one exists to demo against), and featureserver is a config-driven field map for any generic ArcGIS parcel/ cadastre service — this is the adapter behind the live demo, verified end-to-end on prod against a real public Esri Tax_Parcels service (8 parcels ingested → summary → LADM export, then cleaned up). New ParcelFabricPanel (admin ingest form, load-in-view, colour by tenure/lifecycle, click-to-review, LADM GeoJSON export) and five routes under /api/africa/parcels/*. This is the ingest half of the module's parcel fabric — an existing authoritative cadastre flowing into the LADM store; the SAM2 segmentation pipeline that generates parcel candidates from imagery (prompts/map-africa/ 07-parcel-fabric-development.md) is still the other, not-yet-built half. See the new subsection in §8.4. Earlier: 2026-07-28 — three updates, most recent first. Cartos Harvest is merged to main (tag cartos-harvest-v1) — the ninth edition is no longer branch-only; every reference to "branch harvest, not yet merged" below is now historical (and as of 2026-07-29 Harvest is deployed and live at https://harvest.cartosatlas.com, with its own Caddy vhost and Keycloak redirect origin like every other edition — see §8.7). The same merge ships persisted derived products (M03) for Harvest: any Crop Type / Health / Yield / Water / Risk panel run can tick "Save to products" to persist its result as a lineage-tracked derived_dataset row (reuses migration 0013 — no new migration), browsable in a new Saved Products panel (SavedProductsPanel.tsx) and queryable by a new harvest_products agent tool (GET /api/harvest/products + /products/:id); and the farmer registry (migration 0085) landed, completing the harv-boundaries module with a FarmerRegistryPanel.tsx + two more agent tools (parcel_fabric_summary, farmer_lookup). A ride-along platform fix in the same merge: the map agent's Anthropic response parser now round-trips Opus 5 thinking/redacted_thinking content blocks verbatim (llm.rs's ContentBlock enum), fixing intermittent 502s on reasoning turns — engine-wide, not Harvest-specific. Earlier the same day: two more updates. First, the ownership/licensing framing was reworked per the Owner's direction: Cartos — the engine and every edition — is the sole and exclusive property of Steven P. Santovasi (the Owner); Solution Strategy Partners (SSP) holds a single unrestricted, perpetual, irrevocable, worldwide, royalty-free, sublicensable master license to all of Cartos; downstream edition instances (Cartos Atlas, Cartos Grid, …) are non-exclusive sublicenses under SSP's master license, not direct grants from the Owner. See the reworked table in §1 and EarthWatch3D-PRD/PRD.md §20 for the full licensing language. Second, §8.7 was added documenting Cartos HarvestSTAC-native crop intelligence (crop type, health, yield, water/drought, field boundaries, agri-risk) for ministries of agriculture, agri-insurers/lenders, and extension services, anchored on Kenya + Nigeria and reusing Cartos Africa's country-scope and foundation-layer fabric. Earlier: 2026-07-26 — Cartos Sentry, the reactive crisis-monitoring edition, is merged to main and LIVE at https://sentry.cartosatlas.com — the eighth live Cartos edition (alongside the newly-documented seventh, Cartos Guardian, live at https://guardian.cartosatlas.com since 2026-07-22). A new §8.6 covers Sentry in full, and a new §8.5 closes the gap and documents Guardian for the first time. Cartos Guardian is the proactive counterpart — UAE critical-infrastructure protection, Strait-of-Hormuz-focused, with a transparent per-site strike-risk model (0-100 score from criticality + Hormuz proximity + category weight) ranking 103 curated sites, a POI-collateral layer, grid/strategic-vector overlays, and live AIS/ADS-B traffic — 5 modules (guard-command/guard-energy/ guard-utilities/guard-emergency/guard-defense), theme Red. Cartos Sentry is the reactive counterpart — optical + SAR + thermal satellite change detection for crisis monitoring: watch Guardian's highest-risk assets → autonomously detect an event → open a scored incident with cross-modality confidence + before/after evidence → report + alert — 3 modules (sentry-command/sentry-detect/sentry-incidents), theme Camo. The Guardian → Sentry handshake is the moat: strike-risk score seeds Sentry's monitoring-watchlist priority (P1/P2/P3), which sizes the commercial-tasking spend a P1 incident can defensibly justify. Sentry ships the platform's first background ingestion loop (sentry_monitor.rs — an opt-in tokio interval daemon that autonomously screens the watchlist, resolves before/after epochs via STAC, runs real model-free detectors, fuses cross-modality confidence, and raises + dispatches incident alerts with dedup, all with no fabricated certainty — a hard "mock guard" means a degraded model sidecar never raises a fake incident), the platform's first incident state machine + outbound alert dispatch, and the platform's first commercial-data-tasking connectors (cost-visible Umbra/Capella/ICEYE/Maxar/Planet vendor cards + a live Umbra open-archive search, for the crisis-hour reactivity open imagery can't deliver). Both editions are author-owned reference instances, not yet licensed externally, live on their own sovereign subdomains exactly like every other edition (hostname-detected, dedicated Caddy vhost + Keycloak redirect origin). Earlier: 2026-07-18 — Cartos Africa (the One Africa edition) is merged to main and LIVE at https://africa.cartosatlas.com — the sixth live Cartos edition, standing alongside Atlas/Grid/Tower/Milepost/Blueprint on its own sovereign subdomain (dedicated Caddy vhost + Let's Encrypt cert + Keycloak redirect origin; hostname → edition detection selects it automatically, same as every other edition — reachable directly, not only via ?edition=africa). A new §8.4 covers it in full: a continental-scale Government edition for African governments, national mapping agencies (NMAs), and ministries — an operational layer over a free/open national-basemap substrate — 9 modules (africa-command + eight use-case departments: Food Sustainability & Agriculture, Health & Immunization, Border Management, Port Management, Land Use & Land Cover, Parcel Fabric Development, Flood & Climate Risk, Settlement Management) on the shared IndustryModule surface (cartos-web/src/modules/africa/, edition preset africa, default theme Desert); a country-scope system (lib/country-scope.tsxCountryScopeContext, an iso3/adm1 filter that reaches map layers, KPIs, dashboards, and agent answers; CountryScopePanel HUD chip with AU-region quick-groups — North/West/Central/East/Southern — plus a sub-national ADM1 drill-down once exactly one country is selected; geoBoundaries ADM0 — all 54 AU member states — and a priority-country ADM1 set, 147 units across Kenya/Nigeria/Egypt/Ghana/Ethiopia/South Africa); a foundation-layer fabric (FoundationLayersPanel + a typed lib/africa-data-catalog.ts registry of continental imagery/terrain/land-cover/ boundary sources grouped by category — keyless/open entries render live, premium/derived entries list as license-badged reference rows; normative license posture: geoBoundaries not GADM, GLO-30 DSM not FABDEM, Esri imagery view-only, commercial sources never client-keyed); the Continental Command dashboard (features/africa/AfricaCommandDashboard — initiative KPIs, a country- onboarding board, cross-module roll-up charts, honours the current scope); real data loaded into all 8 use-case modules via OpenStreetMap, Natural Earth (public domain), and curated anchors, with truthful caveats — africa-parcel is a built-up-extent reference, not legal parcels, and the flood module's raster/ COG/SAR layers are license-flagged references pending a COG render path (real counts + full caveats in §8.4); and the new africa_scope agent tool (resolve country names / ISO3 codes / an AU region / whole-continent → the canonical scope_iso3 list) plus a countries filter added to query_catalog_layer (§9.6) so "ask the map" answers stay inside the selected scope. Real data loaded in prod across all 9 modules — 7,379 health facilities · 3,011 food/market points · 1,928 built-up-extent parcel-reference polygons · 1,285 settlements · 435 ports · 406 land-cover polygons · 124 flood/hydrology features · 14 border crossings · 201 command boundaries (54 ADM0 + 147 ADM1) — with symbology applied (see the real-counts table in §8.4). Engine-level ride-alongs shipped the same day (apply platform-wide, not just to Africa): the "Satellite · Esri" basemap is now framed as a global high-res imagery option in every module's basemap pulldown, relabelled from a UAE-specific name (§9.4); the agent panel and the External Data Sources drawer now scope to the actual current module (agentFocusFromPath / moduleIdFromPath in shell/AppShell.tsx) instead of always resolving to Smart City on Grid/industry/Africa routes (§7); and every floating popup panel now follows the active theme accent — a 27-file CSS sweep plus two new --accent-rgb/--bg-rgb custom properties so panel borders, glows, resize chips, and glass backgrounds recolour with the platform theme instead of hardcoding holographic cyan (§9.4). Also shipped 2026-07-18, after the merge (engine-wide, every edition): the header edition switcher is now a pulldown (shell/EditionSwitcher.tsx — a single accent-dot + name + caret trigger opening a menu, click-outside/Escape to close) instead of a horizontal pill row, freeing header width so the module-nav tabs fit on one row; Cartos Africa is in the list. Catalog points now render a holographic 3-layer symbology stack — a blurred glow halo in the feature's data-driven color, a bright core "orb" with a rim-light stroke, and an animated sonar-pulse selection ring — instead of a flat circle (lib/catalog-layers.ts, opt out per layer via style.point.halo/pulse), plus feature-type icons drawn on top from zoom 9 — medical cross, anchor, cart, tree, gate, droplet, bolt, graduation cap — auto-classified from each feature's own attributes (amenity/harbour/ natural/boundary/power/…) with zero per-layer config (lib/catalog-icons.ts; migration 0061 gives Africa's border crossings an explicit gate icon). And session vectors now always draw above rasters: a new raiseVectorsToTop re-raises every cat-* vector layer above whatever raster was just added — foundation, satellite, GIBS, STAC, ArcGIS, or a basemap swap — via a self-terminating styledata listener, and the Compare tool's non-"current" panes now copy the session's vectors on top of the compared raster too, matching the main map (§9.6). Earlier: 2026-07-13 (a new §9.10 covering Mapillary crowd-sourced street-level imagery, shipped to main today: a new portable capability feature mapillary (crates/cartos-core/src/catalog.rs FEATURES, seventeenth entry) attachable to any module like Overture. The web overlay (features/mapillary/MapillaryOverlay.tsx, lib/mapillary.ts) pulls Mapillary's public mly1_public MVT vector tileset straight into a MapLibre vector source — no server of ours — as two layers: drive-path mapillary-sequences and mapillary-images photo points (zoom ≥ 14); clicking a photo point resolves it through the Mapillary Graph API (graph.mapillary.com/{id}) and shows the dated street-level image + compass heading in a floating popup, with attribution and an "Open in Mapillary" link. Panel controls: coverage on/off, opacity, a "captured since" year slider, and a 360°-panoramas-only toggle, all persisted per panel; a new camera-icon mapillary kind in shell/PanelDock.tsx. Default-on in the native trio (Smart City, Emergency Services, Maritime) and every industry-edition department module (Cartos Tower/Milepost/Blueprint via IndustryModule) — not Cartos Grid, where it's an admin add-on like any cross-pollinated feature. A new cross-module agent tool mapillary_street_imagery (crates/cartos-agent/src/builtin/mapillary_street_imagery.rs, register_mapillary) searches the Graph API by bbox or point+radius for "is there street imagery of this intersection?"-style questions, with pano_only/since_year/limit filters, returning configured: false gracefully when no server-side token is set. External, community-contributed data — © Mapillary contributors, CC-BY-SA — a reference overlay, not sovereign ground truth (that's Cartos Field); labelled as such in the UI. Needs a free client token (VITE_MAPILLARY_TOKEN, web) and a server token (MAPILLARY_TOKEN, agent tool) — the panel shows a setup notice without one. New backfill migration 0044_enable_mapillary.sql (deploy needs a cartos-api rebuild). Cross-referenced from the feature-registry table in §7.4, the agent tool-registry list in §7, the data-sources table in §10, and the roadmap in §13.7. Earlier: 2026-07-11 — a new §9.9 covering the map-tools UI + navigation suite shipped to main, live across every native/Grid/industry-edition module: a shared "widget kit" holographic HUD design language (features/widgets/widget-kit.css, wk-* classes, theme-token-driven via color-mix() so a platform theme recolours every map-tool panel) restyling Measure/Time of Day and styling the two new tools; a new Draw & Annotate capability feature (draw, features/draw/DrawOverlay.tsx, migration 0043) — Select/Point/Line/Polygon/Rectangle with per-feature stroke/fill/width + optional label, a feature list, Clear all, Copy/Download GeoJSON, localStorage persistence, default-on in Smart City/Maritime/Emergency Services; and a new always-on Navigate compass rail (features/navigate/NavWidget.tsx, replaces MapLibre's default zoom/compass/geolocate/fullscreen controls in every native/Grid/industry module) — a collapsible, drag + resizable HUD with a live compass rose bound to bearing/pitch/zoom (click-to-reset-north), a pitch slider, geolocate, fullscreen, a globe/mercator toggle, a multi-CRS coordinate readout (lib/coords.ts, new proj4+mgrs deps: WGS84 dec/DMS, Web Mercator, UTM, MGRS, 21-zone CONUS State Plane — the map itself still only renders Mercator/Globe, MapLibre's limit, so CRS drives the readout not the projection) for both map-centre and cursor, and a toggleable UTM/MGRS grid overlay (lib/grid-overlay.ts, Off·UTM·MGRS) with zone/GZD boundaries, 100 km squares, zoom-adaptive spacing, vertical easting labels, and colour/line-style/thickness controls; lib/nav-controls.ts now only adds the metric+imperial scale bars. Every floating map surface is now uniformly drag + resizableDockablePanel (already was), the new Navigate rail, CatalogLegend (now closable), and every showFloatingPopup window including the ArcGIS identify popup (the last native maplibregl.Popup converted). Cross-referenced from the feature-registry table in §7.4 (now sixteen features) and the roadmap in §13.7. ⚠ Ops gotcha (already fixed, noted for the next one): a new module FeatureDef needs a tenant_features backfill migration (mig 0042 for the map-tools set, 0043 for draw) + an API restart before it surfaces for tenants seeded before the feature existed — load_entitlements reads the DB enable-state rows, not the compile-time catalog defaults. Earlier: 2026-07-10 — Time of Day (features/time-of-day/TimeOfDayOverlay.tsx, lib/time-of-day.ts), a dependency-free NOAA/SunCalc-style solar-position engine that drives setLight 3D building shading, hillshade illumination direction/colour, and setSky day/twilight/night gradients from a solar-time + date slider (sunrise/golden/noon/sunset presets, Play + speed, a hillshade-contrast Shadows approximation — real deck.gl cast shadows are a noted TODO); and Measure (features/measure/MeasureOverlay.tsx, extended lib/measure.ts), the measure tool upgraded to a 3D-aware Distance/Area/Height panel with map.queryTerrainElevation-derived slope length, a per-segment 3D/horizontal/Δh/bearing table, and an m/ft toggle, degrading gracefully to 2D with no terrain — both are PanelDock rail items, default-on in Smart City/Maritime/Emergency Services via crates/cartos-core/src/catalog.rs (no new migration needed), and cross-referenced from the feature-registry table in §7.4 and the roadmap in §13.7. Earlier: a dedicated §8.3 write-up of the three Cartos industry editionsCartos Tower (Airport), Cartos Milepost (Transportation/DOT), and Cartos Blueprint (Infrastructure PM/AEC), built 2026-07-07 and now live on their own subdomains (tower./milepost./blueprint.cartosatlas.com, verified against deploy/Caddyfile.prod) — closing the gap flagged in the 2026-07-09 refresh (this guide covered the catalog/dashboard work over these editions but never described the editions themselves). Covers the 29-module lineup read straight from cartos-web/src/modules/industry/modules.ts (Tower 10 / Milepost 10 incl. an Executive Overview portfolio surface / Blueprint 9 incl. a Portfolio View), the shared config-driven IndustryModule surface + IndustryDashboard, the edition.ts hostname-detection preset system extended to tower/milepost/blueprint, the engine+editions+instances table in §1 extended to all five live editions, and the per-project/portfolio-wide access-grant RBAC that backs Milepost/Blueprint's scope model (migrations 0036/0037, crates/cartos-data/src/industry.rs + crates/cartos-api/src/industry.rs, features/industry-access/ProjectAccessPanel.tsx, and its modules/admin/ProjectAccessConfig.tsx User-Admin console tab). §2 now also lists all five live app subdomains, not just the marketing site. Earlier: the operational-data / admin-layer / ask-the-map epic — ALL 4 phases shipped to main and live on cartosatlas.com: Phase 1 a DB-backed Datasource + Layer Catalog (migrations 0038/0040 catalog_features) scoped by tenant/edition/module/group/role/user-type, with a tech_admin CatalogConfig console and a portable cat--prefixed reconciler + CatalogLayersOverlay mounted in every MapLibre-native module; Phase 2 real OpenStreetMap data (via tools/osm_to_geojson.py + Overpass, ingested with the new cartos-cli load-geojson region loader) live in all 7 MapLibre-native module families — Cartos Grid/Charlotte (293 substations, 1,278 transmission spans), Smart City/Abu Dhabi (95 hospitals, 201 schools), Maritime/Gulf (94 marine facilities), Emergency Services/Abu Dhabi (56 stations), Cartos Tower/DFW (265 airfield movement segments), Cartos Milepost/Indianapolis (1,537 motorway segments), Cartos Blueprint/Las Vegas (363 construction sites) — plus a live bottom stats bar and GET /api/catalog/kpi; Phase 3 an attribute-driven symbology engine (lib/catalog-style.ts — categorical/graduated color + size, dashed lines, polygon fill/outline) with an in-panel style editor + on-map legend, and real 3D (polygon fill-extrusions + deck.gl 3D point columns via lib/catalog-3d.ts); Phase 4 two cross-module agent tools, catalog_layers + query_catalog_layer ("ask the map" — attribute filter, proximity, group-and-count, coordinate sample, all scope-enforced) over any catalog layer the caller can see. Alongside: per-edition full-screen dashboards — every Grid + industry-edition department module now has its own /<moduleId>/dashboard route (shell/DashboardPage) that returns to its own map, not a hardcoded Smart City; a new reusable CatalogDashboard (ECharts charts, auto-classified attribute breakdowns, computed alerts, reorderable cards, theme switching) is the standard dashboard body for the industry editions (Tower/Milepost/Blueprint), falling back to representative KPI tiles where a module has no catalog data yet; Grid keeps its own chart-rich KPI dashboard bound to live /api/grid data; and a new CatalogKpiStrip pre-stocks the native module dashboards (Smart City, Emergency, Maritime, Grid) with real catalog feature counts. Two shell/auth fixes: the native Government KPI strip no longer leaks onto Grid/industry-edition modules (nativeModuleFromPath — it previously defaulted to Smart City's Abu Dhabi data and a "Dashboard" link that bounced back to /smart-city); the Grid/industry catalog stats bar (MapStatsBar) moved off the map into the shell's bottom footer slot alongside the native KPI strip; and the Keycloak realm's login-page displayName was fixed from the retired "Cartos Atlas" to "Cartos". Earlier: hard rebrand: this guide carries no affiliation with the former parent org — every prior parent-org name and the standalone former-partner-integration name are gone from this doc and its rendered artifacts; the retired external integration module and its dedicated embedding-doc reference dropped from the module catalog table and nav table; Government edition now referred to only as Cartos Atlas. Earlier: deployment-readiness hardening Phases A–D: JWKS live refresh loop + CARTOS_REQUIRE_TENANT_CLAIM, migration 0033 RLS on 5 previously-uncovered tables + opt-in cartos_app non-superuser DB role, CI gate ci.yml + cargo test 4/4, DB pool 8→50 + entitlement cache + shared HTTP clients + router timeout/concurrency limits + 15 s KPI cache, FlatGeobuf debounce + code-splitting + tab-visibility polling, GET /readyz health probe + compose healthcheck, iframe-bridge origin pinning, JSON logs + request-ID, migration 0034 dedup guard. Earlier: PR #123 — real connector adapter seam: crates/cartos-api/src/connectors/RealConnector trait, is_real() config-driven dispatch, real_sync() registry; connectors/esri_un.rs reference implementation verified live against a public ArcGIS Feature Server; cartos-data additions: get_config + upsert_real_assets; grid::sync now routes to real adapter when config.service_url is set, otherwise mock feed unchanged. Earlier: PR #120 — Admin → Integrations tab: point-and-click CRUD for Grid SoR connector registry (tech_admin-gated), migration 0032 unique-kind index, IntegrationsConfig.tsx, new POST/PATCH/DELETE /api/grid/integrations{/:kind} routes. Earlier: PRs #113–#117 — cloud-native data layer spikes: serverless PMTiles vector tiles from MinIO, in-browser DuckDB-WASM analytics over GeoParquet, FlatGeobuf bbox-streamed vector, COG range-read raster, Turf.js client-side geoprocessing, and broad layer export — all on the existing MinIO object store, no new services; spiked / proof-of-concept, production hardening TBD. Earlier: PRs #111 fix mock SoR labels; PRs #102–#109 Cartos Grid all seven SoR connectors end-to-end; PR #103 AIS analytics pushed to Cesium MDA globe). This is the single orientation doc for everything built so far — platform, modules, features, how to run it, the marketing site, and the GTM artifacts.


Contents

  1. What Cartos is
  2. Live URLs & quick links
  3. Architecture at a glance
  4. Repository layout
  5. The platform (Phase 1 substrate)
  6. The modules
  7. Cross-cutting features every module inherits
  8. Data sources, integrations & keys
  9. HTTP API reference
  10. The marketing website & GTM artifacts
  11. Phase 2 — Analyst Intelligence Layer (Wave 2A built)
  12. Phase 3 — Cartos Field
  13. Reference docs

Jump to: §7.4 Tenant Capability System · §7.5 Cartos Field — Phase 3A · §8.1 Cartos Grid — Electric Utility · §8.2 Historic AIS analytics · §8.3 Cartos Tower/Milepost/Blueprint · §8.4 Cartos Africa — One Africa edition · §8.5 Cartos Guardian · §8.6 Cartos Sentry · §8.7 Cartos Harvest · §9.1 Cartos agent overhaul · §9.2 Map Export & Mapbook · §9.3 3D Data — Point Clouds & Gaussian Splats · §9.4 Platform theme system · §9.5 Cloud-native data layer spikes · §9.6 Layer Catalog — ask the map · §9.7 Dashboards · §9.8 Time of Day & Measure · §9.9 Draw & Navigate rail · §9.10 Mapillary street imagery · §13 Phase 2 Analyst Intelligence Layer · §13.4 Real segmentation model · §13.6 MCP server


1. What Cartos is

Cartos is a sovereign geospatial vertical-AI platform: ingest, serve, and visualize geospatial data inside your own territory, with an AI analyst wired in from the first click. Every "vertical" (city, port, emergency services…) is a slice over one shared data fabric, one agent, and one shell — not a separate product.

Engine + Editions + Instances model

Cartos is the horizontal ENGINE — the data fabric, agent, capability system, shell, and all cross-cutting features. Industries are licensed EDITIONS of that engine. An edition can be licensed as multiple INSTANCES by different organisations.

Ownership & licensing (reworked 2026-07-28 — see EarthWatch3D-PRD/PRD.md §20 for the full language). Cartos — the engine and every one of its editions — is the sole and exclusive property of Steven P. Santovasi (the "Owner"). The Owner grants Solution Strategy Partners ("SSP") a single unrestricted, perpetual, irrevocable, worldwide, royalty-free, sublicensable master license to the whole Cartos platform and every edition — SSP may use, host, deploy, modify, commercialize, distribute, and sublicense Cartos for any purpose, at no cost, but the license does not transfer ownership. Every downstream edition instance below is delivered as a non-exclusive sublicense under SSP's master license, not a direct grant from the Owner — e.g. Cartos Atlas (the owner-held, first Government-edition instance) and Cartos Grid (the RAiN Partners instance of the Electric-Utility edition) are sublicensed instances, each the first, not the sole instance of its edition, provided "as-is" at the state existing at handoff.

Edition Instance(s) Licensee
Government / Defense Cartos Atlas First, non-exclusive sublicensed instance under SSP's master license (not the sole Government version)
Electric Utility Cartos Grid RAiN Partners — non-exclusive sublicensed instance
Airport Cartos Tower Owner-owned reference instance (built 2026-07-07; not yet sublicensed externally)
Transportation / DOT Cartos Milepost Owner-owned reference instance (built 2026-07-07; not yet sublicensed externally)
Infrastructure Delivery / AEC Cartos Blueprint Owner-owned reference instance (built 2026-07-07; not yet sublicensed externally)
Government — Continental Cartos Africa Owner-owned reference instance — merged to main (PR #127) and live at https://africa.cartosatlas.com; standalone continental Government edition for African governments / NMAs / ministries
Government — Critical-Infrastructure Protection (proactive) Cartos Guardian Owner-owned reference instance — merged to main (PR #133/#134) and live at https://guardian.cartosatlas.com; UAE strike-risk protection, Strait-of-Hormuz focus
Government — Crisis Monitoring (reactive) Cartos Sentry Owner-owned reference instance — merged to main and live at https://sentry.cartosatlas.com; optical+SAR+thermal change detection, watches Guardian's highest-risk assets
Agriculture Intelligence Cartos Harvest Owner-owned reference instance — merged to main (tag cartos-harvest-v1) and LIVE at harvest.cartosatlas.com; STAC-native crop type/health/yield/water/boundaries/risk, anchored on Kenya + Nigeria. See §8.7.

A new industry segment gets a new edition; a new customer within that segment gets a new sublicensed instance of the existing edition. All nine editions above are now built and merged to main; eight are also live, each on its own subdomain of cartosatlas.com (edition detected from hostname — see §8.3 for Tower/Milepost/Blueprint, §8.1 for Grid, §8.4 for Africa, §8.5 for Guardian, §8.6 for Sentry); the ninth, Cartos Harvest, is LIVE at harvest.cartosatlas.com (deployed 2026-07-29, Caddy vhost, or Keycloak redirect origin yet) — see §8.7. Guardian and Sentry are a deliberate pair — proactive protection feeding a reactive monitor — not two independent verticals; see the Guardian → Sentry handshake in §8.6.

The three phases

Phase Name Status
I The Sovereign Platform — ingest/serve/visualize + agent, live vertical modules Live (built) · three more industry editions — Cartos Tower (Airport, 10 modules), Cartos Milepost (Transportation/DOT, 10 modules + portfolio/project RBAC), Cartos Blueprint (Infrastructure PM/AEC, 9 modules + portfolio/project RBAC) — built 2026-07-07, live on their own subdomains (see §8.3); Cartos Grid live operational data (assets/outages/work orders/vegetation/parcels/CIP projects + KPI API + agent tools); all seven SoR connectors end-to-end (OMS/ADMS, IBM Maximo, SAP PM, Esri Utility Network, Primavera P6, Microsoft Project, File/CSV import); Admin → Integrations tab for connector-registry CRUD (tech_admin); real connector adapter seam (crates/cartos-api/src/connectors/RealConnector trait, config-driven dispatch; Esri UN verified live against a public ArcGIS service; other connectors remain mock until credentials supplied); Historic AIS analytics (vessel track replay, density heatmap, port calls); AIS analytics pushed to Cesium MDA globe via cartos.ais.set bridge; cloud-native data layer spikes (PMTiles / GeoParquet+DuckDB-WASM / FlatGeobuf / COG / Turf.js geoprocessing / broad export) proven on existing MinIO — no new services; Grid Data panel; spiked / proof-of-concept; Operational Data & Layer Catalog — all 4 phases shipped (admin-managed datasources + map layers scoped by tenant/edition/module/group/role/user-type; real OpenStreetMap data live in 7 module families; attribute-driven symbology incl. real 3D; two cross-module "ask the map" agent tools — see §9.6); per-edition full-screen dashboards for every Grid + industry module + a reusable catalog-driven dashboard (see §9.7); Cartos Africa — One Africa edition, merged to main and LIVE at africa.cartosatlas.com (9 continental Government modules, country/subnational scope, foundation-layer fabric, Continental Command dashboard, real data in all 9 modules, africa_scope agent tool, see §8.4); holographic point symbology + feature-type icons, an edition-switcher pulldown, and session-vectors-above-rasters ordering shipped platform-wide the same day; Cartos Guardian — critical-infrastructure protection, merged to main and LIVE at guardian.cartosatlas.com (5 modules, 103 curated UAE sites, a transparent per-site strike-risk model, grid/strategic-vector/POI-collateral layers, historical-imagery + live AIS/ADS-B traffic overlays, guardian_strike_risk agent tool, see §8.5); Cartos Sentry — reactive crisis monitoring, merged to main and LIVE at sentry.cartosatlas.com (3 modules, the platform's first background monitoring daemon + real S1/S2 model-free change detectors + incident state machine/alerts + fusion confidence bands + commercial-tasking connectors + an InSAR coherence context layer, watching Guardian's highest-risk assets, see §8.6); Cartos Harvest — agriculture intelligence, merged to main (tag cartos-harvest-v1) and LIVE at harvest.cartosatlas.com (7 modules — command + crop type/health/yield/water/boundaries/risk — a STAC-native crop pipeline over Sentinel-2 via Microsoft Planetary Computer: KMeans temporal-phenology crop classification (v0.2), NDVI-anomaly crop health, season-NDVI yield proxy with inter-annual-CoV uncertainty, gdal_polygonize field-boundary candidates + human review, a farmer registry, and a composed 0–100 agri-risk score with an index-insurance trigger; persisted derived products (M03) — panel runs can be saved as lineage-tracked datasets, browsable in a Saved Products panel and queryable by the harvest_products agent tool; anchored on Kenya + Nigeria, reuses the Cartos Africa country-scope + foundation fabric, see §8.7); a ride-along Opus 5 thinking-block parser fix in cartos-agent's Anthropic client (llm.rs) that ended intermittent 502s on reasoning turns, platform-wide
II Analyst Intelligence Layer — change detection, derived data, 3D reconstruction, auto-reporting Wave 2A + Spike A built (derived-dataset graph, reporting, async job tier, real SegFormer segmentation over real imagery, real two-epoch Sentinel-2 change detection, compute-tier selection, GPU-sidecar deployable artifact, Compare + Map Export capability features refined to EarthWatch3D parity, Compare bidirectional camera sync + "Current map" source, 3D Data point clouds with true geospatial placement, Gaussian splats in MapLibre via three.js + SuperSplat SOGS importer, MCP server) · agent overhauled with rich rendering + export
III Cartos Field — offline-first mobile field-capture edge 3A + 3B built (backend + config console + analyst + agent + seed + media backend + module assignment + portable overlay; Flutter mobile app compiled in CI, Android APK live via Firebase App Distribution)

This guide covers Phase I (§7), Phase II Wave 2A (§13), and Phase III (§7.5 + §14). Cartos Grid (the first vertical edition) is documented in §8.1; Cartos Tower, Milepost & Blueprint (three live vertical editions) in §8.3; Cartos Africa (the sixth live edition) in §8.4; Cartos Guardian (the seventh live edition) in §8.5; Cartos Sentry (the eighth live edition) in §8.6; Cartos Harvest (the ninth live edition) in §8.7.

Two different things are called "Phase 2": the RLS / group-access work (shipped — §7) and the Analyst Intelligence Layer (Wave 2A now built — §13). When in doubt, ask which is meant. "Phase 3A/3B" in this guide means the complete Cartos Field stack — sync backend, web config console, analyst module, media backend, module assignment, portable overlay, and the Flutter mobile app (CI-compiled, Android APK live via Firebase App Distribution; iOS pending Apple Developer enrollment).


What Where
Marketing site (live) https://cartosatlas.com
Interface tour https://cartosatlas.com/interface
Editions overview https://cartosatlas.com/editions
Commercial model (gated, password the demo password — temporary) https://cartosatlas.com/commercial-model
App — Cartos Atlas (Government) https://atlas.cartosatlas.com
App — Cartos Grid (Electric Utility) https://grid.cartosatlas.com
App — Cartos Tower (Airport) https://tower.cartosatlas.com
App — Cartos Milepost (Transportation/DOT) https://milepost.cartosatlas.com
App — Cartos Blueprint (Infrastructure PM/AEC) https://blueprint.cartosatlas.com
App — Cartos Africa (Government · continental) https://africa.cartosatlas.com (see §8.4)
App — Cartos Guardian (Government · critical-infrastructure protection) https://guardian.cartosatlas.com (see §8.5)
App — Cartos Sentry (Government · reactive crisis monitoring) https://sentry.cartosatlas.com (see §8.6)
App — Cartos Harvest (Agriculture intelligence) LIVE at harvest.cartosatlas.com (merged to main, tag cartos-harvest-v1); also runnable locally with ?edition=harvest or VITE_CARTOS_EDITION=harvest (see §8.7)
App (local dev) http://localhost:5173 (Vite dev) against cartos-api on :3000 (or :4000 when Polyphony is up)

The marketing site is hosted on Cloudflare Pages (push main = publish). The app also runs on a small stakeholder-demo VM — one Caddy origin, one edition per subdomain, hostname-detected (deploy/Caddyfile.prod) — with demo/seed data only, not sovereign infrastructure (per deploy/DEPLOY.md); it is also locally and §12 for the site.


3. Architecture at a glance

            ┌──────────────────────────────────────────────────┐

   Browser  │  cartos-web — React + Vite shell                  │

            │  MapLibre (2D) · Cesium iframe (3D) · Dockable    │

            │  panels · AI agent chat · GeoWizard · overlays    │

            │  AnalysisOverlay · CompareOverlay (swipe)         │

            └───────────────┬──────────────────────────────────┘

                            │  HTTPS (Bearer JWT)

            ┌───────────────▼──────────────────────────────────┐

   Rust     │  cartos-api (axum)  — JWT validated vs Keycloak    │

   workspace│  JWKS. Routes per module + agent + admin + layers  │

            │  ├─ cartos-agent  LLM tool registry (Anthropic)    │

            │  ├─ cartos-data   Postgres queries + RLS session   │

            │  ├─ cartos-core   TenantContext (tenant/tier/roles/groups)

            │  └─ model-job worker (SKIP-LOCKED → sidecar loop) │

            └───────┬───────────────────────────────────────────┘

                    │           │ POST /infer (tier routing)

        ┌───────────┴──────┐   ┌───────▼────────────┐  ┌─────────────────┐

        │  Postgres+PostGIS│   │  model-sidecar      │  │  GPU sidecar    │

        │  (RLS tenant+team│   │  FastAPI :8300       │  │  (Dockerfile.gpu│

        │  derived dataset │   │  SegFormer-b0/ADE   │  │  CUDA 12.4 +    │

        │  graph + job Q)  │   │  real imagery fetch  │  │  SegFormer-b5)  │

        └──────────────────┘   │  change.detect (real)│  │  MODEL_GPU_SIDECAR_URL

                               └────────────────────-─┘  └─────────────────┘

        ┌──────────────────┐   ┌──────────────────────┐  ┌─────────────────┐

        │  Keycloak        │   │  MinIO + Caddy        │  │  mcp-server/    │

        │  (SSO/JWT)       │   │  object store + proxy │  │  stdio → AI host│

        └──────────────────┘   └──────────────────────┘  └─────────────────┘

4. Repository layout

Path What it is
crates/cartos-core Type primitives — TenantContext (tenant_id, tier, roles, groups), Role, Tier, plus the declarative capability catalog.rs (FEATURES registry + CATALOG of modules).
crates/cartos-data Postgres queries + set_tenant_session / set_admin_bypass helpers. Every read runs in a transaction with RLS session vars set; WHERE filters are belt-and-braces. capabilities.rs loads/writes per-tenant module + feature enable state. field.rs is the data layer for Cartos Field (apps + captured features). grid.rs is the data layer for Cartos Grid operational tables (assets, outages, work orders, vegetation segments, parcels, CIP projects, and aggregate KPI query). grid_integrations.rs is the SoR connector registry + all sync/import functions (sync_oms, sync_maximo, sync_sap, sync_esri_un, sync_p6, sync_msproject, import_csv_work_orders); also exposes get_config(pool, tenant_id, kind) (reads a connector's config JSONB — used by the real-adapter dispatch in grid::sync) and upsert_real_assets(pool, tenant_id, source, rows) (generic batch UNNEST upsert into grid_assets keyed by external_ref, shared by all real adapters). ais.rs is the data layer for historic AIS (vessel_positions track/density/port-call reads; insert_positions used by the live ingester). catalog.rs is the data layer for the Datasource + Layer Catalog (scoped datasource/layer CRUD with secret-omitting projections, list_module_layers/list_accessible_layers cross-module scoped reads, module_kpi per-layer feature counts, list_layer_features/query_layer_features internal-feature GeoJSON + filtered queries — see §9.6). industry.rs is the data layer for the industry-edition per-project RBAC (Cartos Milepost/Blueprint) — list_projects/caller_admin_scope/grant_scope/list_access/grant_access/caller_project_level/update_project/revoke_access — see §8.3.
crates/cartos-agent LLM tool registry (Tool trait, register_*). Module-namespaced builtin tools; Anthropic client (llm.rs). Tool surface narrowed by enabled modules at call time. builtin/catalog_layers.rs + builtin/query_catalog_layer.rs are the two cross-module "ask the map" tools over the Layer Catalog.
crates/cartos-api axum HTTP. JWT validation vs Keycloak JWKS. Per-module route modules, kpi.rs, admin.rs (+ keycloak_admin.rs), mobility.rs, layers.rs, scope.rs, field.rs (Cartos Field routes), grid.rs (Cartos Grid list + KPI + SoR integration endpoints — sync handler does config-driven real-vs-mock dispatch), oms_sync.rs (six optional background schedulers for mock SoR feeds — opt-in via CARTOS_{OMS,MAXIMO,SAP,ESRI_UN,P6,MSPROJECT}_MOCK=1), connectors/ (real connector adapter seam — mod.rs: RealConnector trait + is_real() + real_sync() registry; connectors/esri_un.rs: reference Esri Utility Network real adapter), ais.rs (historic AIS read endpoints), ais_ingest.rs (opt-in live AIS ingester background task), catalog.rs (Layer Catalog routes — GET /api/catalog/layers, /api/catalog/kpi, /api/catalog/layers/:id/features, tech_admin CRUD for layers + datasources, bounded test-connection probe), industry.rs (/api/industry/* — project-access RBAC for Cartos Milepost/Blueprint, see §8.3). Phase 2: derived.rs (M03 derived-dataset graph), report.rs (M02 sessions + HTML/DOCX render), jobs.rs (async model-job queue + background worker). Capability routes in handlers.rs (GET /api/capabilities) and admin.rs (module/feature/user-module writes).
crates/cartos-cli migrate, seed, and load-geojson (the Phase-2 region loader — idempotently ingests a GeoJSON FeatureCollection into the Layer Catalog as an internal-PostGIS datasource + layer + catalog_features; loader.rs). Runs as the postgres superuser → bypasses RLS by design.
crates/cartos-codec Shared codec/types.
cartos-web React + Vite shell. Native modules (Smart City, Maritime, Emergency Services, Cartos Grid — nine utility departments via modules/grid/, Cartos Tower/Milepost/Blueprint — 29 industry-edition department modules via modules/industry/ and the shared IndustryModule surface, see §8.3) + Cesium-iframe modules (Situational Awareness, Maritime Domain Awareness). Capability context (useCapabilities()) drives nav and panel gating. Portable feature components live in src/features/ (e.g. overture/OvertureOverlay.tsx, analysis/AnalysisOverlay.tsx, field-captures/FieldCapturesOverlay.tsx, point-cloud/PointCloudOverlay.tsx, maritime-analytics/MaritimeAnalyticsOverlay.tsx, industry-access/ProjectAccessPanel.tsx — see §8.3). lib/edition.ts resolves the active edition (atlas/grid/tower/milepost/blueprint/full) from a build flag, a ?edition= override, or the hostname, and drives the nav lineup, brand wordmark, default route, and default theme. Key shell additions: AgentMarkdown.tsx (GFM + inline SVG chart renderer), lib/agent-export.ts (HTML/PDF/DOCX export of agent conversations), lib/image-boost.ts (global raster quality control), lib/point-cloud.ts (deck.gl MapboxOverlay + Tile3DLayer / PointCloudLayer; proj4 georeferencing; density decimation), lib/gaussian-splats.ts (three.js overlay canvas, MapLibre v5 defaultProjectionData.mainMatrix projection split; @mkkellogg/gaussian-splats-3d DropInViewer), lib/sogs.ts (SuperSplat SOGS importer — .ssog / .sog / .zip bundle decode via fflate, LOD-aware splat selection), lib/themes.ts + lib/theme-context.tsx (platform theme system — 10 palettes, CSS custom-property tokens on :root, one global theme + per-module overrides, persisted in localStorage), shell/ThemeSwitcher.tsx (palette picker UI), shell/CartosMark.tsx (Cartos inline SVG mark on splash/login/shell), lib/map-styles.tsrecolorStyleAccent / resolveBasemapStyle / holographic-theme / hillshade-theme keys (theme-matched basemaps recoloured live on theme change), lib/grid-data.ts (Grid operational layer fetch + MapLibre source/layer reconciler, KPI fetch; DEPT_LAYERS maps each Grid department to its default data layers; fetchIntegrations, syncIntegration, importGridCsv for SoR connector management), lib/ais.ts (Historic AIS fetch helpers + MapLibre ais-* source/layer helpers for track replay, density heatmap, and port markers). Cloud-native data layer spike libs (PRs #113–#117): lib/pmtiles-layer.ts (registers pmtiles:// protocol, addPmtilesAssets/removePmtilesAssets — vector tile archive range-read from MinIO), lib/duckdb.ts (lazy-boots DuckDB-WASM self-hosted via Vite ?url, fetches a GeoParquet file from MinIO, exposes runDuckQuery for ad-hoc columnar SQL), lib/flatgeobuf-layer.ts (loadFgbAssets range-reads only the features inside the current viewport bbox, re-reads on map moveend), lib/cog-layer.ts (registers cog:// protocol via @geomatico/maplibre-cog-protocol; addCog/removeCog), lib/layer-export.ts (broad client-side export: exportGeoJSON / exportShapefile / exportFlatGeobuf / exportCsv). Spike feature panels: features/duckdb/DuckdbSpikePanel.tsx (DuckDB-WASM analyst query workspace, launched from grid-capital), features/geoprocessing/GeoprocessingPanel.tsx (Turf.js buffer/convex hull/centroid over a live Grid layer), features/export/ExportPanel.tsx (format picker + download + GeoPackage link from MinIO). Operational Data & Layer Catalog (see §9.6): lib/catalog.ts (typed fetch client — layers/datasources/KPI/features CRUD), lib/catalog-layers.ts (reconcileCatalogLayers — the cat--prefixed MapLibre source/layer reconciler, style-aware), lib/catalog-style.ts (symbology engine — ColorSpec/SizeSpec, colorExpr/sizeExpr/heightExpr, legend + classification helpers), lib/catalog-3d.ts (deck.gl ColumnLayer overlay for 3D point columns), features/catalog/CatalogLayersOverlay.tsx (the portable per-module overlay + panel), features/catalog/LayerStyleEditor.tsx (tech_admin live style editor), features/catalog/CatalogLegend.tsx (on-map legend), features/catalog/MapStatsBar.tsx (live bottom stats bar, now rendered in the shell's footer slot for Grid/industry modules), features/catalog/CatalogKpiStrip.tsx (live-data tile strip mounted in native module dashboards), features/catalog/CatalogDashboard.tsx (reusable ECharts dashboard — the industry-edition dashboard standard), modules/admin/CatalogConfig.tsx (tech_admin datasource/layer console). Dashboards (see §9.7): shell/DashboardPage.tsx (shared full-screen dashboard shell for Grid + industry editions), modules/grid/GridDashboardPage.tsx, modules/industry/IndustryDashboardPage.tsx.
overture-sidecar/ Python/DuckDB FastAPI sidecar for Overture Maps GeoParquet queries. Proxied by cartos-api at /api/overture/*. See §10.
model-sidecar/ Phase 2 model sidecar (FastAPI :8300). Stateless — never touches the DB. main.py implements GET /healthz (Gate-0 check: ok + device + models_loaded + segment_real), POST /infer (job-kind dispatch), POST /render/docx (M02 DOCX), POST /render/markdown-docx (agent conversation DOCX), POST /render/geopdf (GeoPDF georeferencing via GDAL — accepts PNG bytes + Mercator bbox, runs gdal_translate -of PDF -a_srs EPSG:3857 -a_ullr, returns PDF bytes). render.py = the DOCX renderer. segment_real.py = the real SegFormer model (Spike A): lazy-loads SegFormer-b0/ADE20K, fetches Esri World Imagery for the AOI (fetch_aoi_image), and also tiles-based imagery (fetch_aoi_tiles, used for Sentinel-2 change detection). Two job handlers: infer.segment (real model when SEGMENT_REAL=1 and ML stack present; deterministic mock fallback) and change.detect (real two-epoch diff when epoch tile URLs are supplied; mock fallback). Dockerfile.gpu = CUDA 12.4 + cu124 torch + SegFormer-b5 GPU build.
mcp-server/ Cartos MCP server (Node + @modelcontextprotocol/sdk, stdio transport). Read-only proxy over cartos-api. Eight tools: cartos_who_am_i, cartos_capabilities, cartos_smart_city_incidents, cartos_maritime_vessels, cartos_emergency_units, cartos_derived_datasets (with optional aoi bbox param), cartos_reports, cartos_report. Config: CARTOS_API_URL + CARTOS_TOKEN. dist/index.js is the compiled entry point.
tools/export_report.py Operator tool: fetch a saved analysis session → write .html + .pdf (headless-Chrome print) + .docx (via API → sidecar). Usage: python tools/export_report.py --session <uuid> [--api ...] [--narrate].
tools/build_pmtiles.py · tools/build_geoparquet.py · tools/build_cog.py · tools/build_geopackage.py · tools/upload_minio.py · cartos-web/scripts/build-flatgeobuf.mjs Cloud-native data layer generators (spikes, PRs #113–#117). One-off batch scripts that produce object-storage-native formats from a GeoJSON/raster source and optionally upload to MinIO. Scratch outputs go in tools/_tmp/ (gitignored). upload_minio.py is the shared MinIO upload helper (creds from env: MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY; defaults match deploy/docker-compose.yml). See §9.5.
tools/osm_to_geojson.py · tools/load_demo_catalog.sh · data/catalog/*.geojson Layer Catalog real-data acquisition (Phase 2, see §9.6). osm_to_geojson.py queries the public OpenStreetMap Overpass API (bbox + key/value, or an inline --query-str) and writes a GeoJSON FeatureCollection. load_demo_catalog.sh is the idempotent load manifest (runs cartos-cli load-geojson per dataset, locally via cargo or in-container). data/catalog/*.geojson are the nine acquired real datasets (provenance + reproducible reload).
deploy/ docker-compose (postgres + keycloak + minio + caddy + overture-sidecar + cartos-api + model-sidecar). The dev compose now includes the API and both sidecars as Linux containers (see §5 Option A), sidestepping Windows Smart App Control. Dockerfile.api builds the Rust binary in a multi-stage container. The API in-container uses CARTOS_KEYCLOAK_URL (internal service) and CARTOS_KEYCLOAK_PUBLIC_URL (browser-facing issuer) as two separate env vars.
website/ Live marketing site at cartosatlas.com (Cloudflare Pages; push main = publish). See §12.
cartos-pricing/ Commercial model & government business case (internal + public-safe; docx/pdf). See §12.
cartos_screenshots/ Source product screenshots → optimized to website/images/*.webp via website/tools/optimize-screenshots.py.
cartos-field-mobile/ Flutter mobile app (com.cartos.field). CI-compiled (Android APK live via Firebase App Distribution). Source layout: lib/models.dart, lib/api/field_api.dart, lib/db/local_store.dart, lib/sync/sync_service.dart, lib/sync/background_sync.dart, lib/auth/auth_service.dart, lib/offline/offline_maps.dart, lib/screens/. See cartos-field-mobile/README.md.
cartos-phase2/ Phase 2 planning + GTM doc set. The three Gate-0 build contracts in cartos-phase2/docs/phase-2/specs/ (derived-dataset-schema.md, session-result-object.md, gpu-job-contract.md) are the canonical specs that Wave 2A implemented. ⚠ cartos-phase2/docs/strategy/ip-timing.md is time-sensitive (public disclosure affects patentability).
cartos-phase3-mobile/ Phase III planning + GTM doc set (MRD/PRD). The code is now in cartos-field-mobile/.
cartos-grid-PRD/ Cartos Grid (Electric Utility edition) PRD + Capability Brief — documents the engine+editions model, the nine department modules, the licensing arrangement with RAiN Partners, and the deep-integration roadmap (operational data tables + system integrations). See also EarthWatch3D-PRD/PRD.md §20 for the non-exclusive Cartos Atlas Government edition framing.
docs/ Engineering & GTM docs (see §14).

7. The platform (Phase 1 substrate)

Every vertical inherits these. They are the moat.

Sovereign data fabric + team isolation (group access)

Authentication & access control

The AI agent (agent-native from click one)

7.4 Tenant Capability System

Status: built and wired; merged to main.

What it is: per-client sovereign configuration — a declarative, in-repo catalog of what modules and features can exist on the platform, plus per-tenant DB enable-state and per-user access overrides, with a three-tier admin surface to manage them.

The catalog (crates/cartos-core/src/catalog.rs)

Two static constructs compiled into the binary:

DB schema — enable state

Two migrations add the persistence layer:

Effective entitlements (crates/cartos-data/src/capabilities.rs)

load_entitlements(pool, tenant_id, user_id) returns an Entitlements struct: modules (tenant_modules enabled, minus user_module_denials) + features (tenant_features enabled, as "module_id/feature_id" strings). Helpers set_module_enabled, set_feature_enabled, and set_user_module_access are the write paths called by the admin API handlers.

Cross-pollinating features

The global feature registry decouples features from modules: a Tech Admin can attach any catalog feature to any module as an "advanced" add-on, beyond that module's defaults. For example, overture can be cross-added to the Maritime module so that maritime operators see Overture POI and road data alongside their vessel tracks. The API write-path (PATCH /api/admin/features/{module}/{feature}) validates the module id and feature id independently (not checking membership) for exactly this reason.

The Overture overlay is now a portable feature component (cartos-web/src/features/overture/OvertureOverlay.tsx) that owns its own layers, 3D buildings logic, and control panel. It is mounted by any module that has the <module>/overture capability enabled, passing in the module's map ref and a namespaced panel id. Smart City and Maritime both mount it.

Three admin tiers

All three are Keycloak realm roles in deploy/keycloak/cartos-realm.json; the full admin role is a superset:

Role Gate method What they can do
provider_admin ctx.is_provider_admin() Toggle which modules a client deployment has (PATCH /api/admin/modules/{id})
tech_admin ctx.is_tech_admin() Toggle features per module, including cross-adding advanced features (PATCH /api/admin/features/{module}/{feature})
user_admin ctx.is_user_admin() View/revoke per-user module access (GET/PATCH /api/admin/users/{id}/modules)

Enforcement — three gates

  1. Launcher nav (ModuleNav): AppShell fetches /api/capabilities on mount and stores the result in a CapabilitiesContext (useCapabilities()). ModuleNav receives enabledModules from the shell; items not in the enabled set are hidden entirely. If the fetch fails, null is left in context and the nav shows everything (fail open; hard enforcement is server-side).

  2. Per-module feature panels: modules read useCapabilities() and gate their dockable panels behind enabled.features.includes("<module>/<feature>"). For example, Smart City wraps its Overture panel in a capability check for smart-city/overture. The OvertureOverlay is not even mounted when the feature is off. The analysis panel (Analyst Intelligence) is gated the same way — smart-city/analysis, maritime/analysis, emergency-services/analysis — and its dock button is hidden when the feature is disabled for that module.

  3. Agent tool surface: as described in §7.3, the /api/agent/chat handler narrows the tool set to modules the calling user has enabled. A module whose tools are removed from the surface is invisible to the LLM.

    Hard data-route enforcement (HTTP 403 from module data routes when the module is disabled) is not yet implemented — the capability system currently gates the nav + panels + agent surface; the underlying REST routes for module data remain accessible to any authenticated user with a valid JWT.

Web admin surface (cartos-web/src/modules/admin/)

The Admin module (AdminModule.tsx) is a tabbed config with five tabs rendered based on the caller's roles:

Admin console usability pass (2026-07-29). The Modules and Features tabs gained module and feature filters — with nine editions and 67 department modules the ungated lists had become long enough to be hard to work in. The Catalog and Integrations tabs gained a Test button with a coloured status badge (modules/admin/TestBadge.tsx, backed by a probe route in crates/cartos-api/src/grid.rs), so a datasource or connector endpoint can be checked in place instead of by triggering a sync and reading the outcome afterwards.

Changes take effect on each user's next page load (capabilities are fetched on mount).

Identity federation

docs/identity-federation-design.md documents the planned model for sourcing users, teams, roles, and credentials per sovereign deployment by federating Keycloak to the client's own IdP or Active Directory. This is design only, not built — it is the natural next step after the capability system for clients who require that identity stays under their own control.


7.5 Cartos Field — Phase 3A/3B

Status: built and merged to main. The full loop — config → capture → sync → analyst map — is in place, the Flutter mobile app compiles in CI and distributes Android APKs via Firebase App Distribution. Remaining work: on-device Android test (APK is live), iOS TestFlight (pending Apple Developer enrollment), and a minor definition-version reconciliation.

What it is: the full Cartos Field stack — a configurable, offline-first mobile field-capture platform that extends the sovereign data fabric to the field edge. What is built:

  1. Sync/field backend — DB tables, data layer (cartos-data), and API routes.
  2. Web config console — a "Field Apps" tab in Admin · Configuration.
  3. Capability integration — Field is a first-class catalog module, route-gated.
  4. Analyst web module — captured ground truth on the map (/field).
  5. Agent tools — cross-module; the agent can discover apps + summarize captures.
  6. Media backend — photos and signatures upload and download via /api/field/media.
  7. Field Apps → Modules — field apps are assignable to platform modules; their captures surface on those modules' maps via a portable overlay.
  8. Demo seeds — two published apps (Asset Inspection → Smart City; Port & Vessel Inspection → Maritime) with sample captures.
  9. Flutter mobile app — schema-driven capture, geometry editor, photos + signatures, offline queue, offline basemap caching, background sync, OIDC/PKCE auth, CI-compiled on Android (APK distributed via Firebase App Distribution).
  10. CI/distribution — Android CI workflow; iOS CI workflow (signed upload to TestFlight gated on Apple secrets).
  11. Git infra fix.gitattributes (*.sql text eol=lf) preventing sqlx checksum failures from Windows autocrlf.

Data model (migration 0009_field_init.sql)

Two new tables join the data fabric:

field_apps — a versioned field-app definition authored by a Tech Admin:

Column Type Notes
id UUID PK gen_random_uuid()
tenant_id TEXT Tenant scope
slug TEXT URL-safe identifier; UNIQUE(tenant_id, slug)
name TEXT Human label
description TEXT Optional
definition JSONB {"layers":[{key, label, geometry: point|line|polygon, color, fields:[{key, label, type, required, options?}]}]}
version INTEGER Bumped automatically whenever definition changes
status TEXT draft or published
owning_team TEXT Group-access scope (same pattern as all tenant data)
created_at / updated_at TIMESTAMPTZ

field_features — captured ground truth synced from the mobile app:

Column Type Notes
id UUID PK Server-assigned
client_id TEXT Mobile-generated; UNIQUE(tenant_id, client_id) — the idempotency key for store-and-forward
tenant_id TEXT
app_id UUID FK → field_apps(id) ON DELETE CASCADE
layer_key TEXT Which layer within the app definition
geometry JSONB GeoJSON geometry
properties JSONB Captured field values
captured_by TEXT User id (from JWT at sync time)
captured_at TIMESTAMPTZ Device timestamp
device TEXT Optional device identifier
accuracy_m DOUBLE PRECISION Optional GPS accuracy
owning_team TEXT Forwarded from the capture batch
synced_at TIMESTAMPTZ Server receive time

Both tables carry (tenant_id, owning_team) indexes and follow the same owning_team IS NULL OR owning_team = ANY($groups) group-access filter as all other Cartos tenant data (see docs/group-access-design.md).

Field types supported in the definition schema: text, number, choice (comma-separated options), date, boolean, photo, signature.

Data layer (crates/cartos-data/src/field.rs)

API routes (crates/cartos-api/src/field.rs, src/media.rs)

Config operations require tech_admin (or full admin); capture sync, feature reads, and media endpoints are open to any authenticated tenant user.

Method Route Auth Purpose
GET /api/field/apps any tenant user List all apps visible to the caller's groups (includes modules array)
POST /api/field/apps tech_admin Create a new field app (slug + name required)
GET /api/field/apps/{slug} any tenant user Fetch a single app with its module assignments (mobile pulls the definition here)
PATCH /api/field/apps/{slug} tech_admin Update name, description, status, or definition; bumps version when definition changes
DELETE /api/field/apps/{slug} tech_admin Delete app and all its captured features (cascade)
PUT /api/field/apps/{slug}/modules tech_admin Set which catalog modules this app surfaces in (replaces the full set; validates against the module catalog)
POST /api/field/apps/{slug}/sync any tenant user Store-and-forward capture batch; idempotent. Returns {received, inserted, duplicates}
GET /api/field/apps/{slug}/features any tenant user List captured features (default limit 1 000, max 5 000). Optional ?limit=N
GET /api/field/captures/{module_id} any tenant user (host-module gate) Returns all apps assigned to module_id + their features. Gated in-handler by the caller's effective modules (not cap_guard) — so a Smart City user reads Smart City's field data without the field module.
POST /api/field/media any tenant user Raw-body upload (up to 25 MB; extension from ?ext= or Content-Type). Returns {key, url, bytes, content_type}. Files stored under CARTOS_MEDIA_DIR/{tenant}/{uuid}.{ext} (swappable to MinIO/S3).
GET /api/field/media/{key} any tenant user Stream media back. Tenant-scoped: the key must start with the caller's tenant_id/ prefix; .. is rejected.

ApiError::NotFound was added to the error enum to produce clean HTTP 404 responses on unknown slugs.

Web config console (cartos-web/src/modules/admin/FieldAppsConfig.tsx)

A Field Apps tab appears in Admin · Configuration alongside Modules, Features, and Users & Teams. It is gated on tech_admin (the same check as the Features tab). The component presents a two-pane layout:

The mobile app pulls the published definition from GET /api/field/apps/{slug} and submits captures (with media keys already resolved) to POST /api/field/apps/{slug}/sync.

Capability integration

Cartos Field is a first-class catalog module (field, "Cartos Field", vertical field-ops) — not a bolt-on. It plugs into all four capability gates:

Analyst web module (cartos-web/src/modules/field/FieldModule.tsx)

A native MapLibre module at /field that closes the loop config → capture → sync → analyst map. It lists the tenant's published apps, and for the selected app renders its captured field_features on the map — points, lines, and polygons coloured by their layer's definition colour — with a click popup showing each feature's attributes (including photo/signature thumbnails via authenticated blob fetch using api().getBlob) plus capture lineage (who / when / device / accuracy). A DockablePanel lists apps and per-layer capture counts; the map fits bounds to the data.

Portable captures overlay (cartos-web/src/features/field-captures/FieldCapturesOverlay.tsx)

A self-contained portable React component that renders a host module's assigned field captures on that module's MapLibre map. It calls GET /api/field/captures/{module_id}, colours features by their layer definition colour, and provides click-to-detail with attribute table and photo/signature thumbnails (using MediaImage, same authenticated getBlob pattern as the analyst module). It is currently mounted in Smart City, Maritime, and Emergency Services. The component is namespaced (stable DockablePanel id per module) and is self-contained — no coupling to the host module beyond a map ref and the module id.

Media backend (crates/cartos-api/src/media.rs)

Photos and signatures flow through the API rather than directly to object storage, so the mobile contract is storage-backend-agnostic:

Both routes sit under /api/field/ and are therefore field-module gated (except the captures/{module} carve-out described above).

Field Apps → Modules (migration 0011_field_app_modules.sql)

A many-to-many table field_app_modules (tenant_id, app_id, module_id) lets a Tech Admin assign each field app to one or more platform modules. Data-layer helpers:

Seed assignments: asset-inspection → smart-city; port-inspection → maritime.

Agent tools (cartos-agent)

Two cross-module tools (module returns None — always exposed regardless of which module is active) let the agent reason about field data:

Demo seed (cartos-cli seed)

seed_field adds two published apps with captured features, both idempotent via deterministic UUIDs:

Flutter mobile app (cartos-field-mobile/)

The native client is a Flutter app (com.cartos.field). It is CI-compiled and the Android APK is live. Key source files:

CI / distribution:

Remaining 3B items: on-device Android field test (APK distributable now); iOS TestFlight (pending Apple enrollment); minor definition-version reconciliation.


8. The modules

The canonical nav lineup (reorderable — see §9):

Module Type What it surfaces
Smart City Native (MapLibre + dashboard) Assets, sensors, incidents & jurisdictions with live telemetry, KPIs, sensor-reading charts, AI summaries, and Overture Maps overlays with 3D buildings. All feature panels capability-gated (see §7.4).
Situational Awareness Cesium iframe A photoreal 3D common operating picture in a streaming globe (bridge to ../cesium_project_SPS). Catalog default_features: [] — this is the EarthWatch3D iframe; native MapLibre overlays (analysis, compare, export…) do not apply. External-layer and satellite pushes are handled via the bridge panels, not capability features.
Emergency Services Native (MapLibre + ops dashboard) Units, incidents (+ history window), traffic & CCTV cameras with a live operations dashboard.
Maritime Native (MapLibre + dashboard) Vessels, ports & port incidents across territorial waters. Historic AIS analytics overlay (track replay, density heatmap, port-call counts — see §8.2). Can carry the Overture overlay as an advanced cross-added feature (Tech Admin).
Maritime Domain Awareness Cesium iframe Wide-area maritime coverage, tracks & context rendered in 3D.
Cartos Field Native (MapLibre) Field-captured ground truth: published apps + their captured features (point/line/polygon) coloured by layer, with attribute + lineage + photo/signature thumbnail popups (see §7.5). Capability-gated on the field module. A portable FieldCapturesOverlay also surfaces each module's assigned captures directly on Smart City, Maritime, and Emergency Services maps.
Energy Planned Disabled nav placeholder (Phase 1+).
Cartos Grid (nine modules) Native (MapLibre + configurable KPI dashboard) The Electric Utility edition — nine switchable department surfaces on the shared GridModule.tsx component. See §8.1.
Cartos Tower, Milepost & Blueprint (29 modules) Native (MapLibre + configurable KPI dashboard) Three more industry editions — Airport (10), Transportation/DOT (10), Infrastructure PM/AEC (9) — switchable department surfaces on the shared IndustryModule.tsx component. See §8.3.
Cartos Africa / One Africa (nine modules) Native (MapLibre + IndustryModule, country-scope + foundation-layer overlays) Standalone continental Government edition for African governments, NMAs, and ministries. See §8.4.
Cartos Guardian (five modules) Native (MapLibre + IndustryModule, strike-risk overlay) UAE critical-infrastructure protection, per-site strike-risk model, Strait of Hormuz focus. See §8.5.
Cartos Sentry (three modules) Native (MapLibre + IndustryModule, monitoring/incident overlays) Reactive crisis monitoring — optical/SAR/thermal change detection watching Guardian's highest-risk assets. See §8.6.
Cartos Harvest (seven modules) Native (MapLibre + IndustryModule, country-scope + foundation-layer + STAC crop panels) Agriculture intelligence — STAC-native crop type/health/yield/water/boundaries/risk. Merged to main (tag cartos-harvest-v1) and LIVE at harvest.cartosatlas.com. See §8.7.
Admin Native Four-tab config surface — Modules (Provider Admin), Features (Tech Admin, with cross-add picker), Field Apps (Tech Admin — see §7.5), Users & Teams (User Admin, incl. per-user module access). Not in the module nav; opened via the user badge. Tabs shown only for the roles the caller holds.

Overture Maps overlay (portable feature)

The Overture overlay is a portable feature component (cartos-web/src/features/overture/OvertureOverlay.tsx) mounted by any module that has the <module>/overture capability enabled. It currently appears in Smart City (default feature) and can be cross-added to Maritime or any other module by a Tech Admin. The component owns its own MapLibre layers, 3D buildings logic, and dockable control panel; the mounting module only passes a map ref and a namespaced panel id.

Architecture: a Python/DuckDB FastAPI sidecar (overture-sidecar/main.py, port :8200) queries the public Overture GeoParquet directly from S3 via DuckDB spatial, snaps the viewport bbox to a ~2 km grid for cache reuse, and returns GeoJSON. cartos-api proxies it at GET /api/overture/{theme} (auth required). The sidecar is the first instance of the "Python sidecar" shape the platform anticipates for heavy data work (DuckDB + parquet kept out of the Rust hot path). In production it runs as the overture-sidecar service in docker-compose.prod.yml; the API is pointed at it via OVERTURE_SIDECAR_URL.

Four themes (independent toggles, each with a per-theme zoom gate below which the panel doesn't load):

Theme Geometry Zoom gate Rendering
Places (POI) Point ≥ 12 Circles with category-grouped colours (9 groups: Food, Shopping, Health, Education, Lodging, Transport, Finance, Civic, Leisure) + name labels at zoom ≥ 15
Transportation Line ≥ 13 Road-class colour + width coding (motorway through cycleway, 11 classes) with zoom-interpolated line width
Buildings Polygon ≥ 14 Fill + optional 3D extrusion (see below)
Divisions Polygon ≥ 9 Dashed boundary outlines + 5 % fill

All themes respond to click-for-attributes, showing a floating popup with the feature's Overture properties.

Smart City — 3D buildings

The Buildings theme exposes a 3D buildings sub-panel with three independent, simultaneously-togglable rendering modes:

Overture extrusion (MapLibre fill-extrusion):

Overture wireframe (deck.gl LineLayer):

Esri 3D Buildings (deck.gl Tile3DLayer + I3SLoader):

New web dependencies added: deck.gl ^9.3.3, @loaders.gl/i3s ^4.4.2, @loaders.gl/core ^4.4.2 (cartos-web/package.json).

Source files: cartos-web/src/lib/overture.ts (theme catalogue, colour expressions, fetch helper), cartos-web/src/lib/buildings-3d.ts (deck.gl overlay handle — createBuildings3DBuildings3DHandle), cartos-web/src/features/overture/OvertureOverlay.tsx (the portable component), cartos-web/src/modules/smart-city/SmartCityModule.tsx (mounts it).


Native module shape: <Module>Module.tsx (MapLibre + DockablePanels) + <Module>Dashboard.tsx. Iframe modules use lib/iframe-bridge.ts.


8.1 Cartos Grid — Electric Utility edition

Status: built and merged to main (PR #67 surfaces; PRs #92–#95 live operational data; PRs #102, #104–#109 all seven SoR connectors). Nine department module surfaces, configurable KPI dashboard, capability-gated; plus six live operational data tables (assets, outages, work orders, vegetation segments, parcels, CIP projects) with real PostGIS geometry, tenant/team RLS, live map layers in every department, KPI API, three Grid agent tools, and a full connector registry for seven system-of-record integrations (OMS/ADMS, IBM Maximo, SAP PM, Esri Utility Network, Primavera P6, Microsoft Project, File/CSV import) — all wired end-to-end.

What it is: the first vertical edition of the Cartos engine, licensed to RAiN Partners for the US electric utility market. Nine department-level module surfaces are delivered via one shared config-driven component (cartos-web/src/modules/grid/GridModule.tsx) driven by the config table in modules/grid/modules.ts (GRID_MODULES). Adding a new department is a new entry in that config — no new component needed.

The nine department modules

Module id Label Department badge Panels included Default map centre
grid-command Grid Command Executive cockpit dashboard, basemap, analysis, compare, export CONUS overview (z4)
grid-operations Network & Asset Operations Grid operations dashboard, basemap, analysis, compare, export, pointcloud Charlotte NC (z10)
grid-capital CIP & Major Projects Capital improvement program dashboard, basemap, analysis, compare, export, pointcloud Charlotte NC (z9) — CIP projects map layer live (P6 + MS Project SoR)
grid-storm Storm Response Emergency / storm ops dashboard, basemap, analysis, compare, export Florida (z6)
grid-fleet Fleet & Field Fleet / field ops dashboard, basemap, export, pointcloud Charlotte NC (z10)
grid-vegetation Vegetation Management Vegetation / forestry dashboard, basemap, analysis, compare, export, pointcloud Central California (z7)
grid-realestate Real Estate Management Real estate / ROW dashboard, basemap, analysis, compare, export Charlotte NC (z9)
grid-environmental Environmental Management Environmental / permitting dashboard, basemap, analysis, compare, export Charlotte NC (z8)
grid-stakeholder Stakeholder Management Stakeholder / public affairs dashboard, basemap, export Charlotte NC (z9)

Each module renders a HUD bar (module label + department badge) across the top of the map. The PanelDock shows only the panels declared for that module (the panels array in GridModuleConfig); the dashboard panel is in every module's default set.

Configurable KPI Dashboard (GridDashboard.tsx)

A new dashboard capability feature (registered in catalog.rs) that is the default on every Grid module. The dashboard component:

Engine reuse

All nine Grid modules inherit every engine cross-cutting feature via the shared GridModule.tsx: theme-matched basemaps, the analysis / compare / map-export / point-cloud / dashboard overlays from PanelDock, the full capability-gate system (gated per grid-<dept>/<feature>), the agent chat panel, and the platform theme switcher. Three Grid-specific agent tools are now registered (cross-module — module() returns None so they surface on every Grid department): grid_kpis, grid_active_outages, and grid_open_work_orders (see §7 — The AI agent, for descriptions).

Live operational data (PRs #92–#95)

Migration 0023_grid_operational_data.sql adds five operational tables; migration 0029_grid_projects_p6.sql adds a sixth. Together these six tables make every department map surface live:

Table Geometry Key fields What it holds
grid_assets Geometry(Geometry,4326) — point or LineString kind (transformer/pole/conductor/switch/substation), status, health (healthy/at_risk/critical), install_year, source, external_ref Network assets — point equipment and conductor feeder lines. source/external_ref added by migration 0028 (Esri UN) to support idempotent upserts
grid_outages Point status (active/restoring/resolved), cause, customers_affected, started_at, restored_at, source, external_ref, est_restoration, feeder Active and historic outages. source/external_ref/est_restoration/feeder added by migration 0025 (OMS/ADMS)
grid_work_orders Point kind (inspect/repair/replace/veg/other), status (open/in_progress/done), priority, crew, asset_id FK, due_date, source, external_ref Field work orders, optionally linked to an asset. source/external_ref added by migration 0026 (Maximo)
grid_veg_segments LineString risk (critical/high/moderate/low), span_miles, last_trimmed Vegetation encroachment spans along transmission/distribution corridors
grid_parcels Polygon kind (easement/fee/row), encroachment (none/structures/vegetation/access/other), area_acres Land parcels, easements, and right-of-way
grid_projects Point name, phase (planning/design/permitting/construction/closeout), status (on_track/at_risk/delayed/complete), budget_musd, pct_complete, start_date, finish_date, source, external_ref CIP capital-improvement projects. Fed by Primavera P6 and Microsoft Project SoR connectors

All five tables are tenant + team scoped (owning_team IS NULL OR owning_team = ANY($groups)) with the tenant_team_isolation Postgres RLS policy (same pattern as all other Cartos data). GiST indexes on geom and compound indexes on (tenant_id, owning_team) and (tenant_id, <status-key>). Seeded for tnt_dev_local with representative rows at each department's map centre (Charlotte NC, central Florida, central California).

API endpoints (crates/cartos-api/src/grid.rs, registered in main.rs):

Method Route Returns
GET /api/grid/assets Assets as {items: GridAssetRow[]} (geometry as geojson string)
GET /api/grid/outages Outages as {items: GridOutageRow[]}
GET /api/grid/work-orders Work orders as {items: GridWorkOrderRow[]}
GET /api/grid/vegetation Vegetation segments as {items: GridVegRow[]}
GET /api/grid/parcels Parcels as {items: GridParcelRow[]}
GET /api/grid/projects CIP projects as {items: GridProjectRow[]}
GET /api/grid/kpi Aggregate GridKpis — assets total/at-risk by kind, active outages + customers out, open work orders by kind, vegetation segment count by risk level, parcels total + encroached count, plus projects_total / projects_active / projects_budget_musd

All endpoints require a Bearer JWT; they are not gated by a specific Grid module capability (any authenticated tenant user can read). Each query runs inside a set_tenant_session transaction (RLS session vars) with a belt-and-braces owning_team IS NULL OR owning_team = ANY($groups) WHERE filter. Limit: 5 000 rows.

Frontend — Grid Data panel (lib/grid-data.ts, GridModule.tsx):

Cloud-native data layer — object-storage format spikes (Grid Data panel, PRs #113–#117)

Status: spikes / proofs-of-concept. Demo data only (synthetic Grid assets + CIP projects). Public-read MinIO bucket in dev; not yet access-gated or hardened. See §9.5 for the full implementation reference.

The Grid Data panel has a second group of toggles below the live operational layer checkboxes, labelled "Cloud-native (spike)". These drive five object-storage-native format experiments — zero new services; the browser range-reads everything directly from MinIO:

The DuckDB-WASM panel is launched from the grid-capital (CIP & Major Projects) module (distinct launcher button in the panel dock). It loads grid-projects.parquet from MinIO into an in-browser DuckDB instance and presents a SQL query input; results render in a table. No analytics server; PostGIS is untouched.

The Geoprocessing panel (buffer / convex hull / centroid via Turf.js) and Export panel (GeoJSON / Shapefile / FlatGeobuf / CSV client-side + GeoPackage from MinIO) are available on all Grid modules via the panel dock, as portable features/ components following the standard DockablePanel contract.

Systems-of-record (SoR) integrations (PRs #102, #104–#109)

All seven connectors are now wired end-to-end. A new grid_integrations registry table tracks each connector's status, last sync timestamp, and last sync result. Six connectors run realistic mock/synthetic feeds that demonstrate the live-integration pattern (ready for production credential swap); the seventh (File/CSV) ingests real uploaded data today. Every sync is idempotent — upsert keyed by (tenant_id, external_ref) — and tenant/RLS scoped.

Connector registry table — grid_integrations (migration 0025):

Column Type Notes
id UUID PK
tenant_id / owning_team TEXT Tenant + group scope; full tenant_team_isolation RLS
kind TEXT oms_adms / esri_un / maximo / sap / p6 / msproject / file_csv
name TEXT Human-readable connector label
target TEXT Which Grid table the connector feeds
status TEXT configured / connected / syncing / error
config JSONB Vendor/feed metadata
last_sync TIMESTAMPTZ Time of most recent sync (null until first sync)
last_result JSONB Result summary from the last sync call

The seven connectors:

# Kind Connector Target table Migration Mock rows Notes
1 oms_adms OMS / ADMS (outage management) grid_outages 0025 ~45 distribution incidents Status/cause/customers evolve each sync; drives storm map + customers-out KPI
2 maximo IBM Maximo (work management) grid_work_orders 0026 ~60 work orders source='maximo', ref prefix MAX-; open/critical-open counts
3 sap SAP PM (plant maintenance) grid_work_orders 0027 ~50 work orders source='sap', ref prefix SAP-; shares the sync_work_orders helper
4 esri_un Esri Utility Network grid_assets 0028 ~80 devices Kind/status/health/install_year evolve; drives asset-health KPI + assets map layer
5 p6 Primavera P6 (CIP schedule) grid_projects 0029 ~40 CIP projects Introduced the new grid_projects table; phase/status/% complete evolve
6 msproject Microsoft Project Schedule grid_projects 0030 ~30 CIP projects source='msproject', ref prefix MSP-; shares the sync_projects helper
7 file_csv File / CSV import grid_work_orders 0031 (real data) The only non-mock connector; operator uploads a CSV; bad rows skipped + counted

CSV import (connector 7 — file_csv):

Operator uploads a work-order CSV via the "Import CSV" file-picker on the dashboard SoR chip. The API (crates/cartos-api/src/grid.rs → import_csv) parses a header-driven comma-separated file and batches a single UNNEST upsert into grid_work_orders.

Required CSV columns: external_ref, lon, lat. Optional columns: kind, status, priority, crew, due_date — defaulted/normalised if absent or outside the allowed value set.

A column template is available at docs/samples/grid-work-orders.csv:

external_ref,kind,status,priority,crew,due_date,lon,lat

CSV-1001,inspect,open,high,Crew 7,2026-07-15,-80.84,35.23

Re-uploading the same file updates existing rows (upsert on external_ref) rather than creating duplicates. The API response carries {imported, created, updated, skipped}.

New grid_projects table (migration 0029):

CIP capital-improvement projects table, fed by both P6 and MS Project schedulers:

Column Type Notes
id UUID PK
tenant_id / owning_team TEXT Tenant + group scope + RLS
name TEXT Project name (e.g. "CIP Substation 001")
phase TEXT planning / design / permitting / construction / closeout
status TEXT on_track / at_risk / delayed / complete
budget_musd NUMERIC(12,2) Capital budget in $M
pct_complete INT Percentage complete (0–100)
start_date / finish_date DATE Planned schedule
source TEXT p6 or msproject
external_ref TEXT Upsert key; partial unique index (tenant_id, external_ref)
geom Geometry(Point,4326) Project location

The grid_projects table carries the full tenant_team_isolation RLS policy (same pattern as all other Grid tables). GiST index on geom; compound indexes on (tenant_id, status) and (tenant_id, owning_team).

New Grid KPIs from grid_projects:

The GET /api/grid/kpi response now includes three additional fields:

Field Type What it counts
projects_total i64 All CIP projects (any source)
projects_active i64 Projects where status != 'complete'
projects_budget_musd f64 Sum of budget_musd for active projects ($M)

CIP projects map layer (grid-capital module):

DEPT_LAYERS["grid-capital"] now defaults to ["projects", "assets"]. Project features are rendered as status-coded circles on the map: delayed → red, at_risk → amber, complete → grey, on_track → violet (#a78bfa). Clicking a project circle opens the standard Grid detail popup. This makes the grid-capital (CIP & Major Projects) module live.

Frontend — dashboard SoR strip (GridDashboard.tsx):

The GridDashboard component loads connectors via fetchIntegrations and renders a "Systems of record:" chip strip below the KPI cards:

API surface (SoR endpoints):

Method Route Auth Purpose
GET /api/grid/integrations any tenant user List connectors + status / last_sync / last_result
POST /api/grid/integrations tech_admin Register a connector — body {kind, name, target, status?}
PATCH /api/grid/integrations/{kind} tech_admin Partial update — name / target / status / config (null args = keep existing)
DELETE /api/grid/integrations/{kind} tech_admin Remove a connector registration
POST /api/grid/integrations/{kind}/sync any tenant user Run a mock connector sync; returns sync-result JSON
POST /api/grid/integrations/file_csv/import any tenant user Upload a work-order CSV; body {csv: "..."}
GET /api/grid/projects any tenant user CIP project rows as {items: GridProjectRow[]}

Calling sync on file_csv returns a 400 Bad Request with a clear message directing the caller to the import endpoint instead.

Opt-in continuous mock schedulers (crates/cartos-api/src/oms_sync.rs):

Six background tasks — one per mock connector — can be enabled individually via env vars to make map layers and KPIs evolve continuously as if receiving live feeds. file_csv has no scheduler (it is upload-driven).

Connector Env var to enable Tenant var Interval var Default interval
OMS/ADMS CARTOS_OMS_MOCK=1 CARTOS_OMS_TENANT CARTOS_OMS_INTERVAL_SECS 120 s
IBM Maximo CARTOS_MAXIMO_MOCK=1 CARTOS_MAXIMO_TENANT CARTOS_MAXIMO_INTERVAL_SECS 180 s
SAP PM CARTOS_SAP_MOCK=1 CARTOS_SAP_TENANT CARTOS_SAP_INTERVAL_SECS 180 s
Esri UN CARTOS_ESRI_UN_MOCK=1 CARTOS_ESRI_UN_TENANT CARTOS_ESRI_UN_INTERVAL_SECS 300 s
Primavera P6 CARTOS_P6_MOCK=1 CARTOS_P6_TENANT CARTOS_P6_INTERVAL_SECS 300 s
MS Project CARTOS_MSPROJECT_MOCK=1 CARTOS_MSPROJECT_TENANT CARTOS_MSPROJECT_INTERVAL_SECS 300 s

All tenant vars default to tnt_dev_local; all interval vars have a minimum of 30 s.

Real connector adapter seam (PR #123)

PR #123 ships the seam that lets any connector flip from mock feed to live system-of-record without any schema or UI change. connectors/esri_un is the reference implementation — verified against a public ArcGIS Feature Server (fetched and upserted 25 point features into grid_assets). The other five connectors (oms_adms, maximo, sap, p6, msproject) are still mock feeds; they go live once an operator pastes a service_url into their config. file_csv is upload-driven and does not use this seam.

How it works — crates/cartos-api/src/connectors/:

The grid::sync handler reads the connector's stored config via gi::get_config once up front, then calls connectors::is_real(&config). If config.service_url is a non-empty string the handler routes to connectors::real_sync(kind, …); otherwise the existing mock feed runs unchanged. The gap is explicit: if a connector has a service_url set but no real adapter is wired yet, real_sync returns a 400 with a clear message rather than silently falling back.

// grid::sync (simplified)

let config = gi::get_config(pool, &ctx.tenant_id, &kind).await?;

if connectors::is_real(&config) {

    return Ok(Json(connectors::real_sync(&kind, pool, &ctx.tenant_id, &config).await?));

}

// … existing mock feeds below …

RealConnector trait (connectors/mod.rs):

pub trait RealConnector {

    async fn sync(

        &self,

        pool: &PgPool,

        tenant_id: &str,

        config: &serde_json::Value,

    ) -> Result<serde_json::Value, ApiError>;

}

Adding a new real adapter = implement RealConnector for a new struct + add one arm to real_sync. No migration, no UI change.

connectors/esri_un.rs — reference implementation:

Queries any ArcGIS Feature/Map Server layer (config.service_url) for its point features as GeoJSON and upserts them into grid_assets. Config keys (set via Admin → Integrations → ⚙):

Key Required Default Notes
service_url yes .../FeatureServer/0 or .../MapServer/0
token no ArcGIS token for secured services
where no 1=1 SQL filter clause
id_field no OBJECTID Attribute used as the external_ref key
name_field no Attribute used as the asset's display name
asset_kind no pole grid_assets.kind; clamped to the allowed CHECK set
max no 500 resultRecordCount cap

Scope today: only point features are mapped (line/polygon geometries are skipped). external_ref is formatted UN-{id}. The sync response includes {source, mode:"real", service_url, fetched, upserted, created, updated}.

cartos-data additions (PR #123):

Operator workflow — flipping a connector mock → real:

  1. In Admin → Integrations, click ⚙ on the target connector (e.g. esri_un).
  2. Set {"service_url": "https://…/FeatureServer/0", "token": "…"} in the config field and save (PATCH /api/grid/integrations/{kind}).
  3. Click Sync on the dashboard SoR chip — grid::sync now detects service_url and runs the real adapter instead of the mock feed.
  4. To revert: clear service_url from the config and sync — the mock feed resumes.

Data layer (crates/cartos-data/src/grid_integrations.rs):

Every function runs inside a set_tenant_session transaction (RLS session vars) and updates the grid_integrations row's status, last_sync, and last_result on completion, so the dashboard chip strip reflects the outcome immediately.

Source files:

crates/cartos-data/src/grid_integrations.rs · crates/cartos-api/src/grid.rs (SoR handlers: integrations, create_integration, update_integration, delete_integration, sync, import_csv) · crates/cartos-api/src/oms_sync.rs (six background schedulers) · cartos-web/src/lib/grid-data.ts (fetchIntegrations, syncIntegration, importGridCsv, createIntegration, updateIntegration, deleteIntegration; CONNECTOR_KINDS / GRID_TARGET_TABLES / INTEGRATION_STATUSES constants; projects layer meta, GridKpis.projects_*) · cartos-web/src/modules/admin/IntegrationsConfig.tsx (Admin → Integrations tab UI) · cartos-web/src/modules/grid/GridDashboard.tsx (SoR chip strip + Import CSV picker) · crates/cartos-data/migrations/0025_grid_integrations_oms.sql · crates/cartos-data/migrations/0026_grid_maximo_sync.sql · crates/cartos-data/migrations/0027_grid_sap_sync.sql · crates/cartos-data/migrations/0028_grid_esri_un_sync.sql · crates/cartos-data/migrations/0029_grid_projects_p6.sql · crates/cartos-data/migrations/0030_grid_msproject_sync.sql · crates/cartos-data/migrations/0031_grid_file_csv.sql · crates/cartos-data/migrations/0032_grid_integrations_unique_kind.sql · docs/samples/grid-work-orders.csv (CSV column template) · crates/cartos-api/src/connectors/mod.rs (RealConnector trait + is_real + real_sync registry) · crates/cartos-api/src/connectors/esri_un.rs (Esri UN reference real adapter).


Source files (all §8.1): cartos-web/src/modules/grid/GridModule.tsx · modules/grid/modules.ts · modules/grid/GridDashboard.tsx · modules/grid/GridDashboard.css · modules/grid/GridModule.css · cartos-web/src/lib/grid-data.ts · crates/cartos-data/src/grid.rs · crates/cartos-data/src/grid_integrations.rs · crates/cartos-api/src/grid.rs · crates/cartos-api/src/oms_sync.rs · crates/cartos-api/src/connectors/mod.rs · crates/cartos-api/src/connectors/esri_un.rs · crates/cartos-agent/src/builtin/grid_kpis.rs · grid_active_outages.rs · grid_open_work_orders.rs · crates/cartos-core/src/catalog.rs (vertical "utility", nine ModuleDef entries) · crates/cartos-data/migrations/0020_enable_grid_modules.sql · crates/cartos-data/migrations/0021_grid_modules_phase2.sql · crates/cartos-data/migrations/0022_grid_modules_reenable.sql · crates/cartos-data/migrations/0023_grid_operational_data.sql · crates/cartos-data/migrations/0025_grid_integrations_oms.sql0031_grid_file_csv.sql · cartos-grid-PRD/ (PRD + Capability Brief — deep-integration roadmap).


8.2 Historic AIS analytics — Maritime module

Status: built and merged to main (PRs #96–#98). Vessel track replay, density heatmap, and port-call analytics are live in the Maritime module via the MaritimeAnalyticsOverlay. The opt-in live AIS ingester (ais_ingest.rs) can continuously append fixes from the VesselFinder feed. MDA (the Cesium iframe module) is a noted gap — it receives live vessels via the iframe bridge but does not yet use the new historic endpoints.

What it is: a portable DockablePanel overlay (features/maritime-analytics/MaritimeAnalyticsOverlay.tsx) mounted in the Maritime module that surfaces three views of the accumulated vessel_positions time-series:

View Description
Tracks Pick a vessel from a dropdown (ordered by fix count in the last N hours); a track polyline is drawn on the map via ais-track-* MapLibre sources/layers. A time scrubber lets the analyst replay the vessel's path step-by-step; a Play button auto-advances the scrubber. The track colour encodes SOG (speed over ground).
Density All vessel fixes in the selected window are binned to a 0.05° cell grid and rendered as a heatmap (ais-density-heat + circle layer) — bright cells indicate high-traffic corridors. Cell size is fixed at 0.05° (configurable in fetchDensity).
Port calls For each seeded port_aoi polygon, the count of distinct vessels and total fixes within it over the window is returned and shown as labelled markers (ais-port-*).

Time window options: 24 h, 48 h, 7 days (168 h). All queries are scoped by the calling user's tenant_id and groups.

Data model (migration 0024_vessel_positions.sql)

Two tables follow the standard Cartos tenant/team-scoped shape:

vessel_positions — append-only AIS time-series:

Column Type Notes
id UUID PK
tenant_id / owning_team TEXT Tenant + group scope
mmsi BIGINT Vessel identifier
name TEXT Vessel name (optional)
ship_type INT AIS ship type code
sog NUMERIC(5,1) Speed over ground (knots)
cog INT Course over ground (degrees)
heading INT True heading
ts TIMESTAMPTZ Position fix timestamp
geom Geometry(Point,4326) WGS 84 position

Indexes: (tenant_id, mmsi, ts) (track queries), (tenant_id, ts) (window scans), (tenant_id, owning_team), GiST on geom (port-call spatial join).

port_aois — named polygon AOIs for port-call detection:

Column Type Notes
id UUID PK
tenant_id / owning_team TEXT
name TEXT Port name (e.g. "Jebel Ali")
geom Geometry(Polygon,4326) Port boundary polygon

Both tables carry the tenant_team_isolation RLS policy. Seeded with 12 vessels × 96 fixes (48 h at 30-min intervals) routed toward three UAE Gulf ports (Jebel Ali, Khalifa Port, Port Rashid) so port-call queries return populated results immediately.

API endpoints (/api/mobility/history/*)

Registered in main.rs alongside the existing /api/mobility/* live feeds:

Method Route Query params Returns
GET /api/mobility/history/vessels ?hours=48 (1–720) VesselSummary[] — distinct vessels (MMSI, name, fix count, first/last seen) sorted by fix count; max 500
GET /api/mobility/history/track ?mmsi=<int>&hours=48 TrackPoint[] — ordered positions (ts, lon, lat, sog, cog, heading) for one vessel; max 5 000
GET /api/mobility/history/port-calls ?hours=48 PortCall[] — per-port distinct vessel count + total fix count within the port_aoi polygon
GET /api/mobility/history/density ?hours=48&cell=0.05 DensityCell[] — (lon, lat, count) binned to cell-degree grid; max 5 000 cells

mmsi is required for the track endpoint; all others are optional. hours clamps to 1–720. cell clamps to 0.005–1.0.

Opt-in live ingester (crates/cartos-api/src/ais_ingest.rs)

maybe_spawn(db) is called from main at startup. It no-ops unless CARTOS_AIS_INGEST=1 is set in the environment, keeping the default deployment quiet. When enabled:

Env var Default Effect
CARTOS_AIS_INGEST (unset) Set to 1 to enable
CARTOS_AIS_TENANT tnt_dev_local Tenant to attribute fixes to
CARTOS_AIS_BBOX 53.0,23.5,57.0,26.5 (UAE Gulf) Bounding box for VesselFinder feed pull
CARTOS_AIS_INTERVAL_SECS 300 (5 min) Poll interval; minimum 30 s

The ingester samples fetch_vesselfinder (same function used by the live Global Mobility feed) and calls ais::insert_positions to append the current fixes into vessel_positions. Over time this builds a real historic dataset that the analytics endpoints can query.

AIS analytics in the MDA Cesium globe (PR #103):

The previously documented gap — "historic AIS not yet surfaced in the Cesium MDA iframe" — is now closed. PR #103 extends the iframe bridge with a cartos.ais.set message and adds a new portable component AisBridgeControls.tsx that surfaces the same three analytics views (Tracks / Density / Port calls) in any Cesium-backed module:

The MDA module now has full feature parity with the native Maritime module for historic AIS analytics, running on the photoreal 3D Cesium globe.

Source files: cartos-web/src/features/maritime-analytics/MaritimeAnalyticsOverlay.tsx · MaritimeAnalyticsOverlay.css · cartos-web/src/features/maritime-analytics/AisBridgeControls.tsx · cartos-web/src/lib/ais.ts · cartos-web/src/lib/iframe-bridge.ts (AisPayload type, bridge.setAis) · crates/cartos-data/src/ais.rs · crates/cartos-api/src/ais.rs · crates/cartos-api/src/ais_ingest.rs · crates/cartos-data/migrations/0024_vessel_positions.sql.


8.3 Cartos Tower, Milepost & Blueprint — Airport, DOT & AEC editions

Status: built and merged to main 2026-07-07, live on their own subdomains on the stakeholder-demo VM — tower./milepost./blueprint.cartosatlas.com (deploy/Caddyfile.prod; see §2). 29 department module surfaces across three editions, a shared config-driven IndustryModule component, per-project/portfolio-wide access-grant RBAC for the two project-scoped editions (Milepost, Blueprint), and real OpenStreetMap data seeded into each edition's flagship surface (air-airtraffic / dot-executive / aec-portfolio — see §9.6). Live operational data + connectors (mirroring the Cartos Grid path in §8.1) are the next increment.

What it is: three more vertical editions of the Cartos engine — the same "one shared component + a config table" pattern that built Cartos Grid, applied to three new verticals in one pass:

Edition Industry Modules Sample geography Scope model
Cartos Tower Airport 10 Dallas–Fort Worth flat — no portfolio/project split
Cartos Milepost Transportation / DOT 10 Indiana Executive Overview (portfolio) + 9 project-scoped surfaces
Cartos Blueprint Infrastructure delivery / AEC 9 Las Vegas Portfolio View (portfolio) + 8 project-scoped surfaces

All three ship from one config file, cartos-web/src/modules/industry/modules.tsTOWER_MODULES, MILEPOST_MODULES, and BLUEPRINT_MODULES (each a Record<string, IndustryModuleConfig> keyed by module id), merged into INDUSTRY_MODULES for route generation + lookup. Each IndustryModuleConfig carries an id (also the route slug + catalog/DB module_id — the air- / dot- / aec- prefixes are retained even though the edition brand is Tower/Milepost/Blueprint), a label, a department HUD badge, a one-line blurb, a map center/zoom, a panels list, 2–4 useCases bullets, sample kpis, and an optional scope: "portfolio" | "project" (unset = flat, as on every Tower module).

The module lineup (from modules.ts — do not hand-maintain elsewhere)

Cartos Tower (Airport) — ten airfield/landside departments, DFW-anchored: Tarmac Lighting (air-lighting), Surface Condition (air-surface), Lane Striping (air-striping), Signage (air-signage), Property Lease (air-lease), Major Projects (air-projects), Emergency Services (air-emergency, ARFF/SMS), Vehicle Traffic (air-vehicle, landside/ramp), Air Traffic (air-airtraffic, airside ops), Weather (air-weather). Flat — no portfolio/project split; every module is its own independent surface.

Cartos Milepost (Transportation/DOT) — an Executive Overview portfolio tab + nine project surfaces, Indiana-anchored: Executive Overview (dot-executive, portfolio — every DOT project on one map, status/health/cost/schedule roll-up by district), then project-scoped: Pavement Management (dot-pavement), Facility Management (dot-facility), Lane Striping (dot-striping), Live Traffic (dot-traffic), Current Roadwork & Obstructions (dot-roadwork), Major Projects (dot-projects), Crew Status (dot-crew), Snow Removal & Treatment (dot-snow), Emergency Response (dot-emergency).

Cartos Blueprint (Infrastructure PM/AEC) — a Portfolio View + eight project surfaces, Las Vegas-anchored: Portfolio View (aec-portfolio, portfolio — every capital project's status/health + assignable view rights), then project-scoped: Design Status (aec-design), Site Selection (aec-site), Constructability Review (aec-constructability), Environmental & Cultural (aec-environmental), Construction Inspections (aec-inspection), Safety Report (aec-safety, HSE), Stakeholder Management & Access (aec-stakeholder), Live Traffic (aec-traffic, construction/haul routes).

Full per-module tables (id / label / department, plus scope for Milepost and Blueprint) are in §7.4.

The IndustryModule surface (cartos-web/src/modules/industry/IndustryModule.tsx)

A lean cousin of GridModule — one shared, config-driven component renders every department in all three editions. Adding a department is a new entry in modules.ts, not a new component. Per instance it renders:

Edition selection (cartos-web/src/lib/edition.ts)

The same Edition type used by Cartos Atlas/Grid now includes tower, milepost, and blueprint. Selection precedence: a VITE_CARTOS_EDITION build flag → a ?edition= query override (persisted in sessionStorage, so a splash link survives the SPA redirect) → the hostname (tower. / milepost. / blueprint.<domain>) → full. Each edition preset carries a brand wordmark, its modules nav lineup (the same ids listed above), a home default route, and a defaultTheme:

Edition Brand Home route Default theme Dev port
tower CARTOS TOWER /air-airtraffic red 5175
milepost CARTOS MILEPOST /dot-executive purple 5176
blueprint CARTOS BLUEPRINT /aec-portfolio desert 5177

editionOrigin(target) derives the URL for the in-app edition switcher (dev: the target's Vite port; prod: <target>.<domain>), so the header switcher and the splash page can link straight to any edition from any other.

Industry project RBAC — per-project & portfolio-wide access grants

Cartos Milepost and Cartos Blueprint are project-scoped editions: an executive sees the whole portfolio, but individual departments/agencies/teams (or specific users) should only see the projects relevant to them. This is the same group concept as docs/group-access-design.md — extended from row-level owning_team scoping to an explicit grants table, because portfolio access needs to be assignable (an admin decision), not just inherited from a Keycloak group membership.

**Data model — crates/cartos-data/migrations/0036_industry_project_access.sql

Table Purpose
industry_projects The portfolio's projects — tenant_id, edition (milepost | blueprint), code, name, status (on_track|at_risk|delayed|complete), health (green|amber|red), lon/lat/zoom. Unique on (tenant_id, edition, code).
industry_project_access Grants: assign a project (project_id) — or the whole portfolio (project_id IS NULL) — to a subject: subject_type group (a dept/agency/team, modelled as a Keycloak group name) or user (a username). access_level is view | edit | admin (0037; shipped view-only in 0036). One grant per (project-or-portfolio, subject) — re-granting updates the level.

Both tables carry the standard tenant_isolation RLS policy (FORCE RLS); this is the second layer — the primary boundary is the query's WHERE clause, per the fabric's convention (docs/group-access-design.md).

Access is default-deny. list_projects (crates/cartos-data/src/industry.rs) returns a project only when the caller is an admin (see_all — any of the three admin tiers), or a grant — portfolio-wide or project-specific — matches one of the caller's groups or their username. Each returned row is annotated with the caller's effective access_level: the strongest matching grant (admin > edit > view), computed in SQL via a CASE max(...) over every matching grant row.

Access levels (0037) and delegation:

Level Grants
view See the project.
edit view + modify the project's status/health (and other mutable fields) via PATCH /api/industry/projects/:id.
admin edit + manage that project's (or portfolio's) access grants — a project-admin can grant/revoke access for the project(s) they administer without being a tenant User-Admin. caller_admin_scope is the delegation check: a portfolio-wide admin grant covers everything in that edition; a project-specific admin grant covers only that project.

API — crates/cartos-api/src/industry.rs, /api/industry/*:

Method Route Access
GET /api/industry/projects?edition= Any authenticated caller — default-deny (server filters to what they're granted; admins see all)
GET /api/industry/access?edition= User-Admin (tenant tier) — full grant list for the edition
POST /api/industry/access User-Admin, or a project-admin delegating within their scope — {edition, project_id?, subject_type, subject_id, subject_label?, access_level?} (idempotent upsert)
DELETE /api/industry/access/:id User-Admin, or a project-admin delegating within their scope
PATCH /api/industry/projects/:id Caller must hold edit or admin on that project (view-only → 403)

edition is a closed set (milepost, blueprint — Tower carries no project-access model, since it has no portfolio/project split) validated in-handler; an unknown edition 400s rather than accepting free text.

UI — two places, one component: features/industry-access/ProjectAccessPanel.tsx lets an admin grant the entire portfolio or a single project to a group (dept/agency/team) or an individual user, pick the access level, and revoke any existing grant (split into "Portfolio-wide" and "By project" lists). It's reused in two places:

  1. In-map, on the Portfolio surface (dot-executive / aec-portfolio) via the "⚙ Manage project access" button, for User-Admins.
  2. User-Admin console — a new "Project Access" tab (modules/admin/ProjectAccessConfig.tsx) with an edition switch (Milepost / Blueprint), for managing the whole tenant's grants in one place without hunting through the map.

Dev seed (0036): sample projects for both editions (e.g. I-465 Reconstruction, SR-37 → I-69 Upgrade for Milepost; Terminal C Expansion, Water Reclamation Upgrade for Blueprint) plus demonstrable grants — the whole Milepost portfolio → city_planning (promoted to admin in 0037), one Milepost project → the restricted user (default-deny demo), Terminal C → maritime_ops (promoted to edit in 0037), the whole Blueprint portfolio → demo.

Deployment — dedicated subdomains

All five Cartos editions (Atlas, Grid, Tower, Milepost, Blueprint) are served by one production build behind one Caddy origin (deploy/Caddyfile.prod) — the app resolves its edition from the request hostname, so there is no per-edition build or deploy. The 2026-07-07 change added three DNS A records (tower. / milepost. / blueprint.<domain>) and three redirect URIs/web origins on the shared cartos-web Keycloak client; login always happens against the Atlas Keycloak realm and redirects back to whichever host started it. See §2 for the live URLs and

Source files: cartos-web/src/modules/industry/modules.ts (TOWER_MODULES / MILEPOST_MODULES / BLUEPRINT_MODULES / INDUSTRY_MODULES), IndustryModule.tsx, IndustryModule.css, IndustryDashboard.tsx, IndustryDashboard.css, IndustryDashboardPage.tsx · cartos-web/src/lib/edition.ts · cartos-web/src/lib/industry-projects.ts (typed API client + editionOfModule) · cartos-web/src/features/industry-access/ProjectAccessPanel.tsx + .css · cartos-web/src/modules/admin/ProjectAccessConfig.tsx · crates/cartos-data/src/industry.rs · crates/cartos-api/src/industry.rs · crates/cartos-data/migrations/0035_enable_industry_modules.sql, 0036_industry_project_access.sql, 0037_industry_access_levels.sql · crates/cartos-core/src/catalog.rs (the 29 ModuleDef entries) · deploy/Caddyfile.prod.


8.4 Cartos Africa — the One Africa edition

Status: merged to main (PR #127) and LIVE at https://africa.cartosatlas.com. Cartos Africa stands alongside the other five live editions on its own sovereign subdomain — a dedicated Caddy vhost + Let's Encrypt cert + Keycloak redirect origin (deploy/Caddyfile.prod, deploy/render-realm.py), hostname → edition detection selects it automatically like every other edition, and it's reachable directly (not only via ?edition=africa). Real data is loaded in prod across all 9 modules and symbology is applied (real-counts table below). africa-parcel now also has a real cadastral ingest — a LADM-aligned parcel + party store (migration 0086, deployed + verified on prod) fed by an Esri Parcel Fabric adapter and a generic ArcGIS FeatureServer adapter — see the parcel-fabric subsection below. The remaining honest data caveat (the flood module's raster/COG/SAR layers are license-flagged references) is still accurate — read it before demoing this edition.

What it is: a sixth Cartos editionCartos Africa, a standalone continental-scale Government edition for African governments, national mapping agencies (NMAs), and ministries. It provides an operational layer over a free/open national base-map substrate — the same "one shared data fabric, one agent, one shell" pattern as every other edition, scoped to a continent instead of a single national government. Driven end-to-end from a prompt package (prompts/map-africa/00-master-prompt.md + 0109 per-module prompts + 10-prd-update.md + a normative 99-data-sources-appendix.md license-posture appendix).

The nine modules

Module id Label Department Notes
africa-command Continental Command Command · continental The roll-up: initiative KPIs, country-onboarding board, cross-module charts (Continental Command dashboard below)
africa-food Food Sustainability & Agriculture Agriculture · food security Cropland extent, crop health, food-security phase tracking
africa-health Health & Immunization Health · immunization Facility inventory, access, campaign micro-planning, cold-chain coverage
africa-border Border Management Security · borders Boundaries, crossing points, corridor monitoring
africa-port Port Management Maritime · ports Port infrastructure, AIS traffic, hinterland corridors
africa-land Land Use & Land Cover Environment · LULC Annual land-cover time series, vegetation classes, change detection
africa-parcel Parcel Fabric Development Land admin · cadastre Esri Parcel Fabric + generic ArcGIS cadastre ingest → LADM-aligned (ISO 19152) parcel + party store (see the subsection below)
africa-flood Flood & Climate Risk Resilience · climate Flood hazard/extent, rainfall, surface water, climate projections
africa-settlement Settlement Management Urban · settlements Settlement extents, population, growth, informal settlements

All nine are config entries in cartos-web/src/modules/africa/modules.ts (AFRICA_MODULES: Record<string, AfricaModuleConfig>, AfricaModuleConfig is a type alias of IndustryModuleConfig — same id/label/department/blurb/ center/zoom/panels/useCases/kpis/optional scope shape as Tower/ Milepost/Blueprint). africa-command renders scope: "portfolio" (a continental view); the other eight are flat (no per-module scope split — the country scope, below, is the cross-cutting equivalent). Registered in crates/cartos-core/src/catalog.rs as nine ModuleDefs (vertical: "government") with a rich shared default feature set (analysis, compare, map-export, dashboard, catalog, satellite, stac, overture, external-overlays, buildings-3d, time-of-day, measure, draw, mapillary, point-cloud) — no new FeatureDef was introduced; Africa modules just opt into features every other edition already has. Migration 0052_enable_africa_modules.sql enables the modules + default features for the dev tenant (new catalog modules are OFF by default; mirrors 0035 for Tower/Milepost/Blueprint).

The AfricaModule surface (cartos-web/src/modules/africa/AfricaModule.tsx) rides the shared IndustryModule component without forking it — it injects edition-specific overlays through IndustryModule's new overlays render-prop ({ mapRef, mapReadyRef, mapEpoch, storageKey, moduleId }): the country-scope boundary/mask layer, the CountryScopePanel selector, and the FoundationLayersPanel, all wrapped in a <CountryScopeProvider> so every Africa module shares one country-scope state. Default theme: Desert (sand & amber dusk — the same palette Cartos Blueprint uses, since Blueprint is also defaultTheme: "desert"; each edition lives on its own subdomain (hostname detection picks the theme per-session), so both being live at once is a cosmetic palette reuse, not a UI collision).

Edition selection

cartos-web/src/lib/edition.ts's Edition type gained "africa". The preset: brand: "ONE AFRICA", home: "/africa-command", defaultTheme: "desert", dev port 5178. Same precedence as every other edition — VITE_CARTOS_EDITION build flag → ?edition= query override → hostname (africa.cartosatlas.com in prod, live). Also selectable in prod via the header edition-switcher pulldown (§9 — shipped 2026-07-18 platform-wide, replacing the old pill row).

Country-scope system — the edition's signature cross-cutting state

The one interaction unique to a continental edition: which countries (and optionally which subnational units) are in scope, filtering every module's map layers, KPIs, dashboards, and agent answers at once.

The foundation-layer fabric

FoundationLayersPanel (features/africa/FoundationLayersPanel.tsx) is the continental imagery/terrain/land-cover/boundary substrate every module rides on — a 🗺️ HUD chip opening a DockablePanel grouped by category (Imagery/Terrain/Land Cover/Buildings & 3D/Transport/Boundaries/Population), driven by a typed registry (lib/africa-data-catalog.ts's FOUNDATION_LAYERS). Each entry carries a license badge — open / view-only / open·nc / licensed / derived — and a mechanism: xyz/wms entries render live (the afr-fnd- raster reconciler adds/removes a MapLibre raster source+layer below the country-scope mask, per-layer opacity slider, prefs persisted in localStorage); reference entries (Overture Buildings, Google Open Buildings 2.5D, Overture Transportation, WorldPop population, Vantor/Maxar premium imagery) are documented upgrades, not rendered here — they'd need a server-side proxy (never a client-keyed premium source). The same renderable subset is also seeded into the standard Layer Catalog as africa-command datasources/map-layers (migration 0054, visibility off by default) so it's admin-configurable and agent-queryable like any other catalog layer, in addition to living in this dedicated panel. Normative license posture (prompts/map-africa/99-data-sources-appendix.md): geoBoundaries, not GADM; GLO-30 DSM is the default terrain story, not FABDEM (FABDEM is non-commercial; a licensed FABDEM+ is offered only as a reference upgrade); Esri imagery is view-only under Esri's ToS; commercial sources are never client-keyed — always a server-proxied datasource.

The Continental Command dashboard

features/africa/AfricaCommandDashboard.tsx — the bespoke dashboard body for africa-command (routed via AfricaDashboardPage.tsx, which gives the eight use-case modules the standard CatalogDashboard instead, same fallback pattern as Tower/Milepost/Blueprint — §9.7). Reads the same cartos.africa.scope sessionStorage key the map/panel write, so the command view reflects whatever the operator has focused on (falls back to the whole continent). Rolls up:

The parcel fabric — cadastral ingest (mig 0086)

Status: merged to main and deployed + verified on prod (migration 0086 applied; live-ingest tested end-to-end against a real public Esri parcel service).

africa-parcel went from a Natural-Earth built-up-extent reference (below) to a real, source-agnostic, LADM-aligned (ISO 19152) parcel + party store — both a real Esri Parcel Fabric and a generic ArcGIS parcel/cadastre FeatureServer map INTO it, so an operator can "go back and forth" between sources on one schema.

Real data — what's actually loaded, and the honest caveats

tools/load_demo_catalog.sh's One Africa section (loads after the Tower/Milepost/Blueprint sections) tags every feature against ADM0/ADM1 as it loads (--tag-country --tag-adm1), so country-scope filtering works on all of it. Symbology (categorical/graduated colour, sized markers, legends) is applied by migrations 0055/0056/0059 (health/port point styling — 0059 fixes a malformed SizeSpec in 0055/0056 that silently broke marker rendering) and 0060 (the five remaining use-case layers). Real, verifiable feature counts as loaded on dev:

Module Dataset Source Real feature count
africa-command ADM0 country boundaries geoBoundaries (CC-BY 4.0) 54 (all AU member states)
africa-command ADM1 priority subnational units geoBoundaries (CC-BY 4.0) 147 (Kenya · Nigeria · Egypt · Ghana · Ethiopia · South Africa)
africa-health Health facilities OpenStreetMap (Nigeria + Kenya) 7,379
africa-port Ports & harbours OpenStreetMap (Nigeria + Kenya + South Africa) 435
africa-food Markets & food supply OpenStreetMap (Nigeria + Kenya) 3,011
africa-land Protected areas / conservancies OpenStreetMap (Nigeria + Kenya) 406
africa-flood Rivers Natural Earth (public domain) 91
africa-flood Lakes Natural Earth (public domain) 33
africa-settlement Settlements & populated places Natural Earth (public domain) 1,285
africa-border Border crossings Curated anchor corridors (public knowledge) 14
africa-parcel Built-up extent — reference only Natural Earth urban areas (public domain) 1,928

Two things this table is deliberately honest about, so nobody mistakes a demo slice for a finished capability:

  1. The builtup_reference row above is a background layer, not the parcel fabric. It's Natural Earth's urban-area polygons, loaded once as a backdrop showing where a parcel fabric roughly sits — it was never meant to be one, and it isn't touched by the real ingest below. africa-parcel now has a separate, real cadastral pipeline: the parcel-fabric subsection above — a LADM-aligned parcel + party store fed by either a real Esri Parcel Fabric or a generic ArcGIS cadastre FeatureServer (migration 0086, deployed + verified on prod). That covers the ingest half of the fabric — an existing authoritative cadastre flowing in. The other half — the SAM2 segmentation pipeline that generates parcel candidates from imagery (prompts/map-africa/07-parcel-fabric-development.md) — is still not built; it remains the module's roadmap item for countries without a digitized cadastre to ingest from.
  2. africa-flood's raster/COG/SAR layers are license-flagged references, not rendered rasters. Migration 0058 seeds two renderable base rasters (Esri World Ocean Base, GLO-30 DSM hillshade — both visibility: off until toggled) plus four reference: true COG placeholders — Digital Earth Africa surface-water (WOfS), Copernicus GloFAS flood hazard, FABDEM+ bare- earth DTM (licensed), and Sentinel-1 SAR (flood-extent mapping) — each correctly attributed and captioned as needing the catalog's COG render path (§9.5) or a licensed/authenticated feed before they can render. Only the rivers/lakes vectors are live today.

Real geographies are pre-fetched for the countries listed above because the public Overpass API was unreachable for an extended stretch during the build (noted in CLAUDE.md); acquired datasets are committed under data/catalog/africa_*.geojson for provenance and reproducible reload, same convention as the other six editions' OSM data (§9.6).

Known gotchas (for the next session that touches this edition)

What's next

The SAM2 segmentation pipeline that generates africa-parcel candidates from imagery (the ingest side — an existing cadastre flowing in — is now built, above); pointing the parcel_fabric adapter at a real ArcGIS Enterprise fabric once a customer has one (it's been verified end-to-end only against the documented schema replayed through a public FeatureServer, not against a live ParcelFabricServer); wiring the foundation panel's reference-only COG/SAR layers to the catalog's COG render path; deeper per-country data beyond the six priority countries (tracked separately, see memory/africa_deeper_country_data_tbd.md); a real country-onboarding tracking table behind the Continental Command KPIs (today representative tiles).

Source files: cartos-web/src/modules/africa/modules.ts, AfricaModule.tsx, AfricaDashboardPage.tsx · cartos-web/src/lib/edition.ts · cartos-web/src/lib/country-scope.tsx, africa-countries.ts, africa-adm1.ts, africa-data-catalog.ts · cartos-web/src/features/africa/AfricaScopeLayer.tsx, CountryScopePanel.tsx, FoundationLayersPanel.tsx, AfricaCommandDashboard.tsx, ParcelFabricPanel.tsx (+ their .css) · cartos-web/src/lib/africa-parcel.ts · crates/cartos-agent/src/builtin/africa_scope.rs · crates/cartos-agent/src/builtin/query_catalog_layer.rs (countries arg) · crates/cartos-data/src/africa_parcel.rs · crates/cartos-api/src/africa_parcel.rs · crates/cartos-data/migrations/0086_africa_parcels.sql · crates/cartos-api/src/catalog.rs (parse_iso3/parse_csv, KPI/features query params) · crates/cartos-data/src/catalog.rs (africa_countries, AfricaCountryRow, FeatureQuery.countries) · crates/cartos-cli/src/loader.rs (--tag-country/--tag-adm1) · crates/cartos-core/src/catalog.rs (the nine ModuleDef entries) · crates/cartos-data/migrations/00520061 · tools/build_africa_countries.py, build_africa_adm1.py · tools/load_demo_catalog.sh (One Africa section) · data/catalog/africa_*.geojson · prompts/map-africa/0010, 99-data-sources-appendix.md · deploy/Caddyfile.prod (africa. vhost) · deploy/render-realm.py (Keycloak redirect/web-origin for the africa host).


8.5 Cartos Guardian — Critical-Infrastructure Protection

Status: merged to main (PR #133/#134) and LIVE at https://guardian.cartosatlas.com (2026-07-22). Documented here for the first time in this guide (it shipped between the 2026-07-18 and 2026-07-26 refreshes). Deployed the same way as every other edition — its own Caddy vhost + Let's Encrypt cert + Keycloak redirect origin, hostname-detected.

What it is: a seventh Cartos editionCartos Guardian — for identifying and protecting UAE civil infrastructure that could be a strike target, with a transparent per-site strike-risk score. Raised 2026-07-22 in response to the UAE infrastructure-strike threat context; covers all UAE, with the immediate view concentrated on the Strait of Hormuz (~56.0°E, 26.0°N). A pure IndustryModule clone (the One Africa pattern — GuardianModule wraps IndustryModule and injects the risk panel through the overlays render-prop; modules live in cartos-web/src/modules/industry/modules.ts). Guardian is the proactive half of a pair with Cartos Sentry (§8.6) — Guardian scores what could be hit, Sentry watches whether it has been.

The five modules

Module id Label Covers
guard-command Command & Risk The roll-up COP (critical_sites, 50 features, union of every domain layer) + the Strike-Risk panel
guard-energy Energy Infrastructure Power plants (incl. Barakah nuclear), transmission/pipelines, petroleum ports (Fujairah/Ruwais/Das/Zirku)
guard-utilities Utilities & Transport Desalination, bridges/tunnels, airports, transport corridors
guard-emergency Emergency Services Police/fire/EMS
guard-defense Defense & Military Military sites (Al Dhafra, Al Minhad, Fujairah Naval…)

Home /guard-command. Default theme Red (Crimson · tactical). Camera centre [56.0, 26.0] zoom 6.6 (the Hormuz view). Every module carries the same rich default feature set: mobility, Overture, External Data Sources, Mapillary, satellite, STAC (the SAR/Sentinel-1 path), plus analysis / compare / map-export / dashboard / catalog / buildings-3d / time-of-day / measure / draw / point-cloud — 16 features, no new FeatureDef needed. Registered in crates/cartos-core/src/catalog.rs as five ModuleDefs (vertical: "critical-infrastructure"); migration 0069 enables the modules

The strike-risk model — the differentiator

Every site carries a transparent 0–100 strike-risk score, computed once offline and baked into the GeoJSON so map, popups, panel, and the agent tool all read the same number (no live re-scoring):

risk = 100 · (0.45 · criticalityN + 0.30 · hormuzExposure + 0.25 · categoryWeight)

hormuzExposure is a haversine proximity to the strait chokepoint (56.45°E, 26.55°N; decays to 0 at 550 km), +0.12 if the site ships via the strait. Tiers: Critical ≥80 / High ≥65 / Elevated ≥50 / Moderate ≥35 / Low. Computed in tools/build_guardian_infra.py; the Fujairah cluster tops the UAE ranking (oil terminal, naval base, power/desal, the ADCOP pipeline terminus — the closest strategic node to Hormuz on the Gulf-of-Oman side). features/guardian/GuardianRiskPanel.tsx (a floating "⚠ Strike Risk" surface) shows a threat summary, a tier bar, ranked Top Targets (click → fly-to + pin), a per-site factor breakdown, and a model explainer; migration 0071 recolours the Command COP layer by risk_tier (crimson→green threat map, radius scaled by risk_score). GuardianRiskKpis (features/guardian/) replaces the placeholder "—" dashboard tiles on every guard-* module (both the in-map dashboard and the full-screen /guard-*/dashboard) with live headline tiles (assessed / Critical / High≥65 / mean / peak / strait-exposed ≤200 km) + a tier bar + a top-6 targets table.

Real data — what's actually loaded

103 curated UAE critical-infrastructure sites across the five modules, each with criticality (1–5), category/subtype/operator/emirate/ hormuz_shipping attributes — curated, not exhaustive Overpass (for critical infrastructure the known strategic targets beat a bulk OSM pull). Layer breakdown: guard-energy/energy_petroleum (22 — power plants + petroleum ports + 3 pipelines), guard-utilities/utilities_transport (15), guard-emergency/emergency_services (11), guard-defense/military_sites (5), guard-command/critical_sites (50, the union COP). Symbology (categorical colour + feature-type glyphs — bolt/flame/droplet/triangle/med-cross/alert — radius graduated by criticality) via migration 0070.

A deeper-data build (also merged + live) added five more capabilities, all license-verified (a Fable-model source deep-dive; OSM data joins on our own osm_id, so ODbL share-alike never touches the produced map):

  1. Grid vulnerabilityguard-energy/grid_network (migration 0072): 1,309 OSM substations + 1,342 HV transmission lines, voltage-weighted risk — 2,651 features.
  2. Strategic vector layers (migration 0073): generation_plants (131 OSM power=plant), bridges_tunnels (827), government_sites (988 — government buildings + embassies/diplomatic missions).
  3. POI collateralguard-command/poi_risk (2,500 soft-targets within 5 km of a critical site, scored sensitivity × proximity × target-risk, including 1,030 fuel/chemical storage tanks as a secondary-hazard layer) — drives a dedicated "POI Risk" tab in the Strike-Risk panel.
  4. Historical imagery overlay (GuardianImageryOverlay) — a NASA GIBS time-slider: MODIS TrueColor + Fire/Burn (dated) and VIIRS Black Marble night-lights (static — a power-outage read).
  5. Live traffic overlay (GuardianLiveOverlay) — reuses the existing /api/mobility/vessels (AIS) and /api/mobility/aircraft (ADS-B) feeds, same-origin, no new key or infra; tankers highlighted.

The agent tool

guardian_strike_risk (crates/cartos-agent/src/builtin/, cross-module, edition-wide like africa_scope) ranks the UAE inventory by the risk model — filters on category/min_tier/min_score/within_hormuz_km/ shipping_only/limit — and returns matched_count + tier_distribution + ranked targets. Backed by cartos_data::catalog::guardian_targets (reads the guard-command/critical_sites union, scope-gated, ranked by risk_score).

Known gotchas

What's next

GEM Global Integrated Power Tracker (CC-BY, email-gated) for authoritative plant data; a Sentinel-2 titiler for 10 m historical imagery; Overture Places POI enrichment; SAR/Sentinel-1 change-monitoring via STAC — this is exactly what Cartos Sentry now does, watching Guardian's highest-risk sites (see §8.6); an aisstream/ADS-B- Exchange procurement upgrade for contract-grade live traffic feeds.

Source files: cartos-web/src/modules/industry/modules.ts (GUARDIAN_MODULES) · cartos-web/src/modules/guardian/GuardianModule.tsx · cartos-web/src/features/guardian/ (GuardianRiskPanel.tsx, GuardianRiskKpis.tsx, GuardianImageryOverlay.tsx, GuardianLiveOverlay.tsx) · cartos-web/src/lib/guardian-risk.ts · crates/cartos-agent/src/builtin/guardian_strike_risk.rs · crates/cartos-data/src/catalog.rs (guardian_targets) · crates/cartos-core/src/catalog.rs (five guard-* ModuleDefs) · crates/cartos-data/migrations/00690074 · tools/build_guardian_infra.py, build_guardian_grid.py, build_guardian_extra.py · tools/load_demo_catalog.sh (guardian section) · data/catalog/guard_*.geojson · guardian-PRD/ (PRD + Capability Brief, build.py → HTML/PDF/DOCX) · deploy/Caddyfile.prod (guardian. vhost) · deploy/render-realm.py (Keycloak redirect/web-origin for the guardian host).


8.6 Cartos Sentry — Reactive Crisis Monitoring

Status: merged to main and LIVE at https://sentry.cartosatlas.com (2026-07-23, still actively shipping features through 2026-07-26 — every item below is browser-verified live). Own sovereign subdomain, same pattern as every other edition (Caddy vhost + Let's Encrypt cert + Keycloak redirect origin, hostname-detected).

What it is: an eighth Cartos editionCartos Sentry — the reactive counterpart to Cartos Guardian (§8.5). Guardian scores what could be hit; Sentry watches whether it has been. Optical + SAR + thermal satellite change detection for critical-infrastructure crisis monitoring: watch Guardian's highest-risk assets → autonomously detect an event (fire / structural damage / flood / blast / spill) → open a scored incident with cross-modality confidence and before/after evidence → report + alert. Raised 2026-07-23 from an SSP concept mockup; PRD scaffolded in sentry-PRD/ (mirrors the Guardian/Cartos Atlas PRD pipeline — build_sentry.py → internal/public × md/html/pdf/docx). ~60% of Sentry reuses shipped Cartos (the Phase-2 change-detection pipeline — job queue, model sidecar, derived datasets — STAC incl. sentinel-1-rtc SAR, the Compare swipe tool, reports, the asset catalog, the IndustryModule shell, measure/draw/annotate/ export/dashboard); four things are genuinely new and described below: (1) a monitoring daemon — the platform's first background ingestion loop, (2) real model-free detectors, (3) an incident state machine + alert dispatch — the platform's first outbound alert channel, (4) commercial tasking connectors.

Ethos: no fake certainty. Every honest limit below is intentional, not an oversight — Sentry ships confidence bands, not invented percentages, and a degraded model never fabricates an incident (the "mock guard," below).

The three modules

Module id Label Covers
sentry-command Crisis Command Watchlist management + AOI pick + Ops-View entry
sentry-detect Change Detection Local/GeoTIFF imagery upload, the change-compare compositor
sentry-incidents Incidents The incident console — alert feed, detail, transitions, tasking, reports

Home /sentry-command. Default theme Camo (field olive, #a3b635 — flipped from an initial Ember/orange during the build per Steven's direction; brand chrome, the AOI-draw tool, and the watchlist/control accents all swept olive; the P1/P2/P3 priority ramp stayed semantic red/amber/grey). Registered in crates/cartos-core/src/catalog.rs as three ModuleDefs (vertical: "crisis-monitoring"); migration 0075 enables the modules for the dev tenant.

1 · The monitoring watchlist + daemon

The daemon's source of truth is an editable monitoring watchlist (sentry_watchlist, migration 0076) — name / asset_key / category / lon / lat / priority (P1/P2/P3) / risk_score / interval_hours / modalities (optical/SAR/thermal) / enabled / owning_team. Seeded from Guardian's top-10 strike-risk sites (ORDER BY risk_score DESC LIMIT 10 against guard-command/critical_sites; priority + scan interval derive from the Guardian tier: risk ≥80 → P1/12h, ≥65 → P2/24h, else P3/72h) — this is the Guardian → Sentry handshake: a strike-risk score becomes a watch priority, which later sizes the tasking spend a P1 incident can defensibly justify. CRUD via /api/sentry/watchlist (tenant + group-scoped) and features/sentry/WatchlistPanel.tsx (inline-editable table: enabled toggle, priority, interval hours, per-modality toggles, risk, delete, click-to-fly, add-at-map-centre, and a relative-time "Scanned" column).

crates/cartos-api/src/sentry_monitor.rsthe platform's first background ingestion loop — is an opt-in tokio interval task (CARTOS_SENTRY_MONITOR=1, mirrors the AIS ingester's pattern) that on each tick finds watchlist rows due for a scan (sentry::due_for_scan, interval_hours elapsed since last_processed_at) and, per due asset+modality: STAC-searches Planetary Computer over a small AOI (crates/cartos-api/src/sentry_screen.rs, ±0.02° box, 60-day window), picks two well-separated epochs (newest = post; earlier, ≥10 days apart = pre; optical filters cloud cover ≤40%), resolves a per-epoch tilejson XYZ URL, and calls the sidecar's change.detect directly (synchronous — no job-queue polling; daemon detections don't persist a derived dataset the way an analyst-run job does). Optical routes to sentinel-2-l2a, SAR to sentinel-1-rtc; thermal is handled separately via FIRMS (below). Dedup: sentry_incident::has_open_incident skips an asset that already has an open incident. A MAX_ROWS_PER_TICK cap bounds the work per tick, and every modality fails independently and gracefully (a transient STAC network error on one source doesn't block the others).

The mock guard. Only a detection whose model_version starts s1- or s2- is trusted as real; a degraded/misconfigured sidecar that would otherwise fall back to the mock detector never raises a fabricated incident. This is the load-bearing honesty check for the whole daemon.

Tunables (prod compose): CARTOS_SENTRY_MONITOR_INTERVAL_SECS (tick period), CARTOS_SENTRY_MONITOR_SCREEN (0 = heartbeat only, no real detection), CARTOS_SENTRY_AOI_HALF_DEG (default 0.02), CARTOS_SENTRY_WINDOW_DAYS (default 60), CARTOS_SENTRY_MIN_CHANGE_PCT (default 5, tuned to 10 in prod to quiet residual coastal noise — see precision hardening below), CARTOS_SENTRY_MAX_ROWS_PER_TICK (default 6), CARTOS_SENTRY_THERMAL_DAYS (1–5, FIRMS NRT's hard cap).

2 · Real model-free detectors

The sidecar's change.detect (model-sidecar/) routes on the STAC collection to two model-free, CPU detectors (numpy + Pillow, no torch — distinct from the Phase-2 SegFormer path used for uploads, below):

Plus FIRMS VIIRS active-fire (crates/cartos-api/src/firms.rs — NASA FIRMS VIIRS_SNPP_NRT 375 m near-real-time area-CSV client, header-mapped parse → GeoJSON, unit-tested). Thermal is a hotspot feed, not a two-epoch comparison — any active fire inside a watched AOI is a candidate regardless of %-change; needs CARTOS_FIRMS_MAP_KEY (degrades gracefully — "no key configured" — without one).

Multi-image baseline masking (optical_change_baseline in s2_change.py) samples a per-cell mean/σ (μ/σ z-score) across several prior epochs to suppress ordinary seasonal/tidal swings before flagging a cell as changed — the false-positive-suppression backbone the precision hardening below builds on. ⚠ GET /healthz still advertises mock-change-0.1 (a static registry string) — the mock only actually fires when neither S1 nor S2 is selected for the collection in play.

3 · Incident state machine + alerts

The reactive core. Migration 0078: sentry_incident + sentry_incident_event (audit timeline) + sentry_alert_channel (webhook config) + sentry_alert (dispatch log). crates/cartos-data/src/sentry_incident.rs implements a validated state machinenew → acknowledged → investigating → resolved | dismissed, with resolved/dismissed reopening back to investigating — plus derive_severity (change % + detection class + watchlist priority → critical/high/medium/low), full CRUD, timeline, channels, and record_alert. Routes (crates/cartos-api/src/sentry_incident.rs): /api/sentry/incidents (+/:id, /transition, /assign, /confirm, /note), /api/sentry/alerts, /api/sentry/alert-channels (create/delete, tech_admin, http(s)-only webhooks). Alert dispatch fires on incident open: in-app is always logged; webhook channels whose min_severity is met get the incident POSTed (non-fatal — a failed webhook is logged, not fatal to the incident). Web: features/sentry/IncidentsPanel.tsx (severity-sorted, open-first alert feed + a detail view with transition buttons, assign, notes, and the audit timeline, camo-accented) on sentry-incidents and sentry-command; a "Raise incident" button appears in the shared Analysis result panel when EDITION === "sentry", turning any analyst-run change.detect result (dominant class, mean confidence, AOI centroid) into an incident.

4 · Fusion confidence bands

The PRD's "no fake 87%." Incidents carry a Low / Medium / High / Confirmed band fused from independent cross-modality corroboration — optical, SAR, and thermal each fail independently, so ≥2 agreeing modalities → High; a lone detection tops out at Medium unless very strong — combined with detector strength (derive_confidence_band, unit-tested, cartos-data). The daemon now collects detections from every configured modality per screen, not just the first hit, storing per-modality detection.evidence alongside the fused band. "Confirmed" is analyst-only (POST /api/sentry/incidents/:id/confirm → sets the band + writes a timeline event). Migration 0079 (confidence_band column). UI: a band chip + a confidence block (per-modality evidence bars + the Confirm button) on the incident detail.

5 · Incident Ops View — the canvas compositor

From an incident, a 🛰 Ops View renders pre/post satellite scenes over the AOI in a canvas compositor (features/sentry/OpsViewPanel.tsx + ImageCompare.tsx) with tabs Optical (O) / SAR (S) / Thermal (T) / Δ Change / 🏚 Damage / ⌆ Coherence. Four compare modes over static AOI crops (the Planetary Computer titiler's item/bbox/{minx,miny,maxx,maxy}.png, CORS * so canvas getImageData works): Swipe (draggable divider), Side-by- side, Overlay (per-side colourise natural/grey/red/blue/green + independent opacity), and Change (per-pixel composite — grey=unchanged, red=appeared/post-brighter, blue=removed/pre-brighter, with a sensitivity control). GET /api/sentry/incidents/:id/imagery?modality= resolves the epoch pair on demand (sentry_screen::resolve_view, reusing the same STAC+tilejson machinery as the daemon); the AOI comes from the incident's stored detection.imagery.bbox (daemon incidents) or the incident point. The Thermal tab resolves a Landsat surface-temperature pair (landsat-c2-l2 lwir11, inferno colormap, 120-day window) through the same compositor, plus FIRMS hotspots as a map overlay with a count and a last-1/3/7/10-day selector (⚠ over a small ±0.02° AOI, Landsat's 100 m thermal resolution reads coarse — a wider AOI resolves more detail).

6 · Commercial tasking connectors

The crisis-hour escalation surface — open imagery is 24–72h latency, so an incident that needs a fresh, higher-resolution look now tasks a commercial vendor over the AOI; the Guardian strike-risk → priority handshake is what makes the spend defensible (a P1 asset justifies a priority order). Migration 0080 (sentry_tasking_order) + crates/cartos-data/src/sentry_tasking.rs + crates/cartos-api/src/sentry_tasking.rs:

Route Purpose
GET /api/sentry/tasking/vendors Vendor registry — Umbra/Capella/ICEYE/Maxar/Planet, real sensor specs + illustrative cost bands
POST /api/sentry/tasking/feasibility {bbox, priority} → per-vendor est. cost (area km² × rate × priority multiplier — P1 1.5× / P2 1.0× / P3 0.75×, with a min-order floor) + earliest-collect window (rapid/standard × priority)
POST /api/sentry/tasking/search {bbox, days} → a live bounded crawl of Umbra's free open-data STAC archive
GET/POST /api/sentry/tasking/orders List / place a cost-visible order — records intent, linked to the incident timeline
POST /api/sentry/tasking/orders/:id/status Advance the order lifecycle

Web features/sentry/TaskingPanel.tsx (AOI from the viewport or an incident, P1/P2/P3 selector, reactive vendor cost cards, an open-archive search with thumbnails, and the order log) on sentry-command/sentry-detect/ sentry-incidents; a "🛰 Task imagery" button on the incident detail hands over the AOI + severity-derived priority. Honest by design: the credentialed vendors (Capella/ICEYE/Maxar/Planet) are reported as "requires vendor API credentials," never fabricated; an order records intent, it does not fly a collect; cost figures are illustrative anchors, consistent with the cartos-pricing convention. Umbra's open archive is a deep year → month → day → item tree with no spatial extent on interior nodes, so the crawl uses a recency-first depth-first search (date-sorted, newest-first, a 90-fetch/ 12-second budget) plus a live proof-of-life sample of the latest collects (unfiltered by AOI, clearly labelled) — the archive's ~20 sites rarely cover the Gulf demo AOIs, so the sample keeps the panel honestly "live" without pretending coverage that isn't there.

7 · InSAR coherence context layer

Done the infra-fit way, not the expensive way: not a live coherence-loss detector (that needs SLC imagery + SNAP/GPU, or an async HyP3/Earthdata connector — the documented v2 path) but a pre-computed expected-coherence context layer from the open Global Seasonal Sentinel-1 Coherence dataset (Kellndorfer et al., AWS sentinel-1-global-coherence-earthbigdata, seasonal 2019–2020 median, 3 arcsec). Sidecar POST /render/coherence crops the global VRT over the AOI via GDAL /vsicurl + -projwin (fetches only the AOI's bytes) → mean/median/low-fraction stats + a colourised RGBA PNG (red = decorrelated, green = coherent). GET /api/sentry/incidents/:id/coherence picks the season from the AOI's hemisphere + month and adds a High/Moderate/ Low interpretation. The ⌆ Coherence Ops-View tab drapes the PNG over Esri. It qualifies the amplitude detectors — change over normally-coherent ground is more likely real; change over already-decorrelated ground is more likely noise — it is a surface-stability layer, not a live-event detector, and bridges into the precision hardening below (the principled path to a sand mask, since NBR/NDWI/NDVI genuinely can't separate bare sand from built surfaces).

8 · Thermal colour ramps + change-over-Esri

Two Ops-View refinements: (a) selectable thermal colour ramps (inferno/magma/plasma/viridis/turbo/hot/jet/rainbow) via a colormap query param on the imagery endpoint, a 🎨 Ramp selector in the T tab. (b) "⧉ Change over Esri" for SAR/optical/thermal — sidecar POST /render/change-raster derives per-pixel appeared(red)/removed(blue) rasters from a tab's pre/post AOI crops as two georeferenced RGBA PNGs; GET /api/sentry/incidents/:id/change-raster?modality= resolves the crop URLs; a toggle drapes both over the high-res Esri basemap with independent appeared/removed transparency sliders.

9 · Local + GeoTIFF/COG imagery upload → change.detect

An analyst uploads their own before/after scenes and compares them in the same compositor as the satellite Ops View. POST /api/sentry/uploads (raw body, tenant-scoped, reuses the Field media seam) for plain images (PNG/JPEG/WebP) — features/sentry/LocalImageryPanel.tsx on sentry-detect and sentry-command. Georeferenced GeoTIFF/COG uploads go through the sidecar POST /render/raster-preview (GDAL CLI: gdalinfo -json for the WGS84 extent bounds, gdal_translate -scale for a 1024 px web PNG — no Python GDAL bindings needed, CLI only) → POST /api/sentry/geo-uploads (80 MB) stores the rendered PNG + returns {url, bbox}. Uploaded imagery also feeds real SegFormer change detection (POST /render/local-change, the Phase-2 model, base64) — for a georeferenced pair the change cells (appeared=red, removed=blue) drape over Esri on the map at their real AOI and can raise a genuinely georeferenced incident. ⚠ Added a persistent media-data volume (/data/media, CARTOS_MEDIA_DIR) in prod — cartos-api previously had no media volume, so uploads (and Field media) were lost on container recreate; see memory/prod_deploy_postgres_safety.md.

10 · xBD damage assessment

Per-building 4-class damage (no-damage / minor / major / destroyed) over an incident's AOI. Honest by design: this is a change-magnitude classifier, not a trained xView2 CNN (a real xBD model needs the xBD dataset, a GPU, and sub-metre imagery; Sentinel's 10 m resolution is triage-grade, not building- grade). Sidecar POST /render/damage-assess fetches the pre/post AOI crop, samples each building footprint's change, and classifies it; GET /api/sentry/incidents/:id/damage pulls the footprints from the overture-sidecar (/overture/buildings?bbox=) and runs the assessment. The 🏚 Damage Ops-View tab colours buildings by class over Esri with per-class counts.

11 · Precision hardening — killing the two documented false-positive sources

The daemon's first prod runs autonomously raised two incidents ("Surface change · AUH" 34% and "Burn scar · Das Island" 27.8%) that were almost certainly false positives — S2 NBR/NDWI swings 25–35% between two dates over heterogeneous coastal AOIs (water + sand + built) with no real event, and Gulf gas flares light up FIRMS across nearly every energy asset. Two masks, tuned against live data:

Prod also tuned CARTOS_SENTRY_MIN_CHANGE_PCT from 5 → 10 once baseline masking landed.

12 · Sentry operations dashboard

features/sentry/SentryDashboard.tsx (ECharts, camo-themed) — Sentry has no catalog layers, so it doesn't fall back to the generic CatalogDashboard like Tower/Milepost/Blueprint do; it's driven by the real Sentry data plane (incidents/watchlist/alerts/tasking orders): a KPI strip (open incidents + critical/high, confirmed, assets watched + enabled, P1 assets, scans overdue, alerts sent, tasking spend), an incidents-raised-over-14-days line, severity and confidence-band donuts, a status funnel, detection-modality and watchlist-priority bars, a recent-incidents table (severity/band/status/age), and an alert-dispatch log. SentryDashboardPage.tsx wraps the shared DashboardPage; routed for every sentry-* module.

13 · Incident report/export

One-click auditable record from the incident detail: 📄 Report opens a self-contained, print-ready styled HTML report (dark camo + a print media query) and DOCX ↓ downloads the same via the sidecar's /render/markdown-docx (reusing the M02 §13.2 render path). sentry_report.rs renders header, severity + confidence-band chips, detection metrics, a per-modality evidence table, KPIs, and the full audit timeline. The report embeds the actual pre/post AOI crop thumbnails (built from the incident's stored tile URL + bbox, not just dates) — HTML as <img> tags, DOCX via an extended sidecar build_markdown_docx that resolves ![alt](url) lines to add_picture. GET /api/sentry/incidents/:id/report.{html,docx} (tenant + group scoped, read-only — no migration).

HTTP API reference

Method Route Purpose
GET/POST /api/sentry/watchlist List / add a monitored asset
PATCH/DELETE /api/sentry/watchlist/:id Edit / remove a monitored asset
GET/POST /api/sentry/incidents List / open an incident
GET /api/sentry/incidents/:id Incident detail
POST /api/sentry/incidents/:id/transition Advance the state machine
POST /api/sentry/incidents/:id/assign Assign to an analyst
POST /api/sentry/incidents/:id/confirm Analyst-only — set confidence band to Confirmed
POST /api/sentry/incidents/:id/note Add a timeline note
GET /api/sentry/incidents/:id/imagery?modality= Resolve the Ops-View pre/post pair for O/S/T
GET /api/sentry/incidents/:id/damage xBD-style per-building damage classes
GET /api/sentry/incidents/:id/coherence Expected-coherence context layer
GET /api/sentry/incidents/:id/change-raster?modality= Per-pixel appeared/removed raster
GET /api/sentry/incidents/:id/report.html | .docx Auditable incident report
GET /api/sentry/tasking/vendors Commercial-tasking vendor registry
POST /api/sentry/tasking/feasibility Per-vendor cost + earliest-collect estimate
POST /api/sentry/tasking/search Live Umbra open-archive search
GET/POST /api/sentry/tasking/orders List / place a tasking order
POST /api/sentry/tasking/orders/:id/status Advance an order's status
GET /api/sentry/alerts Alert dispatch log
GET/POST /api/sentry/alert-channels List / create a webhook alert channel — tech_admin
DELETE /api/sentry/alert-channels/:id Remove a channel — tech_admin
POST /api/sentry/uploads | GET /api/sentry/uploads/*key Upload / serve a local before/after image
POST /api/sentry/geo-uploads Upload a georeferenced GeoTIFF/COG → rendered PNG + bbox
POST /api/sentry/detect-local Run change detection over an uploaded pair

Migrations 00750080: 0075 enables the three modules; 0076 sentry_watchlist; 0077 adds last_processed_at + sentry_scan; 0078 sentry_incident/sentry_incident_event/sentry_alert_channel/sentry_alert; 0079 confidence_band; 0080 sentry_tasking_order.

Honest limits (read before demoing this edition)

Known gotchas

Source files: cartos-web/src/modules/sentry/ (SentryModule.tsx, SentryDashboardPage.tsx) · cartos-web/src/features/sentry/ (SentryControl, WatchlistPanel, IncidentsPanel, OpsViewPanel, ImageCompare, LocalImageryPanel, TaskingPanel, SentryDashboard + their .css) · cartos-web/src/lib/sentry.ts · crates/cartos-api/src/sentry.rs, sentry_monitor.rs, sentry_screen.rs, sentry_incident.rs, sentry_tasking.rs, sentry_report.rs, sentry_upload.rs, firms.rs · crates/cartos-data/src/sentry.rs, sentry_incident.rs, sentry_tasking.rs · model-sidecar/s1_change.py, s2_change.py, main.py (/render/coherence, /render/change-raster, /render/raster-preview, /render/damage-assess, /render/local-change) · crates/cartos-core/src/catalog.rs (three sentry-* ModuleDefs) · crates/cartos-data/migrations/00750080 · sentry-PRD/ (PRD + build_sentry.py) · deploy/Caddyfile.prod (sentry. vhost) · deploy/render-realm.py (Keycloak redirect/web-origin for the sentry host) · docker-compose.prod.yml (MODEL_SIDECAR_URL, CARTOS_SENTRY_MONITOR*, CARTOS_FIRMS_MAP_KEY, the /data/media volume).


8.7 Cartos Harvest — Agriculture Intelligence

Status: merged to main, tagged cartos-harvest-v1 (2026-07-28) — deployed 2026-07-29 and LIVE at https://harvest.cartosatlas.com. The harvest branch (scoping + Phase-0 skeleton + prompts 01–07 crop-pipeline slices + de-branding + H1–H4 depth passes) is merged, and Harvest now has its own Caddy vhost, Keycloak redirect origin and subdomain like every other edition in this guide. It is also reachable from within Cartos Africa. Every capability below is verified against real Kenya/Nigeria Sentinel-2 imagery — it can equally be run locally with ?edition=harvest (see §2). Scoping doc: docs/harvest-ai-scoping.md. Buildable prompt suite: prompts/harvest/ (mirrors prompts/map-africa/).

What it is: a ninth Cartos editionCartos Harvest (internally "HarvestAI") — STAC-native crop intelligence: crop type classification, crop health monitoring, yield forecasting, water-stress/drought detection, field-boundary delineation, and agri-risk/finance analytics, for ministries of agriculture, agri-insurers/lenders, and extension services. The scoping doc is explicit that this is a platform, not a map layer: "HarvestAI should be designed as a production-grade agricultural intelligence platform, not merely as a map layer or an imagery viewer." It reuses the Cartos Africa country-scope + foundation-layer fabric rather than rebuilding it (a CountryScopeProvider wraps every Harvest module; HarvestModule.tsx injects AfricaScopeLayer/CountryScopePanel/FoundationLayersPanel/ AvailableHerePanel from features/africa/ verbatim) and rides the same config-driven IndustryModule surface as every industry edition since Tower/Milepost/Blueprint. Anchor countries (Phase-0 pilot AOIs): Kenya and Nigeria — already seeded in the Africa edition's country-scope.

The seven modules

Module id Label Covers
harv-command Harvest Command Cross-country food-security overview, crop-calendar status by country/season, country onboarding, cross-module alerts roll-up
harv-croptype Crop Type & Land Use Multi-temporal crop-type classification (seasonal phenology), parcel-level class + confidence, cropland extent/LULC context
harv-health Crop Health NDVI/NDRE/NDWI/NDMI time series, anomaly detection vs a multi-year baseline, stress alerts + flag-for-field-visit
harv-yield Yield Forecast Season-NDVI biomass/yield-proxy regression with inter-annual-CoV uncertainty, driver attribution, an illustrative t/ha readout
harv-water Water & Drought NDMI/NDWI moisture-anomaly water-stress + drought-severity detection; ET (WaPOR)/rainfall (CHIRPS) as reference layers; UAE greenhouse/CEA anchor (planned)
harv-boundaries Field Boundaries & Registry gdal_polygonize candidate field-boundary delineation → human-in-the-loop review → the authoritative parcel fabric; a farmer registry (name/national-id/phone/cooperative/crops/region, PII in-scope-only) linked via field_parcels.owner_ref
harv-risk Risk & Finance Composed 0–100 portfolio agri-risk score (yield outlook + in-season stress + inter-annual variability) with driver attribution + an index-insurance parametric trigger

Home /harv-command. Default theme Verdant (#7cc945 — "cropland green & chlorophyll", a new palette in lib/themes.ts distinct from the tactical Emerald/the Africa Savanna gold). Module-id prefix harv- (disjoint from grid-/air-/dot-/aec-/africa-/guard-/sentry-). Map-overlay layer prefix hrv- (hrv-anom-, hrv-cls-, hrv-yield-, hrv-water-, hrv-parcel-, hrv-risk-, hrv-flags-). Registered in crates/cartos-core/src/catalog.rs as seven ModuleDefs (vertical: "agriculture", industry_of_vertical("agriculture") → "harvest"); the platform-level agriculture placeholder ModuleDef these supersede is now dead (no route — see §7.4) and the disabled "Agriculture" nav placeholder was removed from ModuleNav.tsx in favour of the seven real harv-* nav entries. Migration 0082 enables the seven modules + the platform's full default feature set (analysis, compare, map-export, dashboard, catalog, satellite, stac, overture, external-overlays, buildings-3d, time-of-day, measure, draw, mapillary, point-cloud) for the dev tenant, and grants the harvest edition via tenant_editions (the Access-tab machinery from §7.4) — no new FeatureDefs, Harvest reuses the existing capability catalog end to end.

The STAC crop pipeline — the moat

The scoping doc is normative about the engineering position: crop type is a temporal + spectral problem, not a resolution problem (classify from phenological signatures over a season, not single-date RGB); STAC/COG is the authoritative AI layer, tiles are UX only; and every output carries source + date + confidence — no fabricated certainty, the same discipline as Guardian's strike-risk score and Sentry's confidence bands. Concretely, the Rust API (crates/cartos-api/src/harvest.rs, 1,062 lines) does live STAC orchestration over Microsoft Planetary Computer's public, keyless STAC + titiler (https://planetarycomputer.microsoft.com/api/{stac,data}/v1) — sentinel-2-l2a for all seven slices below — and the model-sidecar (model-sidecar/main.py) does the numpy/GDAL compute. Seven pipeline slices, each its own /api/harvest/* handler + panel:

  1. crop-index (GET /api/harvest/crop-index) — STAC-searches for the least-cloudy Sentinel-2 L2A scene over the AOI + date window and returns a keyless, CORS-enabled Planetary Computer titiler URL that band-maths a vegetation index on the fly via the expression param (no raw-COG read on our side, no client key): NDVI (B08-B04)/(B08+B04), NDRE (B08-B05)/(B08+B05) (red-edge/nitrogen), NDWI (B03-B08)/(B03+B08) (canopy/surface water), NDMI (B08-B11)/(B08+B11) (vegetation moisture). All are ratios → scale-invariant, no reflectance scaling needed. Rendered in CropTemporalPanel.tsx as a MapLibre image source with a date/index scrubber — the temporal-context view every other slice builds on.
  2. crop-type (GET /api/harvest/crop-type) — fetches a season of grayscale NDVI AOI crops (single-band PNGs, rescale −1..1) and posts them to the sidecar's POST /render/crop-classify, which builds a per-pixel NDVI time series and, as of v0.2, KMeans-clusters pixels by the shape of their whole time-series (bounded 2–6 clusters, each labelled by its member pixels' mean peak-NDVI + amplitude — a step up from v0.1's flat per-pixel thresholds, since two pixels with the same peak but different seasonality now land in different clusters) into five classes (bare/built/water, sparse veg/grassland, mixed/low-vigor crop, annual cropland — the strong seasonal NDVI swing is the hallmark, dense/ perennial veg); model_version: "harvest-phenology-kmeans-0.2", with a graceful fallback to the v0.1 rule-based thresholds (model_version: "harvest-phenology-baseline-0.1") if scikit-learn is unavailable or the AOI has too few valid pixels. Both are the honest label-free baseline the scoping doc prescribes; a supervised classifier scaffold (model-sidecar/crop_classify_gpu.py + train_crop_classifier.py + requirements.gpu.txt — a Prithvi/Clay fine-tune seam, MODEL_GPU-gated) now exists but is unverified on the CPU dev box, not wired into the default handler — see "What's next". CropTypePanel.tsx renders the class PNG + a legend + per-class fraction/mean-peak-NDVI breakdown.
  3. crop-health (GET /api/harvest/crop-health) — the target month's NDVI vs the same month's NDVI across up to N prior years (a real multi-year baseline, not a fixed threshold), diffed by the sidecar's POST /render/ndvi-anomaly into a diverging red(stressed)→pale→ green(vigorous) map plus stressed_frac/vigorous_frac. CropHealthPanel.tsx overlays it and adds a lightweight "flag for field visit" — drops a marker (hrv-flags-*) for ground-truthing, the honest bridge to Cartos Field.
  4. crop-yield (GET /api/harvest/crop-yield) — season-mean NDVI (an established biomass proxy) relative to the prior-year mean, via POST /render/yield-proxy; uncertainty is the inter-annual coefficient of variation of the regional baseline mean (not per-pixel noise, which would blow the band up), clamped to a credible 5–35% CoV range. An optional illustrative t/ha reference per crop (Africa smallholder, FAOSTAT-order, explicitly not a quote) scales the index. YieldPanel.tsx shows the map, the area-index (expected-vs-normal %), and the uncertainty band; model_version: "harvest-yield-viproxy-0.1".
  5. crop-water (GET /api/harvest/crop-water) — reuses the same index-agnostic anomaly differ against NDMI/NDWI instead of NDVI: a below-baseline moisture anomaly is the water-stress/drought signal, with a drought-severity tier derived from the dry-area fraction. WaterPanel.tsx; ET (WaPOR) and rainfall (CHIRPS) are foundation-layer reference rows, not yet wired into the score; SPI/VHI and a UAE greenhouse/CEA anchor are documented follow-ups.
  6. field-candidates (POST /api/harvest/field-candidates) — the biggest departure from a pure raster render: fetches a season of NDVI crops, posts to POST /render/field-segment, which classifies the seasonal-phenology cropland mask then vectorises contiguous cropland via GDAL's gdal_polygonize into candidate field polygons (area_ha + centroid peak-NDVI), and persists them into the new field_parcels table (migration 0084) as status='candidate'. FieldBoundariesPanel.tsx shows the map + a review queue — PATCH /api/harvest/parcels/:id promotes a candidate to approved or rejected, the human-in-the-loop step that turns a label-free baseline into the authoritative parcel fabric. GET /api/harvest/parcels (+ /summary) reads the fabric back, ?bbox=-filterable.
  7. portfolio-risk (GET /api/harvest/portfolio-risk) — composes three drivers from the above into a transparent 0–100 agri-risk score, explicitly modelled on Guardian's strike-risk formula:
    score = 100 · (0.45 · yield_risk + 0.35 · stress_risk + 0.20 · variability_risk)
    where yield_risk comes from the yield-proxy area-index, stress_risk from the NDVI-anomaly stressed fraction, and variability_risk from the yield-proxy's inter-annual CoV — each clamped 0–1 against a credible band. Tiers mirror Guardian's (Critical ≥80/High ≥65/Elevated ≥50/Moderate ≥35/Low). A transparent, auditable index-insurance parametric trigger fires when biomass < 80% of normal OR in-season stress > 25% of area — index_trigger.reason explains which. RiskPanel.tsx colours each field parcel by a per-parcel risk (the AOI base score modulated by that parcel's own NDVI vigor) and shows the driver breakdown + trigger status. This is portfolio risk analytics for insurers/lenders/ministries, explicitly NOT personalised financial advice (the same "no fabricated certainty" discipline as Sentry).

All seven POST/GET handlers share s2_search (STAC POST to /search), parse_bbox (rejects degenerate or >2° AOIs — a titiler/STAC cost guard), and date_window (YYYY-MM → whole month, YYYY-MM-DD → ±15 days, absent → last 45 days). Data layer: crates/cartos-data/src/harvest.rs (list_crop_calendars, season_stage, insert_field_candidates, parcel read/patch helpers) — same tenant + owning_team scoping convention as every other module.

Persisted products (M03) — the Saved Products panel

Every analytics run (crop-type, crop-health, crop-yield, crop-water, portfolio-risk) has a "Save to products" option; when ticked, the shared maybe_save helper (harvest.rs) persists the whole result body as a lineage-tracked derived_dataset row via the same M03 store every other edition's saved analyses use (§13.1 — reuses migration 0013, no new migration), naming it from the AOI centroid + target date and stamping tool_ref (crop_classify / ndvi_anomaly / yield_proxy / moisture_anomaly / portfolio_risk) and tool_version (the handler's model_version, e.g. harvest-phenology-kmeans-0.2). GET /api/harvest/products?bbox=&kind= lists them newest-first (PNG bytes stripped for the list view — see strip_png), scoped by tenant + owning_team; GET /api/harvest/products/:id returns one in full (incl. the rendered PNG). The Saved Products panel (SavedProductsPanel.tsx) browses the list with per-product headline stats (top crop-type class, mean NDVI-anomaly delta, yield-vs-normal %, drought severity, risk score/tier) and is mounted alongside the other harvest panels in HarvestModule.tsx. The harvest_products agent tool reads the identical store, so "what's the latest yield forecast we ran?" answers from what was actually saved, not a live re-run.

Crop-calendar / agro-ecological-zone model

crop_calendars (migration 0083) makes every module season-aware: country + agro-zone + crop rows carry plant_start/plant_end/peak/ harvest_start/harvest_end (months 1–12, cyclic — a window may wrap year end), a season_label (e.g. "Long rains", "Short rains"), and a source (seeded 'FAO GIEWS'). Seeded for the two Phase-0 anchors: Kenya (6 rows — long/short-rains maize, Rift Valley wheat, Mwea-scheme irrigated rice, semi-arid sorghum, highland beans) and Nigeria (6 rows — early/main-season maize, Fadama wet-season rice, Sudan-savanna sorghum, Sahel millet, perennial cassava). GET /api/harvest/crop-calendar?country=&crop= returns each row annotated with its current phenology stage (planting/vegetative/ harvest/off-season) computed from today's month — the same stage math backs both the crop_calendar agent tool and the HarvestCommandDashboard's season-aware KPI tiles and per-country stage board.

Harvest Command — the dashboard

harv-command gets a bespoke dashboard (HarvestCommandDashboard.tsx, not the generic CatalogDashboard) — food-security + crop-intelligence KPIs, a country-onboarding board, and ECharts charts, all driven live by /api/harvest/crop-calendar (reusing AfricaCommandDashboard.css's hero/ grid/board chrome for visual continuity with Continental Command). Every figure reflects the current month's phenology; crop-type/health/yield map tiles are marked pending until the STAC pipeline lands data for a given country (Phase-0 anchors only cover Kenya + Nigeria so far).

Foundation-layer catalogue

lib/harvest-data-catalog.ts follows the exact lib/africa-data-catalog.ts pattern — a typed registry rendered by the reused FoundationLayersPanel, grouped by category with license badges: Imagery (Esri World Imagery view-only, Sentinel-2 Cloudless EOX CC-BY-NC-SA — flagged open-nc/never embed, Sentinel-2 L2A + Sentinel-1 GRD SAR as STAC references), Indices (MODIS NDVI 16-day via GIBS, the derived NDVI-anomaly layer), Cropland (DE Africa Cropland Extent 10m CC-BY, ESA WorldCereal 10m CC-BY, the derived crop-type layer), Land Cover (ESA WorldCover 10m CC-BY, Esri/Impact Observatory Annual LULC view-only), Soils (iSDAsoil Africa 30m, SoilGrids 250m, both CC-BY), Water & Rainfall (FAO WaPOR v3 ET, CHIRPS rainfall — both open), Terrain (Esri World Hillshade over Copernicus GLO-30 DSM, OpenTopoMap contours), Boundaries (geoBoundaries ADM0/ADM1 — not GADM, same license posture as Africa), Population (WorldPop 100m CC-BY). The Africa edition's AvailableHerePanel ("Available Here" georeference-aware registry — see §8.4) is mounted verbatim too, so a country/zoom-narrowed candidate-source list surfaces automatically without any Harvest-specific registry work.

Farmer registry — completes harv-boundaries

farmer_registry (migration 0085) is a tenant-scoped table of farmers/ land-holders (name, national_id, phone, gender, cooperative, crops[], region, owning_team) — the PII fields (name/national-id/phone) are served only to in-scope callers, same convention as everywhere else in the platform. field_parcels.owner_ref (mig 0084) is the join that turns a delineated field polygon into an operational unit for extension, insurance, and subsidy workflows. Endpoints: GET/POST /api/harvest/farmers (list — optional q name/national-id search — and register), POST /api/harvest/parcels/:id/owner (link a parcel to a farmer, from the Field Boundaries review panel), GET /api/harvest/registry/coverage (farmer count, parcels owned, coverage %). FarmerRegistryPanel.tsx is the UI: search, register (with a crops comma-list input), and a coverage strip; it's mounted in harv-boundaries alongside FieldBoundariesPanel.tsx.

The agent tools

Five edition-wide tools (module() returns None, same pattern as africa_scope/guardian_strike_risk), all in crates/cartos-agent/src/builtin/harvest.rs:

Data & license posture

Same license-class convention as Africa (open/open-nc/view-only/ commercial), with the same two landmines flagged: geoBoundaries, never GADM (non-commercial) for boundaries; Copernicus GLO-30 DSM, never FABDEM (non-commercial) as the default terrain. Decision 2026-07-27 (locked in the scoping doc): ship Phase 0/1 fully open — Sentinel-1/2, DE Africa, WorldCereal, WaPOR, CHIRPS, WorldCover, geoBoundaries, GLO-30 — no commercial imagery licensed yet; Planet/Vantor/Airbus are a future opt-in with the server-side datasource-proxy seam (already shipped for Africa — §8.4) wired but nothing keyed. GPU: local GPU is available and targeted for the Phase-1 foundation-model classifier. A supervised-classifier scaffold now exists (model-sidecar/crop_classify_gpu.py — a Prithvi multi-temporal crop-classification fine-tune seam, MODEL_GPU-gated, defaults to a public HuggingFace checkpoint that's domain-mismatched for Africa until fine-tuned on local labels via train_crop_classifier.py + requirements.gpu.txt) but is unverified on the CPU dev box — not a shipped capability, a documented depth follow-up. The v0.2 KMeans temporal-phenology clustering (§ above) stays the graceful, label-free fallback (crop_classify_gpu.available() gates the GPU path; unset falls back to KMeans).

Relationship to Cartos Africa — africa-food

Cartos Africa already ships an africa-food module (cropland extent, crop health, soils, IPC food-security phase tracking — see §8.4). The scoping decision: Harvest owns the deep crop-ML stack; africa-food stays a light food-security teaser. Migration 0082 includes a commented-out module_industries cross-deploy block (harv-croptype/harv-healthafrica) using the editions-as-first-class machinery from §7.4 — held deliberately until the crop modules carry real data beyond the two pilot countries, so an empty module doesn't show up in the Africa nav. Do it from the Access tab once ready, or uncomment the block.

Known gotchas (for the next session that touches this edition)

What's next

Real deploy (own subdomain, Caddy vhost, Keycloak redirect origin, migrate + data load in the sequence every other edition has learned the hard way — see the gotchas in §§8.4–8.6); verify the supervised Prithvi/Clay crop classifier scaffold on a CUDA host and fine-tune on local labels (the honest follow-up to the v0.2 KMeans phenology baseline, called out in every relevant model_version string); the UAE greenhouse/controlled-environment anchor + SPI/VHI drought indices for harv-water; per-AOI-product agent tools (today's tools are calendar/fabric/registry/products-catalog — a crop_index/crop_health agent tool over a named AOI is the natural next slice, matching query_catalog_layer's pattern); and the held Africa cross-deploy once crop coverage broadens past Kenya/Nigeria.

Source files: docs/harvest-ai-scoping.md · prompts/harvest/ (master + 7 build prompts + PRD-update + data-source appendix) · cartos-web/src/modules/harvest/ (modules.tsHARVEST_MODULES, HarvestModule.tsx, HarvestDashboardPage.tsx) · cartos-web/src/features/harvest/ (CropCalendarPanel.tsx, CropTemporalPanel.tsx, CropTypePanel.tsx, CropHealthPanel.tsx, YieldPanel.tsx, WaterPanel.tsx, FieldBoundariesPanel.tsx, FarmerRegistryPanel.tsx, RiskPanel.tsx, SavedProductsPanel.tsx, HarvestCommandDashboard.tsx + harvest-panels.css) · cartos-web/src/lib/harvest.ts (fetch client) · cartos-web/src/lib/harvest-data-catalog.ts (foundation-layer registry) · crates/cartos-api/src/harvest.rs (17 route handlers) · crates/cartos-data/src/harvest.rs · crates/cartos-agent/src/builtin/harvest.rs (CropCalendar, HarvestCountryProfile, ParcelFabricSummary, HarvestProducts, FarmerLookup) · crates/cartos-core/src/catalog.rs (seven harv-* ModuleDefs, harvest IndustryDef) · crates/cartos-data/migrations/00820085 · model-sidecar/main.py (/render/crop-classify, /render/ndvi-anomaly, /render/yield-proxy, /render/field-segment) · model-sidecar/crop_classify_gpu.py, train_crop_classifier.py, requirements.gpu.txt (unverified GPU scaffold) · cartos-web/src/lib/edition.ts (harvest preset) · cartos-web/src/lib/themes.ts (verdant theme) · crates/cartos-agent/src/llm.rs (ContentBlock::Thinking/RedactedThinking — the Opus 5 fix, engine-wide not Harvest-specific).


9. Cross-cutting features every module inherits

9.1 Agent intelligence surface — Cartos overhaul

Status: built and merged to main (PRs #37, #40).

The agent panel (cartos-web/src/shell/AgentChatPanel.tsx) is a global sidebar in AppShell. It was overhauled to become a full intelligence surface:

Window presence

The agent sidebar is resizable: drag the left edge to set the width (300–900 px); the choice persists in localStorage. A fullscreen/expand toggle pops the panel out to cover the entire app for deep analysis work; a restore button returns to the sidebar layout.

"Ask the map" — voice input

A microphone button below the prompt textarea uses the browser's native Web Speech API (SpeechRecognition / webkitSpeechRecognition, Chromium-based browsers). Speaking transcribes into the prompt box; the user reviews the text and hits Enter. No backend is involved — purely client-side. Unavailable in non-Chromium browsers (an alert informs the user).

Rich response rendering

Assistant answers are rendered as GitHub-flavored Markdown via react-markdown + remark-gfm (AgentMarkdown.tsx). Supported in the panel:

The same AgentMarkdown component is used for the export path (see below), so tables and SVG charts carry through to the rendered report faithfully.

Comprehensive export

An Export button (visible when the conversation has at least one turn) opens a three-option menu:

Option How it works
Print / PDF Builds a Cartos-styled HTML report client-side via buildAgentReportHtml (re-renders each assistant turn through AgentMarkdown with renderToStaticMarkup) → opens in a new tab → triggers window.print(). Browser prints to PDF.
Word (DOCX) Concatenates the conversation into a single Markdown document (conversationMarkdown), POSTs to POST /api/agent/export/docx (cartos-api), which proxies the Python model-sidecar's markdown→docx renderer. Download is triggered client-side on the blob response.
HTML Downloads the same Cartos-styled HTML file (downloadHtml) — portable, self-contained, no server round-trip.

The export is titled from the first user question (truncated to 70 chars). Live map screenshots in the export are a known follow-up — the global agent panel has no handle to the active module's map ref; that bridge is planned once a map-ref context is added to AppShell.

Agent system prompt

crates/cartos-agent/src/agent.rs SYSTEM_PROMPT was updated to direct the agent to respond in GitHub-flavored Markdown, use tables for tabular data, optionally end with Summary/Recommendations sections, and emit chart code-fences for visual data — while staying strictly grounded in tool-returned data.

MCP server — built (PR #43)

The Cartos MCP server is built and operational. See §13.6 for full documentation.


9.2 Map Export & Mapbook

Status: built and wired as a first-class capability feature (PRs #51, #53, #56); merged to main. Migration 0018 enables it for the dev tenant on Smart City, Maritime, and Emergency Services.

Source: cartos-web/src/features/map-export/MapExportOverlay.tsx · cartos-web/src/lib/mapbook.ts

A recreation of the EarthWatch3D advanced export and mapbook, ported to the MapLibre native modules and refined to full EarthWatch3D parity (PR #56). It is a portable capability feature (map-export) that slots into the PanelDock rail alongside Analysis and Compare — wired identically, and addable to any native map module via Admin → Features.

Panel layout (PR #56 refinement)

Export is now consolidated into the left-dock sidebar panel — the same DockablePanel pattern as Analysis and Compare. The old top-bar "Export" menu that existed in Smart City has been removed; all export flows live in the panel. A Create Map | Mapbook mode toggle at the top of the panel selects which workflow is shown. A Data (CSV) section at the bottom of the panel exposes per-module data tables as CSV downloads (DataExport[] prop), portable across any host module.

Create Map mode

The Create Map tab exports the current view. It offers four EarthWatch3D-parity single-page layout templates, each with a preferred default paper size and orientation:

Template Default paper Default orientation Description
Classic Visual Tabloid Landscape Single-page header + map + plate row. The baseline.
Operational Briefing Tabloid Landscape Hero map + right sidebar: stat tiles (zoom/centre/bearing), mission notes (free text), overview inset (auto-zoomed out ~4 levels).
Field Map A3 Portrait Max-map portrait with neatline + bottom sheet plate (project, date, centre coords, zoom). Caveat line for datum/declination. Print-and-fold.
Compact A4 Brief A4 Portrait One-pager: header, stat tiles (zoom/centre/bearing/date), map, notes section.

Selecting a template automatically seeds the paper size and orientation; both are still overridable. All templates include a bearing-aware north arrow, geodesic scale bar, neatline frame, optional classification banner (SEC/FOUO/CONFIDENTIAL/SECRET/TOP SECRET — auto-coloured), and a bounds/projection footer.

Three export buttons are always present in Create Map mode:

Paper sizes

The full plotter paper range is now available (down from five sizes in the original build):

Group Sizes
Office Letter (8.5×11), A4, Tabloid (11×17)
ISO A3, A2, A1, A0
ARCH (plotter) ARCH A (9×12), ARCH B (12×18), ARCH C (18×24), ARCH D (24×36), ARCH E (36×48), ARCH E1 (30×42)
Wide format Wide 30×42, Wide 36×48, Wide 36×60, Wide 42×60, Wide 48×60
Custom User-defined width × height in inches

All sizes support landscape and portrait orientations (the panel swaps the longer and shorter dimension accordingly).

Mapbook mode

A multi-page atlas from the current view, assembled via jsPDF:

  1. Cover page — AOI overview map + labeled tile-grid index showing which sheet covers which cell. A sheet-plate row records scale, sheet count, paper, and generation timestamp.
  2. Sheet per tile — the map flies to each tile's bbox (fitBounds + 'idle' event), captures the canvas, and writes a sheet with a plate label (e.g. "Sheet B-3"), scale bar, north arrow, and a mini index grid indicating the current tile position. View is restored after the last sheet.
  3. Appendix (optional) — free-text notes page.

The tile grid auto-computes rows × columns from a chosen scale (1:N) and an overlap percentage (0–40 %), or the user sets the grid dimensions manually. Maximum 12 × 12 tiles.

OGC GeoPDF export (PR #53)

A true OGC GeoPDF — georeferenced, measurable, opens correctly placed in QGIS/ArcGIS. Available in both Create Map and Mapbook views via the "Export GeoPDF (georeferenced)" button.

How it works — server-side GDAL:

  1. captureNorthUp in lib/mapbook.ts forces a north-up nadir capture (bearing = 0, pitch = 0) and records the current map bbox as Mercator corners, then restores the prior bearing/pitch.
  2. The PNG + the Mercator bbox are POSTed to POST /api/export/geopdf in cartos-api.
  3. cartos-api proxies the request to the model-sidecar's POST /render/geopdf endpoint.
  4. The sidecar runs gdal_translate -of PDF -a_srs EPSG:3857 -a_ullr <xmin> <ymax> <xmax> <ymin> (from gdal-bin), tagging the PNG with Web Mercator corners.
  5. The resulting .pdf bytes are streamed back as application/pdf and downloaded.

CRS — EPSG:3857 (Web Mercator): the MapLibre canvas is Mercator; tagging as EPSG:4326 would geometrically distort the image at non-equatorial latitudes. GIS tools open and measure correctly in EPSG:3857. Nadir/top-down only: captureNorthUp zeroes bearing and pitch so the extent is axis-aligned (required for a four-corner affine georeference). Oblique/3D views require GCP warping — the documented follow-up.

Verified: gdalinfo reports Driver: PDF/Geospatial PDF, CRS EPSG:3857, corners matching the AOI bbox. gdal-bin is installed in both model-sidecar/Dockerfile and Dockerfile.gpu.

Technical notes


9.3 3D Data — Point Clouds & Gaussian Splats

Status: Point clouds — PR #55 (initial), PR #57 (true geospatial placement, density slider, 90° pitch). Gaussian splats — PR #63 (three.js overlay canvas + SuperSplat SOGS importer). All merged to main. Migration 0019 enables the feature for the dev tenant. Compare: basemaps holographic-theme and hillshade-theme were also shipped in PR #59 (theme-matched basemaps in the Compare overlay layer catalog).

Source: cartos-web/src/features/point-cloud/PointCloudOverlay.tsx · cartos-web/src/lib/point-cloud.ts · cartos-web/src/lib/gaussian-splats.ts · cartos-web/src/lib/sogs.ts

The point-cloud capability feature now serves two 3D data types from one PanelDock panel: point clouds / 3D Tiles (via deck.gl) and Gaussian splats (via three.js). A Cloud | Splat mode toggle at the top of the panel selects which viewer is active. The overlay persists while the module is mounted. It is mounted in Smart City, Maritime, Emergency Services, and all nine Cartos Grid department modules (grid-operations, grid-capital, grid-fleet, grid-vegetation by default). A Tech Admin can cross-add it to any other native map module.

Pitch the map (right-drag / ctrl-drag) up to 90° for a full ground-level "standing" view. Smart City, Maritime, Emergency Services, and all Grid modules set maxPitch: 90 on their MapLibre map constructor (MapLibre 5.24 allows up to 180°; the old default was 60°).

Two source modes

3D Tiles service: Paste a tileset.json URL — any OGC 3D Tiles 1.0 point cloud (.pnts) or mesh. Rendered via Tile3DLayer + Tiles3DLoader from @loaders.gl/3d-tiles. On load the map flies to the tileset's cartographicCenter. Works with Cesium ion tilesets, Potree conversions, and hosted municipal or survey datasets.

Local LAS / LAZ file: Drag any .las or .laz file; parsed by LASLoader from @loaders.gl/las. After parsing, placement is determined by the Georeferenced | Center of map toggle:

Mode When to use How it works
Georeferenced (default) File has known coordinates Geographic (lon/lat) files: x/y fall in the lon/lat range → rendered at true position using COORDINATE_SYSTEM.LNGLAT — no CRS input needed. Projected files (e.g. UTM): user enters a CRS (EPSG code, EPSG:nnnn string, or raw +proj=…); resolveProjDef resolves it, proj4 reprojects the cloud's x/y centroid once to WGS 84 lon/lat, and every point rides as a metre offset from that origin. The map flies to the cloud. UAE projected files are typically EPSG:32640 (UTM zone 40N).
Center of map CRS is unknown Cloud is anchored at the current view centre — handy for visualising structure/shape when geographic context is not needed.

If the georeferenced path is selected but no CRS is supplied (or the supplied string is unrecognised), the overlay falls through to center-of-map placement and shows a status message prompting the user to enter an EPSG/proj4.

resolveProjDef handles: EPSG:4326, EPSG:3857, all UTM north zones (EPSG 32601–32660), all UTM south zones (EPSG 32701–32760, adds +south), and any raw +proj=… string. Any other EPSG code currently returns null (center-of-map fallback).

Density slider (PR #57)

A density slider (2–100 %) decimates large local clouds before they are sent to the GPU. The library keeps every Nth point (step = round(1/fraction)), so a 10 M-point file at 10 % renders ~1 M points. This keeps WebGL throughput interactive on typical hardware without discarding the raw parse — raising the slider back to 100 % re-renders the full cloud.

Controls summary

Control Range Effect
Point size 1–8 px Rendered radius of each point in the deck.gl PointCloudLayer
Density 2–100 % Decimation: keep every Nth point (applied to local files; tilesets are already streamed by the server)
Georeferenced / Center of map toggle Placement mode for local LAS/LAZ
CRS input EPSG code / +proj string Required for projected local files in Georeferenced mode
Hide / Show button Toggles layer visibility without clearing the data
Clear button Removes the loaded cloud or tileset

Technical notes

Gaussian splats (PR #63)

Gaussian splat files (.splat, .ksplat, .ply, .spz, and SuperSplat SOGS bundles) are rendered in MapLibre via a dedicated transparent overlay canvas driven by MapLibre's per-frame matrices — not a MapLibre custom layer injecting into the default framebuffer (which is not compositable by three.js). The overlay canvas sits above the map canvas with pointer-events: none.

Rendering architecture (lib/gaussian-splats.ts):

SuperSplat SOGS importer (lib/sogs.ts, PR #63):

@mkkellogg/gaussian-splats-3d has no built-in SOGS decoder. lib/sogs.ts fills the gap:

Splat panel controls:

Control Effect
Mode toggle (Cloud / Splat) Switches between the point-cloud and Gaussian-splat viewers
URL input Load a .splat / .ksplat / .ply / .spz from a URL
Local file drag/drop Load any supported format including .ssog / .sog / .zip (SOGS)
Scale (×0.01 – ×10) World-space scale of the splat relative to its anchor
Heading (0–359°) Yaw about the up axis to align the splat to north
Flip Y Vertical flip (capture up-axis varies between tools)
Lng / Lat inputs Fine-tune the anchor position
Recentre Set anchor to the current map centre
Zoom to splat Fly to the anchor + auto-zoom + 60° pitch
Hide / Show Toggle visibility without clearing the scene
Clear Remove the loaded splat

preserveDrawingBuffer note: the splat overlay uses its own WebGLRenderer with preserveDrawingBuffer: true (keeps the frame visible between browser composite passes); it does not share the MapLibre canvas.

New npm dependencies (PR #63): three (three.js), @mkkellogg/gaussian-splats-3d, fflate (zip decompression for SOGS bundles).


9.4 Platform theme system

Status: built and merged to main (PR #67).

Source: cartos-web/src/lib/themes.ts · lib/theme-context.tsx · lib/themes.css · shell/ThemeSwitcher.tsx · lib/map-styles.ts (recolorStyleAccent, resolveBasemapStyle)

The Cartos engine has a platform-level theme system that re-skins the entire app — including the login screen, splash, all module UIs, and the basemap — without any page reload. Being engine-level, it works equally across every edition (Cartos Atlas, Cartos Grid, and future editions).

Resolution model

There is ONE global theme for the whole app. Any module may set a per-module override that applies while that module is active. Active theme = current module's override if set, else the global theme. Both the global choice and the per-module override map are persisted independently in localStorage (cartos.theme.global and cartos.theme.modules), so a deployment or user keeps its look across reloads.

The 10 palettes

Each palette is a ThemeOption with a ThemeKey, a label, a tagline, and a ThemeTokens set (12 CSS custom-property values + a scheme: "dark" | "light" flag):

Key Label Tagline
holographic Holographic Cartos signature · cyan-on-night
green Emerald Night-ops green
purple Violet Deep ultraviolet
red Crimson Tactical red
camo Camo Field olive & khaki
dark Carbon Monochrome dark
light Daylight Ink on glass (light scheme)
desert Desert Sand & amber dusk
rainbow Rainbow Full spectrum
tiedye Tie-Dye Good vibrations

applyThemeTokens(key) writes all 12 tokens onto :root as CSS custom properties (--bg, --bg-elev-1, --bg-elev-2, --border, --text, --text-dim, --accent, --accent-dim, --good, --bad, --warn) and sets color-scheme; the whole UI re-skins without a React re-render because everything references these variables. The data-cartos-theme attribute on <html> enables CSS selectors for per-theme overrides.

bootTheme() is called before first paint (in main.tsx before ReactDOM.render) so there is no flash of the default palette on load.

Theme-matched basemaps (lib/map-styles.ts)

Two new basemap keys in the map-styles catalog take the active theme accent:

Key Base Effect
holographic-theme Dark vector tiles The road hierarchy, labels, and water are recoloured to the theme accent; background stays near-black.
hillshade-theme Terrain + hillshade Same recolouring applied on top of the 3D-ish terrain texture.

resolveBasemapStyle(key, accent?) checks a themed: true flag on the basemap definition; when true it calls recolorStyleAccent(baseStyle, accent), which walks the MapLibre StyleSpecification layer list and replaces every colour expression matching the target hue with the new accent. themeAccent(key) reads the accent synchronously from THEMES (not from the DOM) so the recolour happens before the style is applied and there is no flicker. These two basemaps are listed in the Compare overlay's layer catalog (PR #59: "Holographic" and "Hillshade-3D" entries), and appear as native basemap options in GridModule and all other native map modules.

ThemeSwitcher UI (shell/ThemeSwitcher.tsx)

A palette picker accessible from the app shell (gear/settings icon in the user badge area). Shows all 10 palettes as colour swatches; clicking one calls applyThemeTokens and saveGlobalTheme. A per-module override dropdown lets power users set a different look for a specific module without changing the global default.

Floating panels now follow the theme accent (fixed 2026-07-18, shipped platform-wide)

Every dockable/floating panel — Basemap, Analysis, Compare, Map Export, the Navigate rail, AgentChatPanel, ArcGisLayersPanel, GeoWizard, LayerStylePopover, SatelliteDataPanel, SensorReadingsChart, StacCatalogPanel, UserBadge, DockablePanel's own chrome, and more — hardcoded the holographic cyan (#00d9ff, rgba(0,217,255,…)) for borders, title bars, active states, glows, and resize chips, so on any non-holographic edition (Cartos Africa/Blueprint = Desert amber, Cartos Tower = crimson) the map recoloured to the theme but every popup stayed cyan. themes.ts now also writes two RGB-triple custom properties on :root--accent-rgb and --bg-rgb (hexToRgbTriple) — so CSS can build themed translucent colours with rgba(var(--accent-rgb), 0.2); a 27-file CSS sweep (299 replacements) changed every hardcoded cyan literal to var(--accent) / rgba(var(--accent-rgb), x), and DockablePanel's glass background now uses rgba(var(--bg-rgb), …) so a panel's body tints too (warm-dark on Desert, light on Daylight). Zero-regression for Holographic by construction — its --accent is #00d9ff, so the swept vars resolve to the exact prior cyan. Verified in-browser on Africa (Desert theme): Basemap panel, panel rail, and the Navigate widget all render amber.

"Satellite · Esri" is now a global basemap option (fixed 2026-07-18, shipped platform-wide)

lib/map-styles.ts's esri-imagery basemap (Esri World Imagery + a thin reference overlay) was labelled "Ortho · UAE HR" with a UAE-specific tagline, even though the underlying Esri World Imagery service is a standard global high-res aerial layer (sub-meter over most metros worldwide, not just Abu Dhabi/Dubai). Relabelled "Satellite · Esri" / "Esri World Imagery · global high-res aerial" so it reads correctly in every edition's basemap pulldown, including Cartos Africa's Foundation Layers registry, which reuses the same public Esri tile endpoint.


9.5 Cloud-native data layer — object-storage format spikes

Status: spikes / proofs-of-concept on main (PRs #113–#117). All formats are proven end-to-end on the existing MinIO object store — zero new services beyond what Phase 1 already runs. Demo / synthetic data. Public-read MinIO bucket in dev; not yet access-gated (no signed URLs) or hardened for production input. Production hardening is scoped as TBD in Phase 2 M03.

The platform evaluated GeoLibre 1.0 (opengeos; MIT) as a potential dependency and adopted the underlying permissive-licensed libraries directly (not the GeoLibre app). The result is five object-storage-native data formats that the browser range-reads directly from MinIO, each exercised in the Cartos Grid Grid Data panel and its companion feature panels. All five spikes reuse the existing MinIO object store (deploy/) and expose toggles/launchers in the existing GridModule.tsx panel system.

The six format experiments

Format PR What it proves Key library
PMTiles (serverless vector tiles) #113 A single .pmtiles archive in MinIO is range-read by the browser via the pmtiles:// protocol — no tile server, no per-request computation pmtiles (Protocol) registered via MapLibre addProtocol
GeoParquet + DuckDB-WASM (in-browser analytics) #114 A .parquet file is fetched from MinIO and queried with full columnar SQL inside the browser (DuckDB-WASM, self-hosted via Vite ?url — no CDN) @duckdb/duckdb-wasm
FlatGeobuf (bbox-streamed vector) #115 A .fgb with a packed Hilbert R-tree index is range-read: the browser requests only the features in the current viewport; re-reads on moveend flatgeobuf (fgb.deserialize with bbox rect)
COG (Cloud-Optimized GeoTIFF, range-read raster) #115 A .tif in MinIO is tile-fetched via a cog:// MapLibre protocol (geotiff.js under the hood) — no raster server, no tile pre-generation @geomatico/maplibre-cog-protocol
Turf.js client-side geoprocessing #116 Buffer / convex hull / centroid computed entirely in the browser over a live Grid layer (no API round-trip, PostGIS untouched) @turf/turf
Broad export #117 A live Grid layer downloaded as GeoJSON / Shapefile (.zip) / FlatGeobuf / CSV (all client-side); GeoPackage produced by a batch tool and served from MinIO flatgeobuf + @mapbox/shp-write

Format generators (reusable operator scripts)

One-off batch scripts; scratch outputs go in tools/_tmp/ (gitignored). tools/upload_minio.py is a shared helper used by all generators (creds from env: MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY; defaults match deploy/docker-compose.yml).

Script Format produced Key dependency
tools/build_pmtiles.py PMTiles v3 archive (points only — pure-Python tiler; for line/polygon use tippecanoe in production) mapbox-vector-tile, shapely, pmtiles
tools/build_geoparquet.py GeoParquet (column-major, compatible with DuckDB read_parquet) pyarrow, geopandas
tools/build_cog.py Cloud-Optimized GeoTIFF (rasterio GDAL COG driver) rasterio
tools/build_geopackage.py GeoPackage (.gpkg) — pure-Python SQLite + GeoPackageBinary; no GDAL/fiona needed shapely, sqlite3
cartos-web/scripts/build-flatgeobuf.mjs FlatGeobuf with spatial index (Node) flatgeobuf npm

Env var

VITE_CARTOS_TILES_URL — MinIO base URL the browser uses for all object-storage format range-reads (default http://localhost:9000; set in cartos-web/.env.local). Consumed by cartos-web/src/lib/env.ts → CARTOS_TILES_URL.

Web surface

All five format experiments surface in the Cartos Grid modules:

All three panels are portable features/ components following the DockablePanel contract (same pattern as OvertureOverlay, AnalysisOverlay, etc.) and can be mounted in any module.

Production hardening — TBD (Phase 2 M03)

Gap Plan
Public-read MinIO bucket (no auth on object storage) Signed URLs from MinIO / S3, or an authenticated range-request passthrough proxy in cartos-api
PMTiles builder is points-only (pure-Python tiler) Use tippecanoe for arbitrary-geometry (line/polygon) tilesets
COG path uses synthetic demo raster Feed real imagery / DEM / change-detection output rasters
GeoParquet fetched whole into DuckDB browser memory Partition by spatial grid + use DuckDB's native HTTP range reads for large files
In-browser GeoPackage read sql.js or @loaders.gl/gpkg for a browser-side read path

Source files: cartos-web/src/lib/pmtiles-layer.ts · cartos-web/src/lib/duckdb.ts · cartos-web/src/lib/flatgeobuf-layer.ts · cartos-web/src/lib/cog-layer.ts · cartos-web/src/lib/layer-export.ts · cartos-web/src/features/duckdb/DuckdbSpikePanel.tsx · cartos-web/src/features/geoprocessing/GeoprocessingPanel.tsx · cartos-web/src/features/export/ExportPanel.tsx · tools/build_pmtiles.py · tools/build_geoparquet.py · tools/build_cog.py · tools/build_geopackage.py · tools/upload_minio.py · cartos-web/scripts/build-flatgeobuf.mjs.


9.6 Operational Data & Layer Catalog — real data, symbology, ask-the-map

Status: built and merged to main — Phases 1–4 (the original epic) plus a 2026-07-18 Phase 5 (holographic symbology, feature icons, vector-over-raster ordering) all shipped, live on cartosatlas.com. The keystone for the operational-data / admin-layer / ask-the-map epic (plan: curious-waddling-widget.md). A DB-backed catalog of datasources and map layers that admins manage, real open-data feeds populate, a symbology engine styles (including in real 3D), and the agent can now query directly — across every MapLibre-native module the caller can see.

What it is: datasources (a connection to data) + map_layers (a renderable layer bound to one) generalize the earlier single-purpose tables (grid_integrations, overlay_layers) into one admin-managed catalog that spans every MapLibre-native module — not just Cartos Grid. Additive: a disjoint cat--prefixed source/layer namespace sits alongside the existing overlay_layers (STAC) and grid-* render paths; nothing pre-existing changes.

Phase 1 — Datasource + Layer Catalog foundation

Phase 2 — real data (region loader + 7 modules populated)

Phase 3 — symbology engine (attribute-driven styling, incl. real 3D)

Attribute-driven styling for catalog layers, rendered from the persisted map_layers.style JSONB slot (cartos-web/src/lib/catalog-style.ts):

Phase 4 — "ask the map" (agent tools)

Two cross-module agent tools (crates/cartos-agent/src/builtin/, module() returns None so they're always available regardless of which module is open) let the agent discover and query any catalog layer the user is entitled to see — see §7 for the full tool-registry list:

Data layer (crates/cartos-data/src/catalog.rs): list_accessible_layers (cross-module scoped list + feature counts), find_layer_by_key (scoped resolve), query_layer_features (a QueryBuilder — optional filters, incl. countries/adm1, bind as parameters, never string-interpolated). list_layer_features (the GET /api/catalog/layers/:id/features handler) and module_kpi (GET /api/catalog/kpi) also gained optional country_iso3/countries + adm1 query params so the Africa dashboards and the map's own feature fetch can filter server-side, not just client-side. Verified against real dev data (e.g. emergency stations: 39 police, 17 fire via group_by; Kenya health facilities via africa_scope + query_catalog_layer: 1,810).

Phase 5 — holographic point symbology, feature-type icons, vector-over-raster ordering

Shipped 2026-07-18, platform-wide (every module with catalog point/vector data, not just Africa):

Deferred

A private-credential datasource must never be fetched client-side — that gap is closed by design (such sources need a server-side proxy — a later phase); a secrets vault (secret_ref is the seam, credentials are plaintext JSONB today); consolidating the cat- / grid-* / overlay_layers reconcilers into one shared implementation; wiring the pmtiles / cog render_types for catalog layers to the §9.5 spike libraries (today the two are separate paths).

Source files: crates/cartos-data/migrations/0038_catalog_datasources_layers.sql · 0039_enable_catalog_seed.sql · 0040_catalog_features.sql · 0041_enable_catalog_more_modules.sql · 0061_africa_border_icon.sql · crates/cartos-data/src/catalog.rs · crates/cartos-api/src/catalog.rs · crates/cartos-core/src/tenant.rs (user_type(), role_slugs()) · crates/cartos-cli/src/loader.rs (load-geojson) · crates/cartos-agent/src/builtin/catalog_layers.rs · query_catalog_layer.rs · cartos-web/src/lib/catalog.ts · lib/catalog-layers.ts (raiseVectorsToTop) · lib/catalog-style.ts · lib/catalog-3d.ts · lib/catalog-icons.ts · lib/layer-style.ts · features/catalog/CatalogLayersOverlay.tsx · LayerStyleEditor.tsx · CatalogLegend.tsx · features/compare/CompareOverlay.tsx (withSessionVectors) · modules/admin/CatalogConfig.tsx · tools/osm_to_geojson.py · tools/load_demo_catalog.sh · tools/set_demo_styles.sql · data/catalog/*.geojson.


9.7 Dashboards — per-edition & catalog-driven

Status: built and merged to main. Every Grid + industry-edition department module now has its own full-screen dashboard bound to its own data, and a reusable catalog-driven dashboard component is the new standard for the industry editions. Two shell bugs the work surfaced are fixed alongside it.

Per-edition full-screen dashboards

Previously only the native Government-edition modules (Smart City, Maritime, Emergency Services) had a full-screen /<module>/dashboard route; Cartos Grid and the industry editions only had an in-map dock panel. Now every department module gets its own full-screen dashboard that shows its own data and returns to its own map on close:

CatalogDashboard — the reusable, catalog-driven standard

features/catalog/CatalogDashboard.tsx extracts the Smart City dashboard's "caliber" — full ECharts graphs, counts, computed alerts, a theme selector, and reorderable cards — into a component driven entirely by a module's real catalog data, so any edition gets the same bar without bespoke chart-wiring:

CatalogKpiStrip — pre-stocking the native module dashboards

The industry-edition dashboards already prepended LIVE tiles built from catalog data; the native module dashboards did not. features/catalog/CatalogKpiStrip.tsx extracts that pattern into a small, portable strip — one tile per internal catalog layer with feature_count > 0 — mounted in SmartCityDashboard, EmergencyServicesDashboard, MaritimeDashboard, and GridDashboard (keyed to the active department moduleId). Renders nothing when a module has no catalog data, so it is safe to mount everywhere. Every module's popup dashboard now opens pre-stocked with its real loaded data (hospitals & schools, stations, ports, substations & transmission) alongside its existing operational KPIs.

Two shell fixes this work surfaced

Source files: cartos-web/src/shell/DashboardPage.tsx · DashboardPage.css · cartos-web/src/App.tsx · cartos-web/src/modules/grid/GridDashboardPage.tsx · cartos-web/src/modules/industry/IndustryDashboardPage.tsx · cartos-web/src/features/catalog/CatalogDashboard.tsx · CatalogDashboard.css · cartos-web/src/features/catalog/CatalogKpiStrip.tsx · CatalogKpiStrip.css · cartos-web/src/features/catalog/MapStatsBar.tsx · MapStatsBar.css · cartos-web/src/shell/AppShell.tsx (nativeModuleFromPath, catalogModuleFromPath) · cartos-web/src/modules/grid/GridDashboard.tsx · cartos-web/src/modules/smart-city/SmartCityDashboard.tsx · cartos-web/src/modules/emergency-services/EmergencyServicesDashboard.tsx · cartos-web/src/modules/maritime/MaritimeDashboard.tsx.


9.8 Map tools — Time of Day & Measure

Status: built and merged to main 2026-07-10. Both are PanelDock rail items — feature-gated capability features (time-of-day, measure) — default on Smart City, Maritime, and Emergency Services (static default_features in crates/cartos-core/src/catalog.rs; no new DB migration was needed, the same mechanism that already covers analysis/compare). Cross-add to any other native map module via Admin → Features. cargo check + tsc + vite build clean at ship.

Time of Day (features/time-of-day/TimeOfDayOverlay.tsx)

Source: cartos-web/src/lib/time-of-day.ts (the solar engine + guarded MapLibre "apply" calls) · cartos-web/src/features/time-of-day/TimeOfDayOverlay.tsx (the panel).

A sun-position lighting panel that drives real map lighting from lat/lon + date + apparent solar time, instead of a static default light. lib/time-of-day.ts is a dependency-free NOAA/SunCalc-style solar-geometry engine (the production CSP blocks external code, so it's inlined rather than an npm package) — it computes sun altitude/azimuth for any date/lat/solar-hour, the day's sunrise/golden-hour/solar-noon/sunset, and an altitude curve for the slider — then translates a sun position into three guarded MapLibre calls:

Every map.* call is wrapped in try/catch so it no-ops safely mid-style-load or on a basemap without terrain/hillshade — the same defensive pattern as the other overlay reconcilers. The panel re-applies on mapEpoch so a basemap swap keeps the lighting, and restores MapLibre's neutral default light/sky on close or unmount (clearTimeOfDay).

Panel controls:

Control Detail
Solar time + date Big HH:MM solar readout; a date picker (drives the day's declination)
Sun-elevation curve An inline SVG altitude-vs-time curve for the day, with twilight/day/night colour bands and a draggable cursor dot showing the current position
Slider Scrubs solar time 0–24 h across the curve
alt / az / compass / phase Numeric altitude + azimuth, a 16-point compass label, and the day/twilight/night phase word
Presets Sunrise · Golden (evening golden-hour start, sun at 6° descending) · Noon (always 12:00 solar) · Sunset — computed from the day's sunrise/set hour angle, disabled where the sun never reaches that altitude (polar edge case)
Play + speed Animates solar time forward via requestAnimationFrame; 15 min/s, 1 h/s, 3 h/s, or 6 h/s
Shadows toggle Strengthens hillshade contrast (hillshade-exaggeration) as a lightweight directional-shadow approximation — see the TODO below
Use map center Snaps lat/lon to the current map view centre

Shadows — known approximation, not real cast shadows. The toggle currently just deepens hillshade contrast; it doesn't cast building shadows onto the ground. Real cast shadows need a deck.gl SunLight shadow map that owns the 3D geometry (buildings), which MapLibre's native fill-extrusion layer doesn't expose — noted as a TODO directly in lib/time-of-day.ts and the overlay's own doc comment; not yet scheduled.

Measure (features/measure/MeasureOverlay.tsx)

Source: cartos-web/src/lib/measure.ts (geodesic + 3D math) · cartos-web/src/features/measure/MeasureOverlay.tsx (the panel).

The measure tool, previously an inline Smart-City-only distance/area button, is now a portable, 3D-aware capability feature with its own measure- MapLibre source/layers (owns its lifecycle: re-adds after a basemap swap via mapEpoch, tears down on close/unmount) — the same overlay contract as TimeOfDayOverlay and CatalogLayersOverlay.

Three modes, selected in the panel:

Clicking the map appends a vertex (each click is deferred ~220 ms so the terminating double-click doesn't also drop two stray vertices — MapLibre fires click+click before dblclick); double-click finishes the shape. Undo removes the last vertex; Clear resets. A m | ft toggle switches every readout between metric (m/km) and imperial (ft/mi, ft²/acre) units, persisted per host module in localStorage.

3D from terrain: each vertex's elevation comes from map.queryTerrainElevation (the native modules already run a hillshade terrain/DEM style — see §9.3). Per segment, lib/measure.ts computes:

Field How
horiz Great-circle (haversine) distance between the two vertices
dh Height delta, end − start (0 when either elevation is unknown)
dist3d Slope distance = hypot(horiz, dh)
brg Initial bearing, clockwise from true north (0–360°)

The panel shows a primary readout (3D for Distance, area for Area, signed Δh for Height) with horizontal + Δh secondary figures, and a live per-segment table (# | 3D | horiz | Δh | brg). Where the basemap has no terrain/DEM loaded, every vertex's elevation is null, so dh is treated as 0 and 3D == horizontal — the panel shows a "2D — no terrain" note rather than failing.

Selection-click guard: an optional armedRef prop is set true while the tool is actively collecting vertices, so the host module's own feature-selection/identify click handlers can bail (if (armedRef.current) return;) instead of firing under a measure click — Smart City's prior measuringRef pattern, now shared by Maritime and Emergency Services too.

9.9 Map tools UI — Widget kit, Draw & Annotate, Navigate rail

Status: built and merged to main 2026-07-11. cargo check + tsc + vite build clean at ship. Draw is a feature-gated capability (draw, default on Smart City/Maritime/Emergency Services); the Navigate rail, its coordinate readout, and its grid overlay are not capability-gated — they render unconditionally in every native module, every Cartos Grid department module, and every industry-edition (IndustryModule) surface, replacing MapLibre's built-in nav control.

Widget kit (features/widgets/widget-kit.css)

A shared "mission-control" HUD visual language for the floating map-tool panels — Measure, Time of Day, Draw, and Navigate all speak it now (Measure and Time of Day were re-skinned into it as part of this work; Draw and Navigate are native). A glassy translucent panel with a cyan-accent glow, segmented controls, mono data readouts, colour swatches, sliders, and toggle switches, all class-prefixed wk- (wk-hd/wk-seg/wk-btn/wk-sl/wk-sw/ wk-chip/wk-table/…) to stay out of the global CSS cascade. Theme-aware by construction: every colour is expressed through the app's theme CSS custom properties (--accent, --bg-elev-1, --border, --text, --text-dim, --accent-ink) combined with color-mix() for tints and glows, so switching a platform theme (§9.4) recolours every map-tool panel live, with no per-panel styling code.

Draw & Annotate (features/draw/DrawOverlay.tsx, capability draw, migration 0043)

A portable drawing/annotation tool following the same overlay contract as MeasureOverlay/TimeOfDayOverlay: it owns a draw--prefixed MapLibre source/layer set, re-adds them after a basemap swap (mapEpoch), persists to localStorage, and tears down cleanly on close/unmount.

Five modes (segmented control, top of the panel):

Mode Behaviour
Select Click a drawn feature to select it, then Delete removes it. (Vertex editing/dragging is out of scope by design — select + delete only.)
Point Click the map to place a point immediately.
Line Click to add vertices; double-click / Enter / the Finish button ends the line (≥ 2 vertices).
Polygon Click to add vertices (auto-closed on finish); double-click / Enter / Finish closes it (≥ 3 vertices).
Rectangle Two corner clicks define the box.

Clicks in Line/Polygon mode are deferred ~220 ms before committing a vertex so a terminating double-click doesn't also drop two stray vertices (MapLibre fires click+click before dblclick — the same pattern MeasureOverlay uses). Esc cancels the in-progress draft; Undo removes the last vertex. Freehand pointer-drag drawing is intentionally not implemented — the interaction is kept click-based.

Per-feature styling: each drawn feature carries its own stroke colour (8-swatch preset row + a custom colour picker), fill colour + fill-opacity slider, stroke-width slider (1–10 px), and an optional text label rendered as a MapLibre symbol layer at the feature's centroid. Styling is picked before drawing and baked onto the feature at commit time (not editable after, short of deleting and redrawing).

Feature list + export: a live list of every drawn feature (colour swatch, label or "unlabeled", kind, a per-row delete ✕); Clear all (confirm-gated); Copy GeoJSON to the clipboard and Download GeoJSON (a FeatureCollection with kind/label/stroke/fill/fill-opacity/ stroke-width as feature properties — portable to any other GIS tool). Persisted per module + user in localStorage (cartos.draw.<module>.<username>), reloaded whenever that identity changes.

Selection-click guard: while any draw mode other than Select is active, an armedRef (the same contract MeasureOverlay uses) is set true so the host module's own feature-selection/identify click handlers bail — drawing never triggers an identify popup underneath, and vice versa.

A collapsible, tall-and-thin vertical rail, default top-right over the map. Collapsed it's a slim glass "NAV" nub; expanded it's a narrow panel that drives the live camera and shows coordinate readouts. It replaced MapLibre's default NavigationControl/GeolocateControl/FullscreenControl in every native, Grid, and industry module — lib/nav-controls.ts now only adds the two built-in MapLibre scale bars (metric + imperial, bottom-left).

Camera controls (unchanged behaviour from the old built-ins, now in the widget-kit HUD):

Multi-CRS coordinate readout (lib/coords.ts, new deps proj4 + mgrs): a compact always-visible CRS picker (all five reachable in the thin rail) plus live Center (on move) and Cursor (on mousemove) readouts, each with a one-click copy-to-clipboard button.

CRS Format
WGS84 Decimal or DMS (toggle), lat.fff, lng.fff or D°M'S"H
Web Mercator (EPSG:3857) Grouped easting/northing metres; "n/a" beyond ±85.06° latitude
UTM Zone + hemisphere + grouped easting/northing metres (proj4, WGS84 ellipsoid); "n/a" beyond ±84°
MGRS Via the mgrs package, 1 m default accuracy, spaced as 17S PV 12345 67890; "n/a" beyond ±84°
State Plane (SPCS) Best-effort 21-zone CONUS NAD83/US-survey-feet table (SPCS_ZONES in lib/coords.ts) — the point's lon/lat is matched against each zone's bounding box (first match wins); a point outside every covered zone returns a clear "n/a — outside a supported State Plane zone" message rather than a wrong number

Every formatter is defensive — out-of-domain points or a proj4 failure degrade to "—" or an explicit "n/a …" string, never a thrown error. Note: the map itself still only ever renders in MapLibre's own Mercator or Globe projection (a MapLibre limitation) — CRS selection drives the readout, not what's actually projected on screen.

UTM/MGRS grid overlay (lib/grid-overlay.ts, Off · UTM · MGRS toggle in the rail): a toggleable graticule rendered as grid--prefixed MapLibre line/label layers, rebuilt on moveend while active.

Move + resize: the expanded rail is drag-by-header and resize (bottom / left / right / both bottom corners) just like DockablePanel, clamped inside the map container and reflowed on container resize (ResizeObserver); double-click the header to reset to the default top-right anchor. Geometry, open/collapsed state, selected CRS, WGS84 dec/DMS choice, grid mode, and grid style all persist in localStorage (cartos.nav.* keys).

Universal drag + resize

Every floating map surface now shares the same interaction: header-drag to move, edge/corner-drag to resize, clamped to the map container. DockablePanel already worked this way; this round extended it to the rest of the map chrome:

Ops gotcha — new FeatureDefs need a backfill migration

load_entitlements reads the tenant_features DB enable-state rows, not the compile-time default_features in crates/cartos-core/src/catalog.rs — so adding a feature to a module's compile-time defaults after a tenant has already been seeded does not retroactively surface it; the launcher panel stays gated off until a backfill migration inserts the enable-state row. Migration 0042_enable_map_tools.sql (Time of Day + Measure), 0043_enable_draw.sql (Draw), and 0044_enable_mapillary.sql (Mapillary, below) all do this — an idempotent INSERT … ON CONFLICT … DO UPDATE SET enabled = true scoped to tenants that already have the relevant native module enabled. Run cargo run -p cartos-cli -- migrate and restart cartos-api after adding any new default feature to an existing module.


9.10 Mapillary — Street Imagery overlay

Status: built and merged to main 2026-07-13. tsc and cargo build -p cartos-core -p cartos-agent -p cartos-api clean at ship. Capability feature mapillary, portable like Overture — attachable to any module via the capability system, not owned by one.

What it is: crowd-sourced street-level photography from Mapillary (Meta) as a map overlay, plus a cross-module "ask the map" agent tool over the same data. It is deliberately framed as external, community-contributed reference imagery — © Mapillary contributors, CC-BY-SA — not sovereign ground truth (that's Cartos Field); the panel and the popup both carry the attribution.

Web overlay (features/mapillary/MapillaryOverlay.tsx, lib/mapillary.ts)

Same portable-overlay contract as OvertureOverlay / MeasureOverlay: owns its own MapLibre source/layers, re-adds them after a basemap swap (mapEpoch), tears down on close/unmount, and persists its panel prefs to localStorage per panel id.

Agent tool — mapillary_street_imagery (crates/cartos-agent/src/builtin/mapillary_street_imagery.rs)

A cross-module tool (module()None, side_effects()ExternalCalls) registered via register_mapillary in builtin/mod.rs, available in every module regardless of which one is open (same shape as the catalog "ask the map" tools). It searches the Mapillary Graph API's images endpoint — graph.mapillary.com/images?bbox=… — for photos inside a bounding box, or (if near_lon/near_lat/radius_m are given instead) a degree-box computed around a point. Optional pano_only (filters client-side, a limit-aware over-fetch keeps result counts honest), since_year (server-side start_captured_at filter), and limit (1–50, default 20). Each returned image carries lon/lat, a formatted YYYY-MM capture month, compass heading, is_pano, a thumb_url, and a mapillary_url deep link. Answers questions like "is there street imagery of this intersection?", "show me recent photos near 54.37,24.47", or "any 360° panoramas of this site?". Reads the server-side MAPILLARY_TOKEN env var at run time (not baked in at build time); when it's unset the tool returns {"configured": false, "message": "…"} instead of erroring, so the agent can explain rather than fail.

Where it's on by default

Added to default_features in crates/cartos-core/src/catalog.rs for the native trio — Smart City, Emergency Services, Maritime — and every department module of the three industry editions built on the shared IndustryModule surface — Cartos Tower (air-*), Cartos Milepost (dot-*), Cartos Blueprint (aec-*). Not a Cartos Grid default; available there (and anywhere else) as a Tech Admin cross-add via the capability system (§7.4).

Config & deploy

Where Env var Notes
Web (cartos-web/src/lib/env.ts) VITE_MAPILLARY_TOKEN A free client access token from mapillary.com → Developers. Client-side by design — that's what Mapillary issues client tokens for. Without it: setup notice, no map layers.
Agent (crates/cartos-agent) MAPILLARY_TOKEN Server-side token, read at run time. Without it: the tool answers configured: false rather than erroring.

Migration 0044_enable_mapillary.sql backfills an enabled tenant_features row for every tenant that already has one of the carrying modules on — the same "new default feature needs a backfill" gotcha as migrations 0042/0043 above. Deploy needs a cartos-api rebuild (new agent tool) and the migration run.

Source files: crates/cartos-core/src/catalog.rs (FEATURES entry + default_features) · crates/cartos-data/migrations/0044_enable_mapillary.sql · cartos-web/src/features/mapillary/MapillaryOverlay.tsx · cartos-web/src/lib/mapillary.ts · cartos-web/src/lib/env.ts (MAPILLARY_TOKEN) · cartos-web/src/shell/PanelDock.tsx (panel kind) · crates/cartos-agent/src/builtin/mapillary_street_imagery.rs · crates/cartos-agent/src/builtin/mod.rs (register_mapillary).


9.11 Cost gate — nothing priced runs without explicit acceptance

Everything Cartos does today is free at the point of use: open imagery, open vector data, CPU inference. The capabilities that would change that — commercial satellite tasking, VHR archive pulls, a rented GPU tier — share one property: they spend the customer's money, per call, at a price known only at request time.

The rule (migration 0090): a priced operation never runs on the strength of a user having asked for it. It produces a quote; a human sees the price, the breakdown and what it buys; a human accepts; only then does it run.

Four properties make that more than a confirmation dialog:

fingerprinted the quote binds to a hash of the exact request, so an acceptance cannot be replayed against a larger, costlier one
single-use consumed atomically by exactly one run
expiring provider prices move; a stale quote is not consent to today's price
capped anyway an accepted quote still cannot breach the tenant's hard cap — consent and solvency are different questions

The agent may request and display a quote. It cannot accept one. The accepting identity comes from the JWT and is recorded on the quote. An agent that can accept its own quotes is a spending machine with extra steps.

Tables: cost_quote, usage_ledger (append-only, the billing substrate), tenant_budget (soft/hard caps + approval threshold). All ENABLE + FORCE RLS; tenant_budget carries the tenant-only policy since a budget has no owning_team.

Routes: POST /api/cost/quote, GET /api/cost/quotes, POST /api/cost/quotes/:id/{accept,decline}, GET /api/cost/budget.

Nothing is priced yet. No provider is wired, so the gate currently has nothing to gate. That is deliberate: it exists before the first paid capability, not bolted on after one.


9.12 Risk symbology & rich feature popups

Two upgrades to how catalog layers read, both platform-wide rather than per-edition, because both are driven by what is in the data instead of by configuration.

Risk symbology (cartos-web/src/lib/catalog-risk.ts). Any catalog layer whose features carry risk_score (0–100) and risk_tier is automatically rendered as severity: tier colour, tier-graduated size, two staggered pulsing target rings, and an optional name label. Nothing is configured per layer — which is the point. Guardian's 50 strike targets and its 2 500 collateral-risk POIs are the same code path, and any layer that later adopts the same two field names inherits the treatment. style.point.risk = { enabled: false } opts out.

Tier colours are imported from guardian-risk.ts rather than duplicated: those are pinned to migration 0071's categorical stops, and a second copy would drift the day someone edited one and not the other.

The orb is sized relative to the feature-type glyph it carries — 3× its width — not to a fixed pixel radius, and drawn at 50 % opacity. Both choices are forced by the glyphs: they are opaque, non-SDF bitmaps (so they cannot be tinted), which means an orb that merely matches a glyph shows no colour at all, and orbs large enough to read must be translucent or they hide each other.

Size steps by tier, not by raw score — a geometric ladder where each tier is 70 % of the one above it, derived from the RISK_TIERS order so it cannot drift from the tier list. A continuous score ramp made a 79 and an 81 nearly the same size even though they are different tiers, which is the distinction the map is communicating; a multiplicative step makes each tier read as a rank below the last. The glyph rides the same ladder, so the symbol scales as a unit and the orb:glyph ratio holds; the pulse rings follow automatically, being built from the same radius expression.

tier multiplier orb radius @z12 glyph @z12
Critical 0.980 21.6 px 14.4 px
High 0.686 15.1 px 10.1 px
Elevated 0.480 10.6 px 7.4 px
Moderate 0.367 † 8.1 px 7.4 px †
Low 0.367 † 8.1 px 7.4 px †

Floored. Unbounded, the ladder takes a Low glyph to ~3.5 px — a featureless dot. The glyph therefore floors, and because flooring the glyph alone would let it outgrow its shrinking orb, the orb floors too, at the multiplier that still leaves a visible ring of tier colour around a floored glyph (2.2× rather than the full 3×). ⚠ Consequence, deliberate: below the floor the symbols stop shrinking, so Moderate and Low render at the same size and are told apart by colour alone. Below that point "smaller" and "readable" cannot both hold.

Density changes what is legible, so the detector adapts to feature count:

sparse (≤ 200) dense (> 600)
name labels on off
pulsing rings High and above Critical only
glyph appears from z9 z12
orb below the glyph zoom damped ×0.5 damped ×0.22

Labels are off on dense layers for two reasons, not one: 2 500 overlapping names are unreadable regardless, and Guardian's POI names are Arabic while MapLibre's RTL text plugin is not loaded in this app. A user toggle (Aa in the layer row) overrides the default either way.

Rich feature popups (cartos-web/src/lib/feature-popup.ts). The previous popup was a 20-row key: value dump, which made a nuclear plant at risk 94 look exactly like a bus stop. Three tiers of treatment now apply automatically:

  1. Risk hero — tier-coloured band, the score as a large numeral, a gauge.
  2. Score drivers — the f_* model factors as labelled bars, and criticality/sensitivity as 1–5 pips (an ordinal, not a measurement).
  3. Grouped attributes — identity promoted to the header, internal plumbing hidden, keys humanised, values formatted by unit convention (*_km, *_m2, *_pct, years without a thousands separator, booleans as ✓/✗).

A layer carrying none of those fields still gets headings, humanised keys and formatted values, so the improvement is not a Guardian special case.

⚠ A bug worth remembering. The new label layer set no text-font, so MapLibre fell back to its default Open Sans Regular — which OpenFreeMap's glyph server 404s. The labels would have shipped silently blank: no error, no console warning, just nothing drawn. grid-overlay.ts had already solved this, so basemapTextFont moved to map-styles.ts and both call sites share it. Its fallback is now a single font, because MapLibre requests a font stack as one comma-joined path and OpenFreeMap 404s any stack it cannot serve whole.

⚠ How this was verified, and why it mattered. tsc and vite build were clean before any of the above was found. The real layers were pulled read-only from prod and driven through the actual reconciler in a headless MapLibre harness (CDP-driven Chrome, real WebGL, real glyphs, real 50/2 500-feature payloads). That harness caught the blank labels and two expressions that nested a zoom ramp inside a *, which MapLibre rejects outright — none of which a type check can see. Consistent with the Stage-0 lesson in §13.8: compiling is not evidence that a map renders.

Clustering and the heatmap (both per-layer toggles in the layer row, offered only where the data carries risk fields).

Clustering collapses a layer into severity-coloured clusters at or below z11, sized by point count and labelled with it. Clicking a cluster drills ingetClusterExpansionZoom then an eased fly to the split — and hovering one shows a pointer, since an expandable thing that looks inert is not much use. A cluster is deliberately kept out of the feature-popup hit set: it is an aggregate, not a feature, and the popup would otherwise render a card describing MapLibre's own cluster bookkeeping. ⚠ A cluster takes the colour of the worst tier inside it, never an average — one Critical site among forty Low ones is exactly what an operator must not lose at overview zoom, and a mean would bury it (clusterProperties: maxSev = max(tier rank)). ⚠ cluster cannot be changed on a live MapLibre source, so flipping the toggle tears the source down and rebuilds it; and a clustered source still emits its clusters as points, so every layer in the point stack gains a ["!", ["has", "point_count"]] filter or clusters render twice.

The heatmap answers a different question from the orbs — not "where is this asset and how bad is it" but "where does risk concentrate", which 2 500 discrete orbs cannot show at all. Weight is the risk score, so a cluster of Critical sites burns hotter than the same count of Elevated ones; raw density would let a car park of low-risk POIs outrank a lone nuclear plant. Five ramps ship (Risk tiers — the default, reusing the tier palette so heatmap and orbs agree about what red means — plus Inferno, Viridis, Turbo, Thermal). ⚠ Intensity is deliberately low: at the obvious values a 2 500-point layer saturates, every populated area pins to the top of the ramp, and the result looks like a heatmap while carrying no information. Turning the heatmap on also suppresses the glow halo and pulse rings, which otherwise muddy it.

Known limit. MapLibre's collision index contains only symbol layers, so a label can still land on a neighbouring feature's orb (circles are not in the index). The dark text halo keeps it readable, and the Aa toggle turns labels off; it is most visible in the dense Dubai cluster at continental zoom.

Live on prod (d4805bc) since 2026-08-01 — visible on Guardian under Critical Sites and POI Collateral Risk.


10. Data sources, integrations & keys

Integration Used for Key / config
Postgres + PostGIS The sovereign data fabric (tenant + team RLS) DATABASE_URL (:5433 on this box)
Keycloak SSO, JWT/JWKS, roles & groups; admin REST realm cartos; CARTOS_KEYCLOAK_ADMIN_CLIENT_SECRET
Anthropic The AI agent ANTHROPIC_API_KEY (agent 503s without)
NASA GIBS Satellite WMTS overlays none (public)
ArcGIS REST / Online / Hub Vector/raster service overlays + Living Atlas none for public services
STAC Imagery catalog discovery per-catalog
VesselFinder / aisstream Live vessels (Global Mobility) reused from EarthWatch3D .env
adsb.lol Live aircraft (Global Mobility) none
TomTom Traffic tiles TOMTOM_API_KEY (optional)
Overture Maps POI, roads, building footprints & divisions (Smart City overlay) none (public S3 GeoParquet via the sidecar); OVERTURE_SIDECAR_URL (default http://localhost:8200)
Esri 3D Buildings (I3S) Photoreal 3D building mesh (Smart City, deck.gl overlay) none (free public service)
Mapillary Crowd-sourced street-level imagery overlay (mapillary feature, §9.10) + the mapillary_street_imagery agent tool VITE_MAPILLARY_TOKEN (web, client token) / MAPILLARY_TOKEN (agent, server token) — free tokens from mapillary.com → Developers; external community data, CC-BY-SA
geoBoundaries Cartos Africa ADM0/ADM1 country + subnational boundaries (§8.4) none (public, CC-BY 4.0) — never GADM, per license posture
Natural Earth Cartos Africa rivers/lakes/settlements/built-up-extent reference vectors (§8.4) none (public domain)
Esri Parcel Fabric / generic ArcGIS parcel FeatureServer Cartos Africa's africa-parcel cadastral ingest — LADM-aligned parcel + party store (§8.4) none for public services; an optional ArcGIS token for secured services is held server-side per ingest call, never returned to the client
Microsoft Planetary Computer (STAC) Cartos Sentry's epoch resolution for real change detection — sentinel-2-l2a (optical), sentinel-1-rtc (SAR), landsat-c2-l2 (thermal) (§8.6); also Cartos Harvest's whole crop pipelinesentinel-2-l2a STAC search + keyless titiler band-math (NDVI/NDRE/NDWI/NDMI) fetched client-side by the panels (§8.7, live at harvest.cartosatlas.com — requires the img-src/connect-src CSP allowance for Planetary Computer) none (public STAC + titiler)
FAO GIEWS crop calendars Cartos Harvest's crop_calendars seed data (Kenya + Nigeria) (§8.7) none (public reference; hand-transcribed into migration 0083)
NASA FIRMS Cartos Sentry thermal/active-fire hotspots (VIIRS_SNPP_NRT) (§8.6) CARTOS_FIRMS_MAP_KEY (free; degrades gracefully unset)
AWS Global Seasonal Sentinel-1 Coherence (Kellndorfer et al.) Cartos Sentry's InSAR expected-coherence context layer, GDAL /vsicurl crop (§8.6) none (public COGs)
Umbra open-data archive Cartos Sentry commercial-tasking live SAR-collect search (§8.6) none (free static STAC)
NASA GIBS (historical) + VesselFinder/aisstream + adsb.lol (live traffic) Cartos Guardian historical-imagery time-slider + live AIS/ADS-B traffic overlays (§8.5) reuses the existing GIBS/mobility integrations above, no new keys

cartos-api loads a gitignored workspace-root .env via dotenvy; the Anthropic / aisstream / TomTom keys are reused from EarthWatch3D's .env.


11. HTTP API reference

All /api/* routes require a Bearer JWT except /healthz.

Method Route Purpose
GET /healthz Liveness (unauthenticated)
GET /readyz Readiness probe (unauthenticated) — 200 when the DB is reachable, 503 otherwise. Used by the compose healthcheck and web depends_on: cartos-api condition: service_healthy.
GET /api/me Returns the caller's TenantContext
GET /api/capabilities Catalog (catalog + features) + this user's effective enabled modules / features. Drives nav gating, feature panels, and admin config UIs.
POST /api/agent/chat Agent conversation (503 without Anthropic key)
POST /api/agent/export/docx Export an agent conversation as DOCX — POSTs {title, markdown} to the model-sidecar's markdown→docx renderer and streams the result back.
POST /api/export/geopdf Produce a georeferenced OGC GeoPDF — accepts a PNG capture + Mercator bbox, proxies to the model-sidecar's POST /render/geopdf (GDAL gdal_translate server-side), returns application/pdf. 502 when the model-sidecar is unreachable.
GET /api/kpi/smart-city/summary · /api/kpi/maritime/summary · /api/kpi/emergency-services/summary Dashboard KPI summaries
GET /api/smart-city/{incidents,assets,sensors,jurisdictions} Smart City data
GET /api/sensors/:id/readings Sensor time series
GET /api/maritime/{vessels,ports,port-incidents} Maritime data
GET /api/emergency/{units,incidents,incidents/history,traffic,cameras} Emergency Services data
GET /api/overture/:theme Overture Maps GeoJSON for viewport bbox (themes: places / buildings / transportation / divisions); proxies the Python sidecar. 502 when sidecar is unreachable.
GET /api/mobility/{aircraft,vessels,status} · /api/mobility/traffic/:z/:x/:y Global Mobility live feeds + traffic tile proxy
GET /api/mobility/history/vessels Historic AIS — distinct vessels in last N hours (?hours=48); max 500
GET /api/mobility/history/track Historic AIS — one vessel's ordered track (?mmsi=<int>&hours=48); max 5 000 fixes
GET /api/mobility/history/port-calls Historic AIS — per-port vessel count + fix count within port_aois (?hours=48)
GET /api/mobility/history/density Historic AIS — vessel density cells (?hours=48&cell=0.05); max 5 000 cells
GET /api/catalog/layers?module= Layer Catalog — caller-visible, non-hidden layers for a module (scope-filtered by tenant/group/role/user-type); module-gated in-handler
GET /api/catalog/kpi?module=&countries=&adm1= Layer Catalog — live per-layer feature counts for a module: {layer_count, total_features, layers}. countries (comma-separated ISO3) / adm1 (comma-separated ADM1 ids) narrow the counts — the Cartos Africa country-scope filter (§8.4); no-op on non-Africa layers.
GET /api/catalog/layers/:id/features?country_iso3=&adm1= Layer Catalog — a layer's internal PostGIS features as a GeoJSON FeatureCollection (scope-checked against the parent layer); same Cartos Africa country/ADM1 filter as the KPI route
GET /api/catalog/admin/layers Layer Catalog — all layers for the tenant — tech_admin
POST /api/catalog/layers Create a map layer — tech_admin
PATCH/DELETE /api/catalog/layers/:id Update / delete a map layer — tech_admin
PATCH /api/catalog/layers/:id/visibility Set visibility on | off | hiddentech_admin
GET/POST /api/catalog/datasources List (no secrets) / create a datasource — tech_admin
PATCH/DELETE /api/catalog/datasources/:id Update / delete a datasource — tech_admin
POST /api/catalog/datasources/:id/test-connection Bounded (8 s), type-aware connectivity probe; records status + result — tech_admin
GET /api/grid/assets Grid network assets (point + line geometry, health-coded)
GET /api/grid/outages Grid outages (point, status/cause/customers_affected)
GET /api/grid/work-orders Grid work orders (point, kind/status/priority/crew)
GET /api/grid/vegetation Grid vegetation segments (LineString, risk/span_miles)
GET /api/grid/parcels Grid parcels / ROW (Polygon, kind/encroachment/area_acres)
GET /api/grid/projects CIP capital projects (point, phase/status/budget_musd/pct_complete)
GET /api/grid/kpi Aggregate Grid KPI summary (assets, outages, work orders, veg, parcels, CIP projects)
GET /api/grid/integrations List SoR connector registry rows (kind, name, target, status, last_sync, last_result)
POST /api/grid/integrations Register a connector — tech_admin; body {kind, name, target, status?}
PATCH /api/grid/integrations/:kind Update connector name/target/status/config — tech_admin; partial patch (null fields keep existing value)
DELETE /api/grid/integrations/:kind Remove a connector registration — tech_admin
POST /api/grid/integrations/:kind/sync Run a mock connector sync; returns sync-result JSON. file_csv returns 400 (use import endpoint).
POST /api/grid/integrations/file_csv/import Upload work-order CSV; body {csv: "..."}; returns {imported, created, updated, skipped}
GET /api/industry/projects?edition= Cartos Milepost/Blueprint projects the caller may see (default-deny; admins see all) — see §8.3
PATCH /api/industry/projects/:id Update a project's status/health/location — requires edit or admin access on it
GET /api/industry/access?edition= List access grants for an edition — user_admin
POST /api/industry/access Grant a project or the whole portfolio to a group or user — user_admin, or a project-admin delegating within their scope
DELETE /api/industry/access/:id Revoke a grant — user_admin, or a project-admin delegating within their scope
/api/sentry/* (23 routes — watchlist, incidents + transitions/confirm/notes, imagery/damage/coherence/change-raster, reports, tasking, alerts, uploads) Cartos Sentry — see the full route table in §8.6
/api/africa/parcels/* (5 routes — ingest (tech_admin), list, summary, :id (GET/PATCH), export) Cartos Africa cadastral parcel fabric — LADM-aligned (ISO 19152) parcel + party store, two ingest adapters (Esri Parcel Fabric information model + generic ArcGIS cadastre FeatureServer) landing in one schema; export returns LADM-shaped GeoJSON — see the parcel-fabric subsection in §8.4 (merged to main, deployed + verified on prod)
/api/harvest/* (17 routes — crop-calendar, crop-index, crop-type, crop-health, crop-yield, crop-water, field-candidates, parcels + /summary + /:id (GET/PATCH) + /:id/owner, portfolio-risk, products + /:id, farmers (GET/POST), registry/coverage) Cartos Harvest STAC crop pipeline + persisted products (M03) + farmer registry — see §8.7 (live at harvest.cartosatlas.com)
GET/POST/DELETE /api/layers · /api/layers/:id Persisted overlay layers
GET /api/field/apps List field apps with module assignments (tenant + group filtered)
POST /api/field/apps Create field app — tech_admin
GET /api/field/apps/:slug Fetch one field app with module assignments (mobile pulls definition here)
PATCH /api/field/apps/:slug Update meta or definition; bumps version when definition changes — tech_admin
DELETE /api/field/apps/:slug Delete app + captured features (cascade) — tech_admin
PUT /api/field/apps/:slug/modules Set module assignments (replaces full set) — tech_admin; validates against module catalog
POST /api/field/apps/:slug/sync Store-and-forward capture batch; idempotent; returns {received, inserted, duplicates}
GET /api/field/apps/:slug/features List captured features; optional ?limit=N (max 5 000)
GET /api/field/captures/:module_id Module's assigned apps + features; gated in-handler by host module (cap_guard exempted)
POST /api/field/media Upload photo/signature (raw body, ≤ 25 MB; ?ext= or Content-Type for extension)
GET /api/field/media/*key Download media; tenant-scoped; path-traversal guarded
GET /api/derived/datasets List derived datasets (tenant+group scoped); ?aoi=w,s,e,n for AOI-bounded provenance query; ?limit=N (max 5 000)
POST /api/derived/datasets Mint a dataset — tech_admin
GET /api/derived/datasets/:id One dataset + its inputs lineage edge IDs
GET /api/reports/sessions List analysis sessions (tenant+group scoped) — id/title/status/created_at
POST /api/reports/sessions Persist a SessionResulttech_admin
GET /api/reports/sessions/:id Full SessionResult body
GET /api/reports/sessions/:id/report.html Rendered HTML report; ?narrate=true adds a grounded LLM narrative
GET /api/reports/sessions/:id/report.docx DOCX export (proxied to the Python sidecar POST /render/docx)
GET /api/jobs List model jobs (tenant+group scoped)
POST /api/jobs Submit a model job — tech_admin; returns {job_id, status, kind}. routing.tier = "local" (CPU/GPU sidecar at MODEL_SIDECAR_URL) or "full" (GPU sidecar at MODEL_GPU_SIDECAR_URL).
GET /api/jobs/:id Job status + outputs
GET /api/jobs/backends Reports both compute tiers — local + gpu — each with device, model, health from /healthz. Used by the Analysis panel to show availability.
POST /api/jobs/:id/cancel Cancel a queued/running job — tech_admin
PATCH /api/admin/scope Set a row's owning_team (admin)
PATCH /api/admin/modules/:module_id { enabled } Provider Admin — toggle a module for this tenant. 403 without provider_admin or admin.
PATCH /api/admin/features/:module_id/:feature_id { enabled } Tech Admin — toggle a feature within a module (default or advanced cross-add). 403 without tech_admin or admin.
GET /api/admin/users/:user_id/modules User Admin — list tenant modules + whether this user may access each.
PATCH /api/admin/users/:user_id/modules/:module_id { enabled } User Admin — grant or revoke a module for a specific user (deny-override).
GET /api/admin/users · /api/admin/groups Keycloak admin (503 without secret)
PUT/DELETE /api/admin/users/:user_id/groups/:group_id Add / remove user ↔︎ group
POST /api/cost/quote Price an operation and DO NOT run it — returns a quote with price, breakdown and expiry (§9.11)
GET /api/cost/quotes Recent quotes for this tenant
POST /api/cost/quotes/:id/accept A human authorises the price. Identity comes from the JWT and is recorded; the agent cannot call this on a user's behalf
POST /api/cost/quotes/:id/decline Decline an open quote
GET /api/cost/budget?period=day|month|total Spend, soft/hard caps, remaining

12. The marketing website & GTM artifacts

website/ — live at https://cartosatlas.com

cartos-pricing/ — commercial model & business case

To regenerate: python cartos-pricing/build.py, then (for docx/pdf) the rasterize + make_docx.py steps in cartos-pricing/README.md.


13. Phase 2 — Analyst Intelligence Layer (Wave 2A built)

Status: Wave 2A fully built (PRs #28–#33, #35–#36, #37, #40); Spike A delivered real segmentation and real two-epoch change detection (PRs #45, #49); compute-tier selection + GPU sidecar deployable artifact (PRs #46, #47); Swipe Compare capability feature (PR #48); MCP server (PR #43); Gaussian splats in MapLibre + SuperSplat SOGS importer (PR #63, Wave 2C M04 client-serving). The full loop — queue → real model sidecar → derived-dataset → report → MCP exposure — is in place and CPU-verified end-to-end. Remaining: multispectral geospatial FM (Prithvi/TerraMind on a GPU; the upgrade path is documented); M04 server-side splat generation (needs GPU — the client viewer is complete); M05 3D building reconstruction.

Phase 2 turns the sovereign platform into one that reasons about change, derives analysis-ready data, and writes auditable reports. Five modules in three waves, with new Gate-0 infra built in Wave 2A:

ID Module Wave Status
M03 GeoAI agents & derived datasets (lineage-tracked — the moat) 2A Built
M02 Comprehensive reporting (HTML/DOCX/PDF from a session) 2A Built
M01 Temporal change detection (two epochs → change layer + confidence) 2B Built — real model (SegFormer diff over real Sentinel-2 tiled imagery)
M04 Gaussian-splat serving (photoreal 3D in the map) 2C Client viewer built (PR #63 — three.js overlay canvas + SuperSplat SOGS importer in MapLibre native modules; see §9.3). Server-side splat generation (from imagery/LiDAR) requires a GPU and is gated.
M05 AOI → 3D LOD 2.5/3 buildings (reconstruct from imagery) 2C Planned

The three Gate-0 build contracts in cartos-phase2/docs/phase-2/specs/ (derived-dataset-schema.md, session-result-object.md, gpu-job-contract.md) define the spec that Wave 2A implements.

On mock vs real (Spike A / PR #45, #49): both model handlers now run real models on real imagery when the ML stack is present. infer.segment runs SegFormer-b0 (fine-tuned on ADE20K) over real Esri World Imagery fetched for the AOI — 150 ADE classes bucketed to built / vegetation / water / bare. change.detect fetches real Sentinel-2 tiles (or any {z}/{x}/{y} tile source) for two dated epochs, segments each with SegFormer, diffs per cell, and returns typed changes (new_structure / removed / modified, from→to class, confidence). Deterministic mocks remain as guarded fallbacks (no ML stack, SEGMENT_REAL=0, or no epochs provided). Accuracy caveat: ADE20K is a ground-scene model; accuracy on overhead imagery is rough. The upgrade path to a multispectral geospatial foundation model (Prithvi-EO / TerraMind) on a GPU is documented in model-sidecar/README.gpu.md. The async queue, SKIP-LOCKED worker, lineage graph, reporting pipeline, and web UI are all production-shaped and unchanged by the model swap.

cartos-phase2/docs/strategy/ip-timing.md is time-sensitive (public disclosure affects patentability).


13.1 M03 Derived-dataset graph (the moat)

Migration: 0013_derived_dataset.sql

The derived_dataset table is the lineage-tracked store every GeoAI tool writes into. It is the platform's durable moat: every analysis result is versioned, provenanced, and queryable over an AOI.

Schema — derived_dataset:

Column Type Notes
id UUID PK gen_random_uuid()
tenant_id TEXT Tenant scope
kind TEXT segmentation, change, splat, etc.
name TEXT Human label
aoi geometry(Polygon,4326) AOI polygon; GiST-indexed for spatial queries
version INTEGER Auto-incremented; supersedes self-FK enables versioned chains
supersedes UUID FK Previous version of this dataset (nullable)
tool_ref TEXT Job kind / tool name (e.g. infer.segment)
tool_version TEXT Model or tool version string
params JSONB Run parameters
session_id UUID FK Owning analysis session (nullable)
storage_uri TEXT MinIO/S3 key for large outputs (nullable; small mock outputs inline in metadata)
licensing JSONB IP/licensing annotations
status TEXT pending, ready, superseded
metadata JSONB Arbitrary output metadata + inline result GeoJSON (mock)
owning_team TEXT Group-access scope (same pattern as all tenant data)
created_by JSONB {"actor":"job","id":"<job_id>"} or {"actor":"user","id":"..."}
created_at TIMESTAMPTZ

Lineage edges — dataset_input:

dataset_input (output_id → derived_dataset, input_id → derived_dataset) — a directed edge declaring that output_id was produced from input_id. The worker inserts these automatically from any UUID-valued entry in the job's inputs object.

Data layer (crates/cartos-data/src/derived.rs):

API surface (crates/cartos-api/src/derived.rs):

Method Route Auth Purpose
GET /api/derived/datasets any tenant user List datasets; ?aoi=w,s,e,n for provenance over AOI; ?limit=N
GET /api/derived/datasets/:id any tenant user One dataset + its inputs lineage IDs
POST /api/derived/datasets tech_admin Mint a dataset manually (GeoAI tools use the data layer directly)

13.2 M02 Comprehensive reporting

Migration: 0014_analysis_session.sql

The analysis_session table stores a full SessionResult as JSONB (group-scoped). Reports are rendered deterministically from the structured fields — nothing is invented — with an optional grounded LLM narrative layered on top.

SessionResult object (crates/cartos-core/src/session_result.rs):

The canonical schema (schema_version = 1) records an ordered Operation trace → methodology, plus KPIs, findings, assumptions, and figures:

Field Type Notes
schema_version u32 1
session_id String Unique session identifier
tenant_id String
title String
aoi GeoJSON Polygon (opt)
inputs Vec<String> Consumed dataset IDs
produced Vec<String> Output dataset IDs
operations Vec<Operation> Ordered tool trace (seq, tool, tool_version, params, input_refs, output_refs, summary, duration_ms)
kpis Vec<Kpi> label, value, unit (opt), trend (opt)
findings Vec<Finding> label, confidence (opt), detail (opt)
assumptions Vec<String> Caveats and data-quality assumptions
figures Vec<Figure> id, kind (map/chart/table), caption
started_at / finished_at DateTime<Utc>
status SessionStatus ok, partial, failed

Report renderer (crates/cartos-api/src/report.rs):

render_report_html(sr, narrative) produces a self-contained HTML report (inline CSS, Cartos design tokens — same #0A1A2F navy / #00A3E0 accent palette as the guide and the app). Sections: cover (title, status badge, session ID, tenant, timestamps) · optional grounded narrative · KPI strip · Findings table · Methodology & Process (from the operations trace — the audit trail) · Assumptions · Figures (captions; image embedding arrives with the viewport-capture slice) · Data Lineage (inputs / produced datasets).

Grounded LLM narrative (optional): ?narrate=true on the HTML report route passes the SessionResult's structured fields (KPIs, findings, assumptions, operations, figure captions — nothing else) to a strictly-grounded Anthropic prompt. The model is told: use ONLY those numbers and claims, no markdown, no headings, 2–4 paragraphs. The deterministic skeleton renders regardless; the narrative fails gracefully to None if the API key is unset or the call errors.

DOCX export: GET /api/reports/sessions/:id/report.docx proxies the session body to the sidecar's POST /render/docx (model-sidecar/render.py, python-docx). Mirrors the HTML skeleton: cover, KPIs table, findings table, methodology table, assumptions bullets, figures, lineage. The proxy streams the bytes back as a download attachment.

PDF export: headless-Chrome print of the HTML report. Two paths:

  1. In-browser: the rendered HTML (opened via "Open" in the Analysis overlay) can be printed to PDF via the browser's native print.
  2. Operator tool: tools/export_report.py --session <uuid> fetches the report, writes .html + .pdf (Chrome) + .docx (via API) to a local directory. Usage: python tools/export_report.py --session <uuid> [--api http://localhost:4000] [--narrate].

API surface (crates/cartos-api/src/report.rs):

Method Route Auth Purpose
POST /api/reports/sessions tech_admin Persist a SessionResult
GET /api/reports/sessions any tenant user List sessions (id/title/status/created_at)
GET /api/reports/sessions/:id any tenant user Full SessionResult body
GET /api/reports/sessions/:id/report.html any tenant user Styled HTML report; ?narrate=true adds LLM narrative
GET /api/reports/sessions/:id/report.docx any tenant user DOCX download (proxied to sidecar)

13.3 Async model-job tier & model sidecar

Migration: 0015_model_job.sql

The model_job table

Postgres SKIP-LOCKED queue with a full job lifecycle:

Column Type Notes
id UUID PK
tenant_id TEXT
idempotency_key TEXT UNIQUE(tenant_id, idempotency_key) partial — prevents duplicate submits
kind TEXT Job kind string matching a sidecar handler (e.g. infer.segment, change.detect)
status TEXT queuedrunningsucceeded | failed | cancelled
inputs JSONB Input refs (dataset IDs or arbitrary params)
params JSONB Run parameters passed to the sidecar
aoi JSONB GeoJSON Polygon AOI
routing JSONB {sovereign: bool, priority: "normal"|"high"}
session_id UUID FK Owning session (nullable)
owning_team TEXT Group-access scope
outputs JSONB Produced derived dataset references (written on completion)
metrics JSONB {duration_ms, device, est_cost_usd} — reported by sidecar
error JSONB Error detail on failure
progress FLOAT 0–1 (future streaming progress)
claimed_by TEXT Worker ID that claimed the job
created_by JSONB
created_at / started_at / finished_at TIMESTAMPTZ

Data layer (crates/cartos-data/src/jobs.rs): submit_job (idempotent), get_job, list_jobs (group-scoped), claim_next_job (FOR UPDATE SKIP LOCKED — safe for multiple workers), complete_job, fail_job, cancel_job.

The background worker (crates/cartos-api/src/jobs.rs)

run_worker(state) is spawned in main (as a tokio::spawn) when the DB is reachable. It polls the queue with a 1.5 s idle sleep, claims one job at a time (SKIP-LOCKED makes it safe to run multiple workers), calls POST /infer on the sidecar, and for each output in the response:

  1. Parses any UUID-valued entry in job.inputs as a lineage edge.
  2. Mints a derived_dataset row via derived::create_dataset.
  3. Calls jobs::complete_job writing the produced dataset IDs into outputs.

On sidecar error the job is marked failed with a retryable error JSON. The worker is intentionally simple — it does not retry; a failed job can be resubmitted.

The model sidecar (model-sidecar/main.py)

Stateless by design. It never touches the database. The only contract with cartos-api is POST /infer (input + AOI JSON → outputs JSON) and GET /healthz. This means the sidecar relocates from CPU to a local GPU to a customer's GPU cluster purely by changing where it is deployed — no code change, no DB credential.

GET /healthz — the Gate-0 health check:

{

  "ok": true,

  "gpu": false,

  "device": "cpu",

  "models_loaded": ["infer.segment@segformer-b0-ade", "[email protected]"],

  "segment_real": true

}

segment_real is true when the ML stack (torch + transformers) is installed and SEGMENT_REAL is not 0. Device is auto-detected: tries torch.cuda.is_available(), falls back to nvidia-smi, then "cpu". Override with MODEL_DEVICE.

POST /infer — job-kind dispatch:

{

  "kind": "infer.segment",

  "tenant_id": "...",

  "session_id": "...",

  "inputs": {},

  "params": { "grid": 8 },

  "aoi": { "type": "Polygon", "coordinates": [...] }

}

Response carries outputs (array of {kind, name, result, metadata, tool_version}), metrics ({duration_ms, device, est_cost_usd}), and summary.

POST /render/docx — M02 DOCX export (model-sidecar/render.py): receives a SessionResult JSON, returns .docx bytes via python-docx. Cartos-branded (cover · KPIs · findings · methodology · assumptions · figures · lineage). Proxied at GET /api/reports/sessions/:id/report.docx.

Compute-tier selection (PR #46): the Analysis panel offers two tiers:

Tier UI label sidecar env var
local Local (CPU/GPU) MODEL_SIDECAR_URL (default :8300) always configured
full Full GPU MODEL_GPU_SIDECAR_URL optional; unset → tier rejected with clear tier_unavailable error, no silent fallback

Jobs carry routing.tier; the worker routes accordingly. GET /api/jobs/backends reports both tiers' availability, device, and model version (probes /healthz on each configured sidecar).

GPU sidecar deployable artifact (PR #47): model-sidecar/Dockerfile.gpu — CUDA 12.4 + cuDNN, cu124 torch wheels, bakes SegFormer-b5 (640) at build time so first inference is offline and fast. SEGMENT_MODEL_ID env selects the model (b0 for CPU, b5 for GPU; larger models and the Prithvi/TerraMind multispectral path are documented in README.gpu.md). Deploy to any GPU host (RunPod, Lambda, AWS g5, Azure NC, on-prem/sovereign GPU box) with the NVIDIA Container Toolkit; set MODEL_GPU_SIDECAR_URL and restart cartos-api.

Architecture decision: cartos-api owns the queue AND the worker; the sidecar is stateless (never touches the DB). This realizes the "hybrid: local CPU/GPU now, wired for a sovereign/customer GPU cluster later" plan from the Gate-0 contract.

Run (dev): cd model-sidecar && python -m uvicorn main:app --host 0.0.0.0 --port 8300

Dependencies: pip install fastapi uvicorn python-docx pillow (+ torch transformers for the real segmentation model; optional — mock fallback works without them). System package gdal-bin is required for the POST /render/geopdf endpoint (both Dockerfile and Dockerfile.gpu install it via apt-get; for a native dev install run apt-get install gdal-bin or the equivalent).


13.4 M01 Temporal change detection — real model

Status: built — real model (PRs #45, #49). Both the segmentation and change detection handlers run a real model on real imagery. Deterministic mocks remain as guarded fallbacks.

Spike A — real segmentation (segment_real.py, PR #45)

infer.segment runs SegFormer-b0 fine-tuned on ADE20K (nvidia/segformer-b0-finetuned-ade-512-512 from HuggingFace, via transformers). The pipeline:

  1. Fetch a 512 × 512 RGB image of the AOI from Esri World Imagery (fetch_aoi_image — public, no auth).
  2. Run SegFormer → 150 ADE class logits, upsampled to grid × 8 resolution.
  3. Bucket each pixel's class label to one of four land-cover classes (built / vegetation / water / bare) using a keyword match against ADE20K label strings.
  4. Summarize each grid cell by dominant bucket → {i, j, cls, confidence}.
  5. Output a segmentation derived dataset (GeoJSON polygons).

The model is lazy-loaded once and cached; available() returns True only when torch + transformers are importable. Model and tensor move to CUDA if detected, else CPU. SEGMENT_MODEL_ID selects the model (default nvidia/segformer-b0-finetuned-ade-512-512; the GPU Dockerfile sets nvidia/segformer-b5-finetuned-ade-640-640). Accuracy caveat: ADE20K is a ground-scene model, not a remote-sensing model. Over overhead imagery, results are plausible but rough. The upgrade to a multispectral geospatial foundation model (Prithvi-EO / TerraMind) is the documented next step.

Real two-epoch change detection (PR #49)

change.detect now runs a real path when two epoch imagery sources are supplied:

  1. Tiled imagery input (fetch_aoi_tiles): mosaic XYZ tiles covering the AOI bbox from any {z}/{x}/{y} tile URL (Planetary Computer Sentinel-2, NASA GIBS, Esri, OSM, etc.) — auto-selecting a zoom where the tile count is ≤ 24, then cropping to the exact bbox. This is a higher-resolution path than the single-export approach.
  2. Segment epoch A and epoch B independently using the SegFormer path above.
  3. Diff per grid cell: if the dominant class changed, record the cell as a change with cls = "new_structure" | "removed" | "modified", from, to, and confidence = min(conf_a, conf_b).
  4. Output a change derived dataset (GeoJSON polygons of changed cells only).

Web UI flow (updated AnalysisOverlay):

Mock fallback: when no epoch imagery is supplied, or the ML stack is unavailable, the deterministic mock (~18% of cells changed, hash-based) remains active and is clearly labelled.

change_cells function signature (segment_real.py):

change_cells(bbox, epoch_a, epoch_b, grid) → (changed_cells, distribution, pct)

changed_cells is a list of {i, j, cls, from, to, confidence}. The Rust worker is unchanged — all the model logic is in the sidecar.


13.5 Phase 2 web UI — Analysis overlay (capability feature)

Status: built and wired as a first-class capability feature (PRs #35, #36); merged to main. Migration 0016 seeds the feature on for the dev tenant.

Source: cartos-web/src/features/analysis/AnalysisOverlay.tsx

The Analysis overlay is no longer a floating pill — it is now a controlled DockablePanel that the PanelDock launcher rail opens and closes (the same pattern as Overture, STAC, and other feature panels). It is registered in the catalog as the analysis feature ("Analyst Intelligence") and is:

Props accepted: mapRef, mapReadyRef, mapEpoch, open, onClose, panelId, storageKey — the same controlled-panel contract as OvertureOverlay.

The overlay wires together the full Phase 2 loop in the browser:

  1. Compute tier: a toggle (Local / Full GPU) selects the routing tier for the job. The panel reads GET /api/jobs/backends on mount and shows which tiers are available and their device/model.
  2. Run (Segmentation): select "Segmentation" → POST /api/jobs with kind: "infer.segment" and the current map view as the AOI → poll GET /api/jobs/:id every 1.2 s → on success, fetch the produced DerivedDataset from /api/derived/datasets/:id.
  3. Run (Change detection): select "Change detection" → "Find Sentinel-2 imagery" triggers a STAC search over the view; pick epoch A (before) and epoch B (after) from the results list; "Run" submits change.detect with the two tile URLs and dates.
  4. Render: the dataset's metadata.result GeoJSON is painted on the map as fill + line layers, colour-coded by classification class (built → red, vegetation → green, water → blue, bare → purple; change: new_structure → amber, removed → red, modified → yellow). KPIs from metadata.kpis are shown in the panel.
  5. Save as report: constructs a SessionResult from the dataset (one operation trace entry, KPIs forwarded) and POSTs it to /api/reports/sessions.
  6. Open / DOCX: lists saved sessions; "Open" fetches the HTML report (getBlob) and opens it in a new tab; "DOCX" downloads via getBlob from /report.docx.

Run and Save are gated to tech_admin. The overlay re-establishes its map layers after a style reload (via mapEpoch).


13.6 MCP server — Cartos as an AI tool host

Status: built — PR #43. Verified: stdio handshake + live tool calls proxied to cartos-api.

What it is: mcp-server/ (TypeScript, Node 20+, @modelcontextprotocol/sdk 1.x, zod). Compiled to dist/index.js. Lets an MCP-capable AI host — Claude Desktop, Claude Code, GitHub Copilot, Gemini via a function-calling bridge — "ask Cartos" by calling the platform's live data routes as MCP tools, with full tenant/group scoping enforced by cartos-api.

Design: read-only proxy over cartos-api. The MCP server holds no state, no DB credentials, no model weights. Authentication is a Keycloak bearer token (CARTOS_TOKEN) supplied by the operator. All scoping (tenant, team groups, module enablement) is enforced by cartos-api on each request — the MCP layer cannot bypass it.

Tools exposed:

Tool Route proxied Purpose
cartos_who_am_i GET /api/me Tenant, tier, roles, team scopes for the connected token. Call first.
cartos_capabilities GET /api/capabilities Enabled modules + features for this tenant.
cartos_smart_city_incidents GET /api/smart-city/incidents Active Smart City incidents (id, kind, severity, location, status).
cartos_maritime_vessels GET /api/maritime/vessels Tracked vessels (position, type, status).
cartos_emergency_units GET /api/emergency/units Emergency units + current availability.
cartos_derived_datasets GET /api/derived/datasets[?aoi=w,s,e,n] Lineage-tracked GeoAI outputs. Optional aoi bbox for provenance over an area.
cartos_reports GET /api/reports/sessions Saved analysis sessions (id, title, status, created_at).
cartos_report GET /api/reports/sessions/:id Full structured SessionResult (KPIs, findings, methodology trace).

Config: two env vars — CARTOS_API_URL (default http://localhost:4000) and CARTOS_TOKEN (a valid Keycloak bearer token for the Cartos Atlas tenant).

Build and wire into Claude Desktop:

cd mcp-server

npm install

npm run build      # → dist/index.js

Add to claude_desktop_config.json (Claude Desktop) or the equivalent for your host:

{

  "mcpServers": {

    "cartos": {

      "command": "node",

      "args": ["<abs-path>/mcp-server/dist/index.js"],

      "env": {

        "CARTOS_API_URL": "http://localhost:4000",

        "CARTOS_TOKEN": "<paste a token>"

      }

    }

  }

}

Get a dev token:

curl -s -X POST http://localhost:8080/realms/cartos/protocol/openid-connect/token \

  -d grant_type=password -d client_id=cartos-web \

  -d username=demo -d password=•••••••• | jq -r .access_token

Dev tokens expire; for a long-lived integration use a service-account / client-credentials grant.

Transport: stdio (the Claude Desktop/Code standard). stdout is the MCP channel — the server logs only to stderr. An HTTP/SSE transport for non-desktop hosts and a service-account auth flow are the documented next steps.

Write actions: not exposed by design. Running a GeoAI job, saving a report, or setting data scope should be added deliberately with an auth review before exposure to an orchestrating AI.


13.7 Roadmap — remaining Phase 2 waves

Item Status
Multispectral geospatial foundation model (Prithvi-EO / TerraMind) — swap SegFormer for a true remote-sensing FM over Sentinel-2/HLS multispectral input via STAC; needs ≥16 GB VRAM Next upgrade — path documented in model-sidecar/README.gpu.md (terratorch + HLS STAC fetch); gated on GPU target
M02 figure capture — viewport screenshot attached to SessionResult figures Planned — requires a map-ref context in AppShell so the global agent panel can capture the active map
MCP write actions (run a GeoAI job, save a report) Planned — add deliberately with auth review
MCP HTTP/SSE transport Planned — for non-stdio hosts
Map Export & Mapbook (PNG, cartographic PDF, multi-page atlas) Built — PR #51, capability map-export (see §9.2)
GeoPDF / GDAL georeferencing — OGC GeoPDF output (server-side gdal_translate, EPSG:3857, opens in QGIS/ArcGIS) Built — PR #53 (see §9.2 OGC GeoPDF export)
Compare: Swipe | Side-by-side toggle + full grouped layer catalog (Basemap/Esri/GIBS + Custom URL) Built — PR #56 (see §9 Compare)
Compare: bidirectional camera sync (both panes interactive, guard-flag feedback-loop prevention) + "Current map" source option (clones live host map style — basemap + active data layers) for side A and/or B Built — PR #58 (see §9 Compare)
Map Export: EarthWatch3D parity — left-dock panel consolidation, Create Map | Mapbook toggle, four layout templates, full plotter paper sizes up to 48×60 + Custom, Data (CSV) section Built — PR #56 (see §9.2)
3D Data — Point Clouds: true geospatial placement (Georeferenced | Center of map toggle), proj4 CRS reprojection for projected LAS/LAZ (any UTM zone + EPSG:4326/3857 + raw +proj=…), density slider (2–100 %), 90° pitch in native modules Built — PR #57 (see §9.3)
GeoPDF oblique / 3D GCP warping — georeference non-nadir (tilted/oblique) views via control-point warping Planned follow-up to PR #53
M04 Gaussian splats — client viewer (three.js overlay + SuperSplat SOGS importer in MapLibre) Built — PR #63 (see §9.3)
M04 Gaussian splats — server-side generation (from imagery/LiDAR on a GPU) Wave 2C — gated on GPU target
Cartos Grid — Electric Utility edition (nine department module surfaces, configurable KPI dashboard, engine + editions model) Built — PR #67 (see §8.1)
Cartos Grid — live operational data (five tables: assets/outages/work orders/vegetation/parcels; RLS; seed; live map layers; KPI API; three agent tools) Built — PRs #92–#95 (see §8.1 Live operational data)
Cartos Grid — all seven SoR connectors end-to-end (grid_integrations registry, OMS/ADMS + Maximo + SAP PM + Esri UN + P6 + MS Project mock feeds, File/CSV real import, grid_projects CIP table, Capital Program module live, dashboard SoR chip strip, six opt-in schedulers) Built — PRs #102, #104–#109 (see §8.1 Systems-of-record integrations)
Historic AIS analytics — vessel_positions time-series, port_aois, track replay, density heatmap, port-call counts, opt-in live ingester Built — PRs #96–#98 (see §8.2)
Historic AIS analytics pushed to Cesium MDA globe via cartos.ais.set bridge (AisBridgeControls.tsx, AisPayload in iframe-bridge) Built — PR #103 (see §8.2)
Platform theme system (10 palettes, CSS token system, per-module overrides, theme-matched basemaps) Built — PR #67 (see §9.4)
Cartos engine rebrand (CartosMark.tsx, edition-neutral shell) Built — PR #67
Cartos Tower, Milepost & Blueprint — three more industry editions (29 department modules total via the shared IndustryModule surface, edition.ts hostname detection extended to 5 editions, per-project/portfolio-wide access-grant RBAC for Milepost/Blueprint, dedicated subdomains) Built 2026-07-07 (see §8.3). Live operational data + SoR connectors (mirroring Cartos Grid) are the next increment.
Time of Day — sun-position lighting panel (dependency-free NOAA/SunCalc-style solar engine; drives 3D building shading, hillshade illumination, sky) + 3D-aware Measure panel (Distance/Area/Height, terrain-derived slope length, m/ft) — capability features time-of-day/measure in Smart City, Maritime, Emergency Services Built 2026-07-10 (see §9.8). Shadows toggle is a hillshade-contrast approximation; real deck.gl cast shadows are a documented follow-up.
Map tools UI + navigation suite — shared "widget kit" HUD design language; Draw & Annotate capability feature (Select/Point/Line/Polygon/Rectangle, styling, GeoJSON export); always-on Navigate compass rail (camera controls, multi-CRS coordinate readout, UTM/MGRS grid overlay, replaces MapLibre's default nav controls); universal drag+resize on every floating map surface incl. the ArcGIS identify popup Built 2026-07-11 (see §9.9)
Mapillary crowd-sourced street imagery — portable mapillary capability feature (coverage vector overlay + click-for-photo popup, default-on in the native trio + all three industry editions) and the cross-module mapillary_street_imagery agent tool (bbox/point+radius search, pano/year filters) Built 2026-07-13 (see §9.10)
Cartos Africa — the sixth live edition, a standalone continental Government edition for African governments/NMAs/ministries (9 continental Government modules, country/ADM1 scope, foundation-layer fabric, Continental Command dashboard, real OSM/Natural Earth data in all 9 modules, africa_scope agent tool) + engine-wide ride-alongs (global "Satellite · Esri" basemap, agent/data-source module-scoping fix, theme-following floating panels, edition-switcher pulldown, holographic point symbology + feature icons, vector-over-raster ordering) Built + merged to main (PR #127) — LIVE at africa.cartosatlas.com (see §8.4). Next: real parcel segmentation, COG/SAR foundation layers, deeper per-country data.
Cartos Harvest — the ninth edition, STAC-native crop intelligence (7 modules — command + crop type/health/yield/water/boundaries/risk; live STAC orchestration over Microsoft Planetary Computer Sentinel-2; v0.2 KMeans temporal-phenology crop classification; a composed 0–100 agri-risk score + index-insurance trigger; a farmer registry; persisted derived products (M03) + a Saved Products panel; reuses the Africa country-scope + foundation fabric; five agent tools) Merged to main, tagged cartos-harvest-v1 — LIVE at harvest.cartosatlas.com (see §8.7). Next: verify the supervised Prithvi/Clay GPU classifier scaffold, UAE greenhouse anchor, per-AOI agent tools.
Opus 5 extended-thinking round-trip fix — ContentBlock::Thinking/RedactedThinking in the Anthropic client (crates/cartos-agent/src/llm.rs), fixing intermittent 502s on reasoning-heavy agent turns, platform-wide Built — shipped with the Cartos Harvest merge (see §7 The AI agent)
M05 AOI → 3D LOD 2.5/3 buildings (reconstruct from imagery) Wave 2C — planned
Cloud-native data layer — object-storage format spikes (PMTiles serverless vector tiles from MinIO, GeoParquet + DuckDB-WASM in-browser analytics, FlatGeobuf bbox-streamed vector, COG range-read raster, Turf.js client-side geoprocessing, broad layer export); all on existing MinIO, no new services; Grid Data panel toggles Spiked / proof-of-concept — PRs #113–#117 (see §9.5). Demo data only. Production hardening (signed URLs, tippecanoe, real rasters, partitioned GeoParquet) is TBD in Phase 2 M03.
Deployment-readiness hardening (Phases A–D) — JWKS live refresh (unknown-kid refetch + 10-min loop), CARTOS_REQUIRE_TENANT_CLAIM, migration 0033 RLS on all tenant tables, opt-in cartos_app non-superuser DB role, CI gate (ci.yml + cargo test 4/4), DB pool 8→50 + entitlement cache + shared HTTP clients + router timeout/concurrency + 15 s KPI cache, FlatGeobuf debounce + vite code-splitting + tab-visibility polling, GET /readyz + compose healthcheck, non-root API container, iframe-bridge origin pinning, JSON logs + request-ID, migration 0034 dedup guard. Done (code-level) — see docs/deployment-readiness.md for the full done-vs-deferred ledger. Deferred infra: CDN, API replicas, managed Postgres/PgBouncer, the demo password → Cloudflare Access, Cesium proxy auth, map-key restrictions.

14. Phase 3 — Cartos Field

Phase 3A + 3B — built and merged to main. See §7.5 for the full implementation detail.

Component Status
Sync/field backend (migration 0009, /api/field/*, cartos-data/field.rs) Built
Web config console (Admin · Field Apps tab, FieldAppsConfig.tsx) Built
Capability integration (field catalog module, cap_guard route gate, migration 0010) Built
Analyst web module (/field, FieldModule.tsx) — captures on the map + photo thumbnails Built
Agent tools (list_field_apps with module arg, summarize_field_captures) Built (cross-module)
Media backend (POST/GET /api/field/media, src/media.rs) Built
Field Apps → Modules (migration 0011, field_app_modules M2M, PUT /api/field/apps/:slug/modules) Built
Portable FieldCapturesOverlay (Smart City, Maritime, Emergency Services) Built
Demo seeds (Asset Inspection → smart-city; Port Inspection → maritime) Built
Flutter mobile app (cartos-field-mobile/) — schema-driven capture, geometry editor, photos + signatures, offline queue + basemap caching, background sync, OIDC/PKCE Built (CI-compiled)
Android CI + Firebase App Distribution (field-mobile-ci.yml) Live — APK published on every merge to main / manual dispatch to field-testers group
iOS CI + TestFlight (field-ios-testflight.yml) — compile check; signed upload gated on Apple secrets Compile CI live, run on demand (workflow_dispatch since 2026-07-29 — a macos-14 runner bills at 10× and the compile is not secret-gated); signed upload TBD (pending Apple Developer enrollment)
IdP federation Deferred — designed in docs/identity-federation-design.md; decoupled; triggers on first sovereign-client onboarding

Remaining 3B items:

What it is: a configurable, offline-first mobile field-capture platform — the sovereign ground-truth edge that feeds the loop and multiplies seats (~10× field vs analyst). Planning doc set: cartos-phase3-mobile/ (MRD/PRD).


15. Reference docs

Doc What
docs/deployment-readiness.md Deployment-readiness hardening ledger — done vs deferred (security, throughput, observability, topology). Read before a production launch.
docs/group-access-design.md Canonical group-access contract + RLS + admin write paths.
docs/identity-federation-design.md Planned model for federating Keycloak to a client's own IdP/AD per sovereign deployment (design, not built).
docs/phase-1-plan.md Broader phase planning.
docs/build-manual-deltas.md Divergences from the original build manual (Smart City first; EarthWatch3D iframe).
docs/demo-script.md Demo walkthrough.
docs/cesium-audit.md Cesium app audit.
cartos-phase2/docs/phase-2/specs/derived-dataset-schema.md Gate-0 contract: derived-dataset table + lineage edge schema. Implemented by migration 0013 + cartos-data::derived.
cartos-phase2/docs/phase-2/specs/session-result-object.md Gate-0 contract: SessionResult schema (schema_version=1). Implemented by cartos-core::SessionResult + migration 0014.
cartos-phase2/docs/phase-2/specs/gpu-job-contract.md Gate-0 contract: sidecar POST /infer request/response + worker lifecycle. Implemented by migration 0015 + jobs.rs + model-sidecar/main.py.
mcp-server/README.md MCP server build + Claude Desktop wiring + token refresh.
model-sidecar/README.gpu.md GPU sidecar deploy (RunPod/Lambda/AWS/Azure/on-prem) + multispectral FM upgrade path.
cartos-pricing/README.md How to regenerate the commercial model + docx/pdf.
website/README.md Site structure + deploy.
cartos-grid-PRD/ Cartos Grid (Electric Utility edition) PRD + Capability Brief — engine+editions model, nine department modules, RAiN Partners licensing, deep-integration roadmap (operational data tables + system integrations).
guardian-PRD/ Cartos Guardian PRD + Capability Brief — the strike-risk model, five modules, deeper-data roadmap. See §8.5.
sentry-PRD/ Cartos Sentry PRD (PRD.md + build_sentry.py) — the reactive crisis-monitoring product case. See §8.6.
EarthWatch3D-PRD/PRD.md §20 Cartos Atlas Government edition framing (non-exclusive; engine+editions model from the edition perspective).
CLAUDE.md Context for Claude Code sessions (layout, conventions, startup).
README.md Public-facing quick start (uses :5432 — adjust to :5433 here).


Questions or something out of date? Update this guide alongside the change.

Live site: https://cartosatlas.com