Overview
Most operational dashboards are built for one audience and bolted onto a second. The result is a business tab that's clean but lacks engineering context, and an engineering tab that's accurate but unreadable by anyone outside the data team.
Sentinel Fleet Operations was designed the other way around: define four distinct operational questions first, then build a data model that answers all four from the same fact layer. The domain simulates an Anduril Sentry surveillance fleet deployed across 10 North American sites in coastal, desert, tundra, plains, and forest environments. The pipeline generates 9 parquet sources (83K rows, deterministic seed) and transforms them through a dbt star schema on DuckDB — 9 staging models, 8 mart models, 66 tests — served through a four-tab Streamlit interface: Operations, Detection Analytics, Reliability, and Pipeline Health.
System Architecture
The build and serve phases are fully decoupled. dbt runs offline; the compiled DuckDB file and dbt artifacts ship with the repository. At runtime, Streamlit reads exclusively from the pre-built database — no source files, no dbt process, no build step in production.
flowchart LR
A["Python Script\ngenerate_data.py"] -->|"9 parquet files\n83K rows, seed=42"| B[/"data/raw/*.parquet"/]
B -->|"read_parquet()"| C["dbt-duckdb\n9 staging models"]
C -->|"materialize"| D["dbt-duckdb\n8 mart models · 66 tests"]
D -->|"ship as static artifact"| E[("data/sentinel.duckdb\n~14 MB")]
D -.->|"run_results.json\nmanifest.json"| F[/"dbt artifacts"/]
E -->|"read-only at runtime"| G["Streamlit App\nstreamlit_app.py"]
F -->|"parsed at runtime"| G
G --> H["Tab 1: Operations\nSite leads · mission commanders"]
G --> I["Tab 2: Detection Analytics\nMission analysts · product engineers"]
G --> J["Tab 3: Reliability\nReliability engineering"]
G --> K["Tab 4: Pipeline Health\nData team"] Star Schema
Two dimension tables anchor the schema. Four fact tables capture distinct operational
realities: daily fleet telemetry, active deployments, component reliability against MTBF
targets, and inventory stock levels. All four facts are queryable independently, and
fct_fleet_health_daily
drives both the Operations top-line KPIs and the Reliability uptime trend — numbers
stay consistent across tabs without per-tab aggregation logic.
erDiagram
DIM_SITE {
string site_id PK
string site_name
string region
string env
int active_tower_count
int total_tower_count
}
DIM_TOWER {
string tower_id PK
string site_id FK
string status
int age_days
}
FCT_FLEET_HEALTH_DAILY {
string tower_id FK
date telemetry_date
float avg_uptime_pct
float avg_sensor_health
int incidents_opened
}
FCT_DEPLOYMENT_STATUS {
string deployment_id PK
string tower_id FK
string site_id FK
float duration_hours
string status
}
FCT_COMPONENT_RELIABILITY {
string component_id PK
string component_type
float observed_mtbf_hours
float target_mtbf_hours
}
FCT_INVENTORY_STATUS {
string part_number PK
int available_stock
string stock_status
int lead_time_days
}
FCT_DETECTION_EVENTS {
string detection_id PK
string tower_id FK
string site_id FK
string target_class
float confidence_score
bool auto_resolved
string time_of_day
}
FCT_ALERT_PIPELINE_LATENCY {
string detection_id PK
float detect_to_alert_sec
float alert_to_notify_sec
float notify_to_ack_sec
float ack_to_resolve_sec
float total_latency_min
}
DIM_SITE ||--o{ DIM_TOWER : "hosts"
DIM_TOWER ||--o{ FCT_FLEET_HEALTH_DAILY : "tower_id"
DIM_TOWER ||--o{ FCT_DEPLOYMENT_STATUS : "tower_id"
DIM_TOWER ||--o{ FCT_DETECTION_EVENTS : "tower_id"
FCT_DETECTION_EVENTS ||--o| FCT_ALERT_PIPELINE_LATENCY : "detection_id" dbt Model Lineage
Nine staging models — one per parquet source — handle only type casting and column
renaming. No business logic lives in staging. The eight mart models join across staging
to produce analytics-ready fact and dimension tables.
66 dbt tests cover primary key uniqueness, foreign key not-null constraints,
accepted_values
on all enum columns (status,
severity,
stock_status,
env,
target_class,
time_of_day),
and referential integrity from staging back to upstream dimensions. Source freshness is
configured on telemetry, detections, and alert_pipeline sources with 6-hour warn / 24-hour
error thresholds.
flowchart TD
SP[/"sites.parquet"/] --> SS["stg_sites"]
TP[/"towers.parquet"/] --> ST["stg_towers"]
TEL[/"telemetry.parquet"/] --> STEL["stg_telemetry"]
DP[/"deployments.parquet"/] --> SD["stg_deployments"]
IP[/"incidents.parquet"/] --> SI["stg_incidents"]
CP[/"components.parquet"/] --> SC["stg_components"]
INP[/"inventory.parquet"/] --> SIN["stg_inventory"]
DETP[/"detections.parquet"/] --> SDET["stg_detections"]
APP[/"alert_pipeline.parquet"/] --> SAP["stg_alert_pipeline"]
SS --> DS["dim_site"]
SS --> DT["dim_tower"]
ST --> DT
ST --> FH["fct_fleet_health_daily"]
STEL --> FH
SI --> FH
ST --> FD["fct_deployment_status"]
SD --> FD
SC --> FR["fct_component_reliability"]
SIN --> FIN["fct_inventory_status"]
SDET --> FDET["fct_detection_events"]
SDET --> FAP["fct_alert_pipeline_latency"]
SAP --> FAP
style FH fill:#2997ff,color:#fff,stroke:#0077ed
style FD fill:#2997ff,color:#fff,stroke:#0077ed
style FR fill:#2997ff,color:#fff,stroke:#0077ed
style FIN fill:#2997ff,color:#fff,stroke:#0077ed
style FDET fill:#2997ff,color:#fff,stroke:#0077ed
style FAP fill:#2997ff,color:#fff,stroke:#0077ed Key Engineering Decisions
Why ship a pre-built DuckDB file rather than running dbt at serve time?
Running dbt at deploy time introduces build dependencies — dbt-core, dbt-duckdb,
NumPy, PyArrow — into the runtime environment. Streamlit Community Cloud installs
only requirements.txt; keeping those four packages
out of it cuts deploy time and eliminates a class of production build failures.
More importantly, committing sentinel.duckdb and
the dbt artifacts to Git means every deploy is bit-for-bit reproducible — the same
data that was tested locally is exactly what runs in production. The tradeoff is a
larger repository (~14MB), which is acceptable for a dashboard consuming synthetic
telemetry.
Why three separate tabs rather than a single unified view?
A unified view forces a design compromise: KPI density that satisfies ops leads is
noise for engineers, and raw telemetry that's meaningful for reliability teams is
unactionable for site managers. Separating tabs by persona lets each view optimize
for its primary question without cluttering the others. The key constraint was that
all three tabs must share the same underlying numbers —
fct_fleet_health_daily drives both the Operations
active-tower count and the Reliability 60-day uptime chart. Persona-specific
presentation, shared fact layer.
Why four stock status levels instead of a binary in-stock / out-of-stock flag?
A binary flag answers the wrong question. Operations leads don't need to know whether
a part is available today — they need to know what to act on first and when lead time
becomes a problem. The four-level
stock_status (critical
→ reorder → watch
→ healthy) encodes both current stock and lead time
into a single actionable field. A part with two units on hand and a 56-day lead time
is categorically different from a part with two units and a 7-day lead time; a
binary flag treats them identically. The four levels give the procurement workflow
the prioritization signal it needs.
Why add detection events as a separate data domain rather than extending existing tabs?
Generic fleet dashboards show uptime and failures. What makes this a Sentry-specific platform
is depth in the product's primary value: detecting targets and getting operator alerts out fast.
The detection domain — target classification weights, confidence thresholds, auto-resolve logic,
and per-stage alert pipeline latency — can't be bolted onto reliability or operations metrics
because the grain is completely different: per-detection versus per-tower-day. Adding two new
fact tables (fct_detection_events,
fct_alert_pipeline_latency) keeps the schema clean and
surfaces the domain knowledge that makes this a mission analytics platform, not a generic IoT
dashboard. The tab order — Operations → Detection Analytics → Reliability → Pipeline Health —
puts detection second so it's the first thing an analyst reaches after the map.
Why surface dbt artifacts in the Pipeline Health tab?
Dashboard consumers have no visibility into whether the data they're reading is fresh.
Shipping run_results.json and
manifest.json alongside the database and parsing
them at runtime closes that gap — the Pipeline Health tab shows pass/fail counts,
step-level execution times, and the timestamp of the last successful build without
requiring the consumer to open a dbt Cloud UI or read a log file.
This is the same pattern I'd implement in any production data platform: observability
surfaced where the data is consumed, not siloed in the tooling that produced it.
Outcome & Reflection
The hardest design problem wasn't the data model — it was figuring out what each persona actually needs from the same underlying facts. Site leads want geographic distribution and open incidents sorted by severity. Reliability engineers want 60-day uptime trends and MTBF actuals versus targets. The data team wants build metadata and source freshness. Three very different views, one star schema.
The static artifact pattern — commit the compiled database, run nothing in production — is a discipline I'd carry into any Streamlit-based analytics application. It eliminates an entire class of deployment failure and makes the build reproducible by definition. The main limitation is refresh cadence: re-building and pushing is a manual step. The natural next iteration is a scheduled GitHub Action that runs the generator and dbt build nightly and commits the updated artifacts automatically.