diff --git a/README.md b/README.md index 11945e7..59c5bc5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A fleet of drones flies fully autonomously: no internet uplink, no external acce This repository describes how to build, deliver, test, and operate that platform: the storage layout, the sync strategy, the network trust model, the CI/CD chain, and the simulation environment that makes it all testable without touching real hardware. +**New here?** Start with the one-pager: [00 — Executive overview](docs/00-executive.md) (includes how **MinIO** fits). Then the [design journey](docs/12-design-journey.md) if you want the narrative. + ## Design principles 1. **Every drone is autonomous.** No cluster orchestrator spans the swarm; intermittent mesh connectivity makes that an anti-pattern. Coordination happens through data exchange, not through a control plane. @@ -30,7 +32,7 @@ Honest map so a reader knows what runs today versus what is design intent. | Peer query | Read-only SQL **gate** over HTTP explorer (keyword allow-list) | Same gate idea via **SSH forced command** + OS/engine hardening | | Bulk sync | Visualised opportunistic transfer volume | rsync/rclone over persistent SSH between peers | | Mesh trust | Ansible templates for WireGuard + ed25519 forced commands | Provisioned per-device keys; nothing joins at runtime | -| Ground segment | k3d/Terraform sim: lake, Grafana, offload CronJob, optional MinIO | k3s warehouse, GitOps overlays, post-flight mirror | +| Ground segment | k3d/Terraform sim: lake, Grafana, offload CronJob, **MinIO as T3** | k3s warehouse with MinIO, GitOps overlays, post-flight mirror | | CI / delivery | GitHub Actions: pytest, smoke flight, Trivy, semver release + fleet manifest artifact | Self-hosted GitLab + registry inside the air gap (same stages) | | Docs | Problem, architecture, ADRs, design journey, open questions | Living ADRs owned by the team | @@ -40,6 +42,7 @@ Start from the [design journey](docs/12-design-journey.md) for the story; use th | Document | Contents | | --- | --- | +| [00 — Executive](docs/00-executive.md) | **Start here** — one-page goal, sync rule, MinIO stance | | [00 — Glossary](docs/00-glossary.md) | Terms and abbreviations used throughout | | [01 — Problem statement](docs/01-problem-statement.md) | Goal, constraints, knowns vs assumptions | | [02 — Architecture](docs/02-architecture.md) | On-board layers, service composition, communication planes | @@ -49,13 +52,13 @@ Start from the [design journey](docs/12-design-journey.md) for the story; use th | [06 — Environments](docs/06-environments.md) | Drone / ground warehouse / dev-simulation infrastructures | | [07 — Observability](docs/07-observability.md) | Logs, service metrics, hardware telemetry | | [08 — Roadmap](docs/08-roadmap.md) | Dependency graph, simple to complex | -| [09 — Open questions](docs/09-open-questions.md) | Known unknowns and proposed answers | +| [09 — Open questions](docs/09-open-questions.md) | Known unknowns and proposed answers (incl. MinIO placement) | | [10 — Domain context](docs/10-domain-context.md) | Swarm autonomy principles this design builds on | | [11 — CI/CD & delivery](docs/11-cicd-delivery.md) | Pipelines, registry, dev containers, fleet releases | -| [12 — Design journey](docs/12-design-journey.md) | **Start here** — a narrative walk-through of how the design came together, linking into the code | +| [12 — Design journey](docs/12-design-journey.md) | Narrative walk-through linking into the code | | [ADRs](docs/adr/) | Architecture Decision Records — the decisions behind the above, in the order they were made | -New here? Read the [design journey](docs/12-design-journey.md) first: it tells the story chronologically and links straight into the code and decisions. The design principles below are the *what*; the [ADRs](docs/adr/) are the *why and when* — each principle traces to a dated, immutable decision record. +New here? Read the [executive overview](docs/00-executive.md) first, then the [design journey](docs/12-design-journey.md) if you want the story. The design principles above are the *what*; the [ADRs](docs/adr/) are the *why and when*. ## Runnable parts diff --git a/docs/00-executive.md b/docs/00-executive.md new file mode 100644 index 0000000..98092cd --- /dev/null +++ b/docs/00-executive.md @@ -0,0 +1,61 @@ +# 00 — Executive overview + +A one-page map of the proposal. Details and trade-offs live in the linked docs. +Nothing here is a mandate: it is a from-scratch platform sketch the team can +reshape once real constraints are on the table. + +## Goal + +Build an **on-prem, air-gapped data platform** for an autonomous drone swarm: +accumulate sensor and detection data on each unit, share only what peers need in +flight, and offload complete flights to a ground warehouse for replay and +training. + +## Constraints (given) + +- No internet / no external access in flight; data stays inside the system. +- Each drone is autonomous; mesh links are intermittent — **no swarm-wide orchestrator**. +- On-board today: **Docker Compose**, **DuckDB**, YOLO-like video analytics; **Parquet** under evaluation. +- **MinIO is already in the stack** — exact placement not fully known yet (see below). +- Ground side can run lightweight Kubernetes; this proposal keeps k3s **on the ground only**. + +## Data contract (proposed) + +| What | Where | Crosses the air? | +| --- | --- | --- | +| Raw telemetry | Local NVMe (Parquet / Hive layout) | **No** — offloads after landing | +| Pose / detections (derived) | Local store + peer pub/sub | **Yes** — small, budgeted | +| Full flight archive | Ground warehouse (Parquet + object store) | After landing only | + +One layout on every floor so offload is a **mirror**, not a migration +([03 — Data platform](03-data-platform.md)). + +## Sync rule + +1. **Fast path:** compact pose frames (~45 B @ 5 Hz) + event-shaped detections. +2. **Bulk path (peers):** sealed derived partitions when the link allows + (rsync/SSH proposed; alternatives open). +3. **Never** push raw high-rate telemetry peer-to-peer in flight. + +## MinIO (first-class, placement TBD) + +MinIO is treated as **already chosen infrastructure**, not something to rip out. + +| Role | Stance in this sketch | +| --- | --- | +| **Ground warehouse (T3)** | **Primary home** — S3 API under the Parquet lake / offload target | +| **On-board (drone)** | **Default: no** — competes with flight-critical CPU/RAM; local Parquet + DuckDB is enough for the hot path | +| **On-board exception** | Not excluded if a team already relies on an S3 API in Compose; then derived-only, never the 5 Hz pose path | +| **5 Hz pose / collision state** | **Not MinIO** — pub/sub (UDP in the PoC; Zenoh proposed) | +| **Inter-drone history catch-up** | Prefer pull of sealed partitions (SSH/rsync); MinIO replication only if ops already standardised on it | + +Open item for the team: document how MinIO is used today, then lock T2/T3 roles +([09 — Open questions](09-open-questions.md)). + +## What to read next + +1. [12 — Design journey](12-design-journey.md) — narrative + deep links into code +2. [Implemented vs proposed](../README.md#implemented-now-vs-proposed-next) — PoC vs production intent +3. [09 — Open questions](09-open-questions.md) — including MinIO placement + +Live visual PoC: [swarm.produktor.io](https://swarm.produktor.io/) (same access as this repository). diff --git a/docs/00-glossary.md b/docs/00-glossary.md index e7fc14b..9a3248e 100644 --- a/docs/00-glossary.md +++ b/docs/00-glossary.md @@ -55,7 +55,7 @@ Terms and abbreviations used throughout this proposal. | **Docker Compose** | Declarative multi-container runtime; the only orchestrator on board a drone | | **k3s** | Lightweight Kubernetes distribution; used on the ground only | | **Dev Container** | Reproducible containerized development environment definition | -| **MinIO** | S3-compatible object store; runs on-board for derived data and on the ground as the warehouse backend | +| **MinIO** | S3-compatible object store **already in the stack**; default home is the **ground warehouse (T3)**. On-board use is an exception, not the baseline | | **Registry mirror** | Local copy of a container registry inside the air gap; drones pull images from it | | **OTA** | Over-the-air update — delivered before a mission while docked, never mid-flight | | **Fleet release manifest** | A versioned lockfile pinning every artifact (image digests, model weights, schemas, configs) that defines one fleet version | diff --git a/docs/01-problem-statement.md b/docs/01-problem-statement.md index 4f3e809..27c18a1 100644 --- a/docs/01-problem-statement.md +++ b/docs/01-problem-statement.md @@ -28,7 +28,7 @@ Plus everything a platform needs around that: reproducible builds and deployment - The current on-board software is **Docker Compose** with two services: **sensor ingestion** and **video-stream object detection** (a YOLO-like model; weights and approaches vary). - **DuckDB** is already in use for local data handling. - **Parquet** adoption is at the evaluation stage — the storage layout in this proposal is the core of what is being asked. -- **MinIO** is available and considered as the inter-drone sync mechanism. +- **MinIO** is already in the stack. Exact placement (ground vs on-board vs sync path) was not fully specified in the brief; this proposal treats MinIO as **first-class ground-warehouse infrastructure** by default and keeps on-board MinIO as an open option — see [00 — Executive](00-executive.md) and [09 — Open questions](09-open-questions.md). - A **lightweight Kubernetes** exists in the ecosystem; this proposal scopes it to ground infrastructure only (see [02 — Architecture](02-architecture.md)). - After landing, each drone's data is **offloaded to an on-prem warehouse** for replay and iterative model training. - Mission intent (declarative goals) reaches the swarm over a narrow **C2 channel**; there is no continuous ground link in flight. @@ -43,6 +43,7 @@ Plus everything a platform needs around that: reproducible builds and deployment | A4 | Missions are bounded (battery), so a "flight" is the natural unit of data lifecycle | Return-to-base on low energy implies discrete flight sessions | | A5 | Mission logic consumes the data platform as a service and is out of scope here | Flight control, planning, and model training are separate concerns | | A6 | A development-only telemetry channel exists on the bench and is absent from production builds | Standard practice; production radio profile carries C2 + swarm data plane only | +| A7 | **MinIO's primary home is the ground warehouse (T3), not the drone** | On-board object store competes with flight-critical CPU/RAM; local Parquet + DuckDB already cover the hot path. On-board MinIO is not excluded if ops already depend on an S3 API in Compose | ## Out of scope diff --git a/docs/02-architecture.md b/docs/02-architecture.md index 29b5a2b..0d76538 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -21,11 +21,12 @@ graph TB HOOK["event hook
fires on new derived data"] PUB["state publisher
pub/sub broadcast"] BULK["bulk sync
rsync over SSH"] - MINIO["MinIO
optional derived bucket"] QAPI["query API
SQL-over-SSH"] end end + MINIO[("MinIO — ground warehouse T3
already in the stack")] + SENSORS --> WRITER VIDEO -->|detections| WRITER WRITER --> NVME @@ -33,15 +34,16 @@ graph TB DUCK --> NVME WRITER -->|derived rows| HOOK HOOK --> PUB - HOOK --> MINIO HOOK --> BULK QAPI --> DUCK PUB -.->|mesh pose| PEERS["peer drones"] BULK -.->|sealed partitions| PEERS QAPI -.->|on demand| PEERS + NVME -->|post-flight offload| MINIO ``` -MinIO stays available where an S3 API helps (on-board derived datasets, ground warehouse). **In-flight peer bulk sync is SSH/rsync**, not object-store replication — see [04 — Swarm sync](04-swarm-sync.md). +**MinIO** is already in the stack and is the **default ground warehouse (T3)** behind offload — keep it, do not rip it out. Default sketch: **not on the drone** (CPU/RAM budget); on-board MinIO remains an allowed exception if Compose already depends on an S3 API. **In-flight peer bulk sync is SSH/rsync** (or team-standard MinIO replication if that is already how ops works) — never the 5 Hz pose path. See [00 — Executive](00-executive.md) and [04 — Swarm sync](04-swarm-sync.md). + ### Layer 1 — Ingestion - `sensor-ingest` subscribes to sensor sources (ROS 2 topics where available, raw drivers otherwise) and normalizes them into typed streams: IMU, barometer, temperature, LiDAR, RSSI, power, and so on. @@ -56,10 +58,11 @@ MinIO stays available where an S3 API helps (on-board derived datasets, ground w ### Layer 3 — Serving and sync -- The **event hook** is the on-board "lambda": when the writer lands new *derived* rows (state, detections), it triggers registered actions — broadcast, optional MinIO put, bulk-sync hint, or a local mission-logic callback. Nothing polls. +- The **event hook** is the on-board "lambda": when the writer lands new *derived* rows (state, detections), it triggers registered actions — broadcast, bulk-sync hint, or a local mission-logic callback. Nothing polls. - The **state publisher** broadcasts compact position/attitude/detection payloads over the mesh pub/sub (UDP in the PoC; Zenoh proposed — [04](04-swarm-sync.md)). -- **Bulk sync** pulls sealed derived partitions from peers over persistent SSH (rsync delta transfer). MinIO is optional where an S3 API is wanted; it is **not** the in-flight peer replication path. +- **Bulk sync** pulls sealed derived partitions from peers over persistent SSH (rsync delta transfer). If the team already operates MinIO replication for that job, reuse it — do not invent a second path. Pose traffic never goes through MinIO. - **Peer queries** are read-only DuckDB SQL — HTTP explorer gate in the PoC; **SSH forced commands** proposed for flight (SELECT-only gate, read-only OS user, columnar responses — [04](04-swarm-sync.md)). +- **MinIO** sits on the **ground** as T3 by default ([00 — Executive](00-executive.md)); on-board MinIO is an explicit exception, not the baseline. ## Communication planes diff --git a/docs/03-data-platform.md b/docs/03-data-platform.md index 1002bab..b28714a 100644 --- a/docs/03-data-platform.md +++ b/docs/03-data-platform.md @@ -68,11 +68,13 @@ Same format on every floor; only volume, retention, and location change: | Floor | Where | Contents | Retention | | --- | --- | --- | --- | | **T0 — hot** | RAM / DuckDB in-process | Sliding window of the last minutes; what mission logic queries in flight | Minutes | -| **T1 — warm** | Drone NVMe | Full raw `telemetry` + `detections` + `state` of the current flight; never leaves the drone in flight | Current flight (+ quota-based headroom) | -| **T2 — shared** | On-board MinIO bucket | Derived data only (`detections`, `state`), replicated opportunistically across the swarm | Current mission | -| **T3 — warehouse** | Ground on-prem object store + Parquet lakehouse | Every flight of every drone, forever; replay, analytics, model training | Years | +| **T1 — warm** | Drone NVMe (Parquet) | Full raw `telemetry` + `detections` + `state` of the current flight; never leaves the drone in flight | Current flight (+ quota-based headroom) | +| **T2 — shared** | Peer-derived cache on NVMe (Parquet) | Derived data only (`detections`, `state`) pulled from peers when the link allows | Current mission | +| **T3 — warehouse** | Ground **MinIO** (or equivalent) + Parquet lakehouse | Every flight of every drone; replay, analytics, model training — **primary MinIO home** | Years | | **T4 — dev** | Engineer laptop / sim farm | Slices pulled from T3, or synthetic data from the simulator | Ephemeral | +**MinIO note:** default sketch keeps MinIO on the **ground (T3)**. An on-board MinIO sidecar is a possible exception if the team already standardises on an S3 API in Compose — derived data only, never the 5 Hz pose path. Placement is an open question ([09](09-open-questions.md), [00 — Executive](00-executive.md)). + ## Flight offload: a mirror, not a migration Because T1 and T3 share the identical layout, offload after landing is: diff --git a/docs/04-swarm-sync.md b/docs/04-swarm-sync.md index b8a0730..0efb2a5 100644 --- a/docs/04-swarm-sync.md +++ b/docs/04-swarm-sync.md @@ -74,7 +74,7 @@ graph LR | **DDS multicast** (ROS 2 default) | Works, battle-tested; but discovery storms and tuning pain on lossy wireless meshes are well documented. Keep as fallback since ROS 2 speaks it natively | | **MQTT** | Needs a broker — a per-drone broker bridge is possible but adds moving parts for no gain over Zenoh | | **Raw UDP multicast** | **Implemented in the PoC** for pose broadcast (fixed frame, no discovery); no query layer, no reliability — also the documented degraded mode | -| **MinIO bucket replication** | Wrong tool for the 5 Hz pose path; optional on board for derived datasets and primary on the ground warehouse — not the in-flight bulk path | +| **MinIO bucket replication** | Keep as a **team option** for bulk derived catch-up if ops already standardise on it; wrong tool for the 5 Hz pose path. Default sketch: MinIO primary on the **ground warehouse**, not on the drone | ## Two sync mechanisms, deliberately separate @@ -86,7 +86,7 @@ Why SSH-based bulk sync over object-store replication: - **Identity is already there.** Every drone holds pre-provisioned ed25519 keys and a fixed `known_hosts`/`authorized_keys` set from ground provisioning ([05](05-network-security.md)) — the trust model needs no new machinery. - **One persistent multiplexed session** (`ControlMaster`) per peer costs almost nothing at idle and survives as a single TCP stream; every transfer rides it without new handshakes. - **rsync delta transfer is resumable** across link drops — exactly the failure mode of an ad-hoc mesh — and the shared partition layout makes it trivially incremental: same paths, same files, pull only what is missing. -- **Zero extra services** on the flight-critical node. MinIO remains available where an S3 API is genuinely wanted (ground warehouse, and optionally on board), but the in-flight bulk path does not depend on it. +- **Zero extra services** on the flight-critical node by default. **MinIO stays first-class on the ground (T3).** On-board MinIO is an exception only if Compose already depends on an S3 API. In-flight bulk path defaults to SSH/rsync; switch to MinIO replication if that is already how the team moves objects. Partition healing is automatic: replication is pull-based, addressed by partition path, and idempotent (each drone only ever writes its own `drone=` subtree — **no write conflicts by construction**). diff --git a/docs/06-environments.md b/docs/06-environments.md index 2d6831f..83ad06c 100644 --- a/docs/06-environments.md +++ b/docs/06-environments.md @@ -12,12 +12,12 @@ graph LR SLICE["T4: data slices"] end subgraph fleet [1 — Fleet, in flight] - D1["drone: Compose data plane
T0 hot + T1 NVMe + T2 MinIO"] + D1["drone: Compose data plane
T0 hot + T1 NVMe + T2 peer cache"] D2["drone …"] end subgraph ground [2 — Ground, on-prem] DOCK["base station docks"] - DWH["T3 warehouse
object store + Parquet/DuckDB"] + DWH["T3 warehouse
MinIO + Parquet/DuckDB"] K3S["k3s: CI runners, registry mirror,
sim farm, dashboards"] TRAIN["model training (out of scope)
reads T3, ships weights"] end @@ -43,7 +43,7 @@ The permanent installation. This **is** allowed to be a cluster — links are wi | Component | Runs on | Role | | --- | --- | --- | -| **Warehouse (T3)** | Object store (MinIO or equivalent) + Parquet | Every flight of every drone; the system's long-term memory | +| **Warehouse (T3)** | **MinIO** (already in the stack) + Parquet | Every flight of every drone; the system's long-term memory | | **Base station docks** | Bare metal | Wired offload + integrity audit + drone provisioning ([03](03-data-platform.md), [05](05-network-security.md)) | | **Registry mirror** | k3s | In-air-gap container registry + artifact storage; the only software source drones ever see | | **CI runners** | k3s | Multi-arch builds, tests, scans ([11](11-cicd-delivery.md)) | diff --git a/docs/08-roadmap.md b/docs/08-roadmap.md index 0a8420b..37125bc 100644 --- a/docs/08-roadmap.md +++ b/docs/08-roadmap.md @@ -9,11 +9,11 @@ graph TD S1["Stage 1
Dev environment + CI skeleton
Dev Containers, GitLab templates, multi-arch builds"] S2["Stage 2
Registry and artifacts
images, model weights, schemas, cleanup policies"] S3["Stage 3
Single-drone data pipeline
ingest → current/ → sealed Parquet → DuckDB"] - S4["Stage 4
Serving layer
event hook, query API, local MinIO"] + S4["Stage 4
Serving layer
event hook, query API"] S5["Stage 5
Virtual swarm simulation
N drones in Compose, seeded scenarios, CI regression"] S6["Stage 6
Mesh network and security
WireGuard overlay, PKI provisioning, mTLS"] - S7["Stage 7
Swarm sync
pose broadcast, detections pub/sub, MinIO replication"] - S8["Stage 8
Ground warehouse
offload + audit, T3 store, replay tooling"] + S7["Stage 7
Swarm sync
pose broadcast, detections pub/sub, bulk pull"] + S8["Stage 8
Ground warehouse
offload + audit, MinIO T3, replay tooling"] S9["Stage 9
Observability
platform metrics as sensor stream, ground dashboards"] S10["Stage 10
Fleet releases
manifest, mirror, dock delivery, atomic rollback"] S11["Stage 11
Query standard
GraphQL contract, schema registry, multi-team surface"] @@ -41,11 +41,11 @@ graph TD | **1 — Dev environment + CI skeleton** | Dev Containers, GitLab shared templates, multi-arch runner fleet | An engineer builds and tests on day one; ARM64/AMD64 both green | — | | **2 — Registry & artifacts** | GitLab registry live, model weights + schemas as versioned packages | One artifact store, scanning wired in (Trivy, SonarQube) | 1 | | **3 — Single-drone data pipeline** | `sensor-ingest` → writer → `current/` → sealed → DuckDB queries on one node | The storage core: rates sustained, compression measured, quotas enforced | 1 | -| **4 — Serving layer** | Event hook + query API + on-board MinIO | Event-driven consumption; nothing polls | 3 | +| **4 — Serving layer** | Event hook + query API (SQL gate) | Event-driven consumption; nothing polls | 3 | | **5 — Virtual swarm** | Simulator, N drones via Compose, seeded scenarios, CI regression gate | Everything after this ships with a test bench | 2, 4 | | **6 — Mesh & security** | WireGuard overlay, offline CA, provisioning flow, mTLS | Zero-trust fabric exists before any real sync traffic | 1 | -| **7 — Swarm sync** | Pose broadcast + detections pub/sub + bulk replication over the mesh | Staleness budgets met under simulated loss/partitions | 5, 6 | -| **8 — Ground warehouse** | Dock offload (mirror + audit), T3 store, replay queries | A full flight round-trips: fly (simulated) → offload → replay | 3 | +| **7 — Swarm sync** | Pose broadcast + detections pub/sub + bulk partition pull | Staleness budgets met under simulated loss/partitions | 5, 6 | +| **8 — Ground warehouse** | Dock offload (mirror + audit), **MinIO as T3**, replay queries | A full flight round-trips: fly (simulated) → offload → replay | 3 | | **9 — Observability** | Platform metrics as sensor stream; ground Prometheus/Grafana/Loki; post-mortem dashboards | Every health question from [07](07-observability.md) answerable | 7, 8 | | **10 — Fleet releases** | Release manifest, registry mirror, dock delivery, atomic rollback drill | The whole fleet moves as one version, rollback rehearsed | 2, 5 | | **11 — Query standard** | GraphQL contract over versioned schemas; documentation for other teams | The multi-team integration surface | 7, 8 | diff --git a/docs/09-open-questions.md b/docs/09-open-questions.md index 3066c66..2807673 100644 --- a/docs/09-open-questions.md +++ b/docs/09-open-questions.md @@ -67,3 +67,16 @@ Sub-250 g class units cannot run the full stack (no GPU, minimal CPU/storage). The observability path now scans only the most recent flights ([ADR-0009](adr/ADR-0009-bounded-lake-scans.md)), but old `flight=` partitions still accumulate on T1 disk after offload to T3. *Proposal:* a scheduled prune of T1 partitions whose flights are confirmed present in the T3 warehouse (offload as the retention gate), configurable by age and free-space watermark. On the drone the same policy is bounded by the NVMe quota ladder ([03](03-data-platform.md)); in the sim environment it is a ground CronJob alongside the offload job. + +## 12 — MinIO placement (already in the stack) + +Exact how MinIO is used today was not fully specified. It should not be ripped out. + +*Proposal (default):* + +- **Ground warehouse (T3):** primary MinIO home — offload target and long-term object API. +- **On the drone:** default **off** — local Parquet + DuckDB cover the hot path; an object store competes with flight-critical CPU/RAM. +- **Exception:** on-board MinIO allowed if Compose already depends on an S3 API — derived datasets only, never 5 Hz pose. +- **Peer bulk catch-up:** prefer sealed-partition pull (SSH/rsync); use MinIO replication only if that is already the ops standard. + +*Settle with the team:* one short diagram of today's MinIO topology (buckets, clients, what syncs). Until then, treat A7 in [01](01-problem-statement.md) as the working assumption. Summary table: [00 — Executive](00-executive.md). diff --git a/docs/12-design-journey.md b/docs/12-design-journey.md index 2ce6da4..650ce9d 100644 --- a/docs/12-design-journey.md +++ b/docs/12-design-journey.md @@ -144,10 +144,13 @@ gives redundancy and a way to cross-check later. On the ground, when the fleet returns, the same path unifies every unit's data into a [local warehouse](06-environments.md). The warehouse is read-mostly for analytics, so transaction contention is not a concern, which opens a -DuckDB / DuckLake approach with MinIO underneath, the +DuckDB / DuckLake approach with **MinIO underneath on the ground (T3)** — MinIO +is already in the stack, so the warehouse leans on it rather than inventing a +second object store — the [same storage model on both ends](adr/ADR-0002-one-storage-format.md). One uniform structure is what lets an analyst trust a single picture even when a -unit's data has a gap. +unit's data has a gap. On-board MinIO stays an open exception, not the default +([00 — Executive](00-executive.md)). > **Read the code** > - [`infra/terraform/ground/main.tf`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/infra/terraform/ground/main.tf#L124-L160) — the post-flight T1 to T3 offload CronJob