Koder Karavan — `products/vertical/karavan`
- Kind: Vertical product — fleetasset tracking (live positions, trips, geofences, alerts, reports) for any asset where positionmovement applies: vehicles, drones, robots, aircraft, vessels, equipment, livestock
- Status: S1 walking skeleton LIVE (KARAVAN-001 done 2026-06-11): GT06 → canonical position → shadow publisher + live map; backend Go no LXC
karavan(10.0.1.196, systemdkaravand.service), vhostkaravan.koder.devcom/api/+/app/atrás do auth-gate dev. 002a multi-tenant + 003 adapters + 022 histórico (history.Store JSONL; kdb-ts pós-campanha) + 004 tripsplayback + 005 geofences + 007 relatórios + 011 obs SDK + 020 integração + 019 comandos downstream (0x800x15, audit durável, confirm 2-passos, provisioning) DONE; geo embarcado (#017): tiles 2.4G via /geo + OSRM LIVE (unit mld + proxy apiv1route) + geocoder Photon LIVE (017b: komootphoton importado do Nominatim do maps, escopo Botucatu,photon.service:2322 no ct karavan; karavand traduz Photon GeoJSON→Nominatim jsonv2 e serve/api/v1/reverse+/api/v1/searchvia Photon quandoKARAVAN_GEOCODER_URLsetado — demo sem dependência do Koder Atlas; restante: refresh-pipeline + CNEFE + escopo BR) + refresh semanal agendado R1R3 (timer karavan-geo-refresh, download condicional Geofabrik, gauge karavangeodataageseconds + SLA warn 30d — self-hosted-data-refresh.kmd) + app MapLibretemas (#013 núcleo + vista 3D fill-extrusion opcional c/ toggle de perspectiva, 2026-06-12) + rotas (#014a) + despacho (#015) + simulação de frota (#016: zonas disjuntas, OSRM local, integra despacho) + painel ops (014b): pipelineingestcanônico, adapters GT06 (público :5024) e OsmAnd phone-as-tracker (público :5055); cutover da PoC gated no Marco 0; + #024 liveness onlineoffline + #021a share link público + 021b Web Push opt-in (triggers approachingarrivalperiodic 1min via koder-notify, chave sintética karavan:share:token) + #023a admin registry DONE; tenancy Koder ID = #012: 012a rebind auditado + 012b login OAuth (PKCE, badge) + 012c-1 rolesenforcement (members seed em tenants.json, viewer<member<admin<owner, gate table-driven fail-closed, T-suite oauth-flow, multi-host OAuth) DONE — 012c-2 substrato kdb: 2a schema+RLS migration (store/migrations/0001_tenancy.sql, boundary orgid, FORCE RLS em `app.currentorgid, T7) + 2b store pgwire DONE (store/kdb.goPGStore= híbrido write-through atrás deKARAVANTENANCYBACKEND=kdb, defaultjson; reusaidenginepkgkdb.WithOrgTx; validado em PG real no dev-linux-fleet — paridade comportamental + T7 SQL-layer + durabilidade; **prod intacto, default json**), 2c id-membership: 2c-i endpoint interno no Koder ID (GET /internalv1orgs{id}membership, X-Internal-Token) + 2c-ii consumidor karavan (internalidrolecliente+cache 60s+fallback seed, atrás deKARAVANROLESOURCE=id, defaultseed`) DONE flag-off (validado em PG real/suíte; cutover ao vivo + deploy do endpoint id pendentes), 2d orgdomains RFC-019 + 2e T7-T9staging cutover pendentes; mobile (#008): 008a walking skeleton DONE — app Flutter `appmobile` (koder_kit: KoderApp+AuthGate, tabs MapAssetsMore, MapLibre, i18n en+pt) + fundação nativa no backend (Bearer auth Koder ID via JWKS; leitor PMTiles v3 próprio em internalgeo servindo/geo/tiles/{z}/{x}/{y}+/geo/styles/{name}.jsonreescrito) ; 008b DONE (2026-06-12) — modo tracker no app (provision autenticadoPOST /api/v1/track/provisionmember-gated, device id 128-bit como credencial, assetphn-*sticky no tenant; geolocator stream + heartbeat 60s + builder OsmAnd puro) + mount/osmandstrict na edge (só provisionados, rate-limit por device, :5055 segue auto-provision LAN; rota nova no vhost Jet) + WS/api/v1/streamno lugar do polling (Bearer, reconnect backoff, metadata 30s degradado) + 4ª tab Alerts (geofence+liveness, poll 30s) ; 008c-1 DONE — platform scaffold Android (applicationId dev.koder.karavan, permissões de localização, ícones kicon 18 arquivos, maplibre_gl 0.26, CI karavan-mobile-ci.yml, gate APK debug no ci-runner-android) — 008c-2 release assinado/Hub e 008c-3 push nativo pendentes - Domain:
karavan.koder.dev(landing). Web app perlanding-pages/products.kmd §Padrão de URL - Backlog prefix:
KARAVAN
Architecture position
Karavan is the second product on the Koder Ion engine (after Koder Home — "Ion is the engine, Home is the product"). It does not reimplement IoT plumbing; it adds the fleet domain:
- Ion (
products/vertical/ion/engine) — device registry, ingestion (MQTTHTTPWS), time-series storage, device shadow (= asset last-known state), rules + alerts, OTA, tenant white-label. Consumed via the engine's public APIs only, never its schema. - Koder Atlas (
services/foundation/atlas) — tiles, OSRM routing, Nominatim geocoding. Karavan is the production consumer that motivates Atlas' deployment (atlas#001). - Koder ID — multi-tenancy contract (koderuserid + workspaceid, T1–T9) and org custom domains (RFC-019 `orgdomains`) for white-label tenants.
The product's own layer: Asset model (assetclass drives 2D3D movement semantics), protocol adapters over a registry framework (#033, k-evolve #009: `internalprotocol — StreamServer runner + Registry porta→decoder, so adding a protocol is additive: write a StreamDecoder, register it, bind a port; GPS-LBS fallback + downlink routing shared) — GT06 + **Teltonika** (Codec8/8E — #034 slice 1) + OsmAnd/HTTP live; Suntech/Concox/Meiligao (#034) + MQTT-via-Ion, LoRaWAN and ADS-B later, an **ingest sanitization filter** (internalingest` Filter — drops duplicateteleportabsurd-speedfuture-clock fixes before the store so junk never corrupts accumulators or fires spurious events; #037 slice 1), tripsplayback, geofences (2D3D), a unified event engine (internal/event — ignitionmotion detected on ingest + bridged geofence & liveness transitions, served at `apiv1events; #036 slice 1, the layer reports & notifications consume; dispatch incidents/missions are bridged in via internaldispatchevents so SAMU activity is a first-class event too — k-evolve #043; dispatch load is also metered via internaldispatchmetrics — karavandispatch{incidents,missions,opstate}total counters — k-evolve #044), a **notification fan-out** (internalnotify` Dispatcher — an event.Sink that filters the stream to alert-grade types and forwards them to channels, webhook channel live, pushemailSMS slice 2; #040 slice 1), reports (odometerengine-hoursmaintenance per-asset + fleet CSV) with *PXKML route export (internal/geoexport, /api/v1/assets/{id}/route.{gpx,kml} for GIS import; #039 slice 1) + a Stops report (trips.Stops / GET /api/v1/assets/{id}/stops — stationary spells ≥ a dwell threshold, the complement of trips over the same position history; #039 slice 2) + a geofence dwell report (geofence.Report / GET /api/v1/assets/{id}/geofence-report — entries + time-inside per fence over a window, reusing Fence.Contains; #039 slice 2c) + a consolidated fleet report (GET /api/v1/reports/fleet — the per-asset report across the whole tenant fleet in JSON, the fleet CSV's richer sibling, reusing buildAssetReport; #045), and driver accounting (internal/driver — per-tenant driver registry + manual asset↔driver assignment emitting driver_changed; RFID/iButton auto-id slice 2; #038 slice 1), and a scheduling engine (internal/schedule — per-tenant recurring reportcommand tasks on an interval, always-on Worker with a persisted NextRun checkpoint + pluggable Executor; cron + real reportcommand executors slice 2; #041 slice 1), and a SAMU dispatch-console foundation (internal/triage — admin-configurable dynamic triage questionnaires per incident type, typed/combo-box questions; the form-builder for the radio-operator console; karavan#033-samu slice 1, owner directive) + a CPF-keyed citizen DB (internal/citizen — caller records, CPF-validated upsert so a repeat caller resolves to one record; #033-samu slice 2), and the SAMU dispatch flow (POST /api/v1/triage/calls — validates the questionnaire answers, registers the caller, and creates a dispatch.Incident reusing the existing dispatch engine, with a per-tenant audit call-log; #033-samu slice 3), and the dispatch board (GET /api/v1/dispatch/board — open incidents + live fleet positions as two standard GeoJSON FeatureCollections, RFC 7946, that the operator console plots as toggleable map layers over the services/foundation/maps#010 basemap; built on a typed internal/geojson substrate; #033-samu slice 4), made live over WebSocket (GET /api/v1/dispatch/board/stream — snapshot frame then per-fix asset deltas and per-event incidentmission deltas, so the console patches its layers without polling; backed by a per-tenant dispatch.Broadcaster pubsub composed alongside the webhook sink via dispatch.MultiSink, which also folds dispatch into the event bus it previously bypassed; #033-samu slice 5), and the operator intake UI (the embedded MapLibre console's "New call" modal — web/static/newcall.js — runs the dynamic questionnaire by incident type, looks the caller up by CPF, and POSTs /api/v1/triage/calls to dispatch, the radio-operator's keystone workflow; #033-samu slice 6; map-click location + board-stream wiring as later sub-slices), and EMS care levels (model.CareLevel BLSALS — international nomenclature: whiteUSB ambulance = BLS, redmobile-ICUUSA = ALS — as an asset attribute + an incident RequiredLevel; the dispatch engine enforces eligibility as an invariant so an ALS call never goes to a BLS-only unit, ALS downgrades to BLS calls; the whole dispatch central is generic with SAMU as one profile, owner-approved 2026-06-27; #033-samu slice 7), surfaced in the console (the admin asset editor sets each ambulance's care_level BLS/ALS; the "New call" modal carries a per-call required_level override of the protocol default; #033-samu slice 8). The intake takes its location by map click too (a "Pick on map" button bridges the modal to the console map via a newcall:pickloc/newcall:loc CustomEvent pair, reusing the existing reverse-geocoder — the modal stays decoupled from the map instance; #033-samu slice 9). The console UI is smoke-tested in a real browser (tests/browser/console-smoke.sh drives webtest/koder_web_test under Xvfb, asserting the intake + EMS selectors render with zero JS errors — catching real-browser breakage jsdom can't; #033-samu slice 10) and a full intake→dispatch E2E (tests/browser/intake-e2e.sh drives the real "New call" modal against a live karavand and asserts the dispatch engine created the incident with the form's ALS care level + the CPF caller as requester — the real-DOM × real-JS × real-API intersection; #033-samu slice 11, the SAMU console is functionally complete*. Durable persistence of the SAMU registries (CPF PII → RLS + retention) is folded into the platform kdb cutover scope (#012c-2e), not piecemealed. That cutover's evidence is being driven (Rule 15-B): the store suite (contract + T7 RLS + durability) and a daemon-level harness (tests/integration/kdb-daemon-e2e.sh — karavand on the kdb backend auto-applies the schema, ingests, and reloads durably from Postgres; FORCE RLS confirmed blocking even the table owner) both run green against a real Postgres 17, plus a cross-tenant isolation harness (tests/integration/kdb-tenant-isolation-e2e.sh — two tenants ingest into the same PG, and after a restart with a wiped mirror each reads back only its own asset via the Host-resolved API, no leak), plus the state.json→PG migration seed (store.MigrateJSONToKDB + PGStore.ImportAssetState + the karavan-migrate cmd — a faithful per-org upsert preserving accumulatorsmetahealth, TestMigrateJSONToKDB green). With store + daemon + isolation + migration all green on real PG, the only thing left before the cutover is the gated flip in isolated staging (ops). The board-stream→console merge was dropped as lossy (the board-stream serves external/future consumers, the console keeps its richer poll+WS path).
Primary couplings
products/vertical/ion/engine (engine) · services/foundation/atlas (geo) · services/foundation/id (authtenancy) · `infranetjet (vhosts) — and infranet/jet#219 (the Traccar PoC at rastreador.crescer.net`, which becomes Karavan's first white-label tenant, Crescer org).
Self-hosted-first pairing
External incumbent: Traccar (current PoC). Flip gate: Karavan's GT06 decoder reaches field parity with Traccar's (Marco 0 Botucatu test), then tenant migration retires the ad-hoc incus container.