CrowdVision · source-available, not open source · © 2026 Nicolò Ghignatti
Implementation reference for the dashboard. For its architectural role, see Contracts Service Architecture; for the domain model, see Telemetry Distribution.
The only Rust service in the platform. Built on Axum and Tokio, it runs an HTTP API and a background telemetry tunnel in a single process, holding per-building preferences in memory for a lock-free hot path.
The source is organised by role: a composition root and shared kernel at the crate root, the two inbound adapters (api/ for HTTP, tunnel.rs for the broker stream), and outbound integrations plus cross-cutting infrastructure grouped under infra/.
| Path | Responsibility |
|---|---|
src/main.rs | Composition root: connect Mongo, hydrate the in-memory map, start the tunnel, serve the Axum router. |
src/state.rs | AppState: the shared DashMap projection plus the typed Mongo collection handle. |
src/models.rs | PreferenceDocument, MetricContract, and the discovery response types. |
src/api/ (dashboard.rs, data.rs, init.rs) | Inbound HTTP handlers: the aggregated catalog (GET /), preferences CRUD, and defaults seeding. |
src/tunnel.rs | Inbound stream worker: routes telemetry:raw to each event’s own building, and records fan-out metrics. |
src/infra/db.rs | Mongo access: connect (with unique index), load_all (hydration), upsert_preference. |
src/infra/discovery.rs | Resolves downstream metric endpoints from *_METRICS_URL environment variables. |
src/infra/metrics.rs | Prometheus registry, telemetry counters and fan-out latency, and the GET /metrics handler. |
graph TD
MAIN["main.rs\ncomposition root"] --> API["api/\ndashboard.rs, data.rs, init.rs"]
MAIN --> TUNNEL["tunnel.rs\nbackground worker"]
MAIN --> DB["infra/db.rs\nconnect + hydrate"]
API --> STATE["state.rs\nAppState: DashMap + Mongo handle"]
TUNNEL --> STATE
API --> DISC["infra/discovery.rs"]
API --> METRICS["infra/metrics.rs"]
TUNNEL --> METRICS
STATE -.-> DB| Variable | Default | Purpose |
|---|---|---|
MONGO_URI | mongodb://localhost:27017 | Durable store for preferences (database crowdvision, collection preferences). |
REDIS_URL | redis://127.0.0.1/ | Broker for the telemetry tunnel. |
PORT | 3000 | HTTP listen port. |
*_METRICS_URL | — | Any variable with this suffix is treated as a service base URL for metric discovery. |
pub struct AppState {
pub building_preferences: Arc<DashMap<String, Vec<String>>>, // hot path
pub mongo_col: Collection<PreferenceDocument>, // durable
}
pub struct PreferenceDocument {
pub id: Option<ObjectId>,
pub building_id: String, // unique index
pub allowed_columns: Vec<String>,
}DashMap shards its internal locks across cores, so the tunnel’s millions of reads almost never contend with an occasional write. The Mongo collection enforces one document per building via a unique index on building_id.
1. connect(MONGO_URI) -> ensure unique index on building_id
2. load_all() -> seed the DashMap from persisted documents
3. start_telemetry_tunnel() -> spawn the background filter task
4. axum::serve(router) -> begin handling HTTPHydration at boot means a restart restores all preferences before the first request is served.
Reads and the tunnel operate purely in memory; durability is handled off the request path.
DashMap lookups; the tunnel never blocks on a write.update_preferences mutates the DashMap immediately, then spawns a fire-and-forget Tokio task to upsert_preference into Mongo. The HTTP response returns 200 without waiting for the database. The same write-through applies to init when it creates a new entry.Because the database write is fire-and-forget, a crash in the narrow window between the in-memory insert and the persisted upsert can lose the most recent change. This is an accepted trade-off for hot-path latency; the next successful save reconciles it.
All routes except /health and /metrics require the mesh-verified x-gateway-claims header (decoded by a GatewayClaims extractor that returns 401 on a missing or malformed header) — the same contract every other service consumes. The user-facing routes are gated at the edge (require_gateway_auth in the Caddyfile, the /contracts AuthorizationPolicy rule in Kubernetes); POST /preferences/init is an internal call and receives the building creator’s claims forwarded by digital-twin. This enforces authentication only; per-building authorization is deferred to the Cedar phase.
| Method · Path | Description |
|---|---|
GET /preferences/{building_id} | Return { building_id, allowed_columns }. 404 if the building has no entry. Requires x-gateway-claims. |
POST /preferences/{building_id} | Replace the allowed-column list (body { allowed_columns }). Instant cache write + async persist. Returns { status, building_id, active_columns }. Requires x-gateway-claims. |
POST /preferences/init/{building_id} | Idempotently seed defaults ["roomName", "roomMaxOccupancy"] with an atomic or-insert, so a repeat call is a safe no-op. Called by the digital-twin on building creation, forwarding the creator’s x-gateway-claims. |
GET / | The aggregated metric catalog (see Metric Discovery). Requires x-gateway-claims. |
GET /metrics | Prometheus exposition: telemetry events received/published counters and fan-out latency. Open (scraped in-cluster). |
Started at boot, the tunnel opens a dedicated Redis PubSub connection and subscribes to telemetry:raw.
sequenceDiagram
participant R as Redis (telemetry:raw)
participant T as Tunnel
participant D as DashMap
participant F as Redis (telemetry:filtered:{id})
R-->>T: { type, buildingId, roomId, value, ... }
Note over T: spawn a task per message (ingestion never blocks)
T->>D: is buildingId a known building?
alt known
T->>F: PUBLISH telemetry:filtered:{building_id} (full payload)
endtelemetry_schema::TelemetryEnvelope — one shared definition with telemetry, which produces it. Anything that is not a tick (no buildingId, no readings array, no ingestedAt) is logged and dropped before routing.buildingId: a reading is published to that one building’s channel and no other, so it can never leak into another tenant’s feed. The exact bytes received are republished — the envelope is parsed to route it, never to rebuild it, because a reading’s fields belong to whichever plugin produced them.allowed_columns only drive what the dashboard table displays (client-side).roomName, temperature, totalDeviceCount). No frontend map translates it. The DashMap is consulted just to confirm the building is known.task::spawn, so processing time never back-pressures the subscription stream.resolve_channel) so it is unit-tested in isolation; the worker increments received/published counters and observes fan-out latency, exported via GET /metrics.GET / builds the catalog the frontend uses to populate column pickers.
discover_services() collects every environment value whose key ends in _METRICS_URL.GET /contracts is fetched and deserialised (either a ServiceMetricsContract or a bare metric list).source and added via push_unique_metric, which deduplicates on (kind, interface, source).New metric types therefore appear automatically once a service advertises them; no change is needed here.
The types in step 2 are not defined here. MetricContract, ServiceMetricsContract and MetricsDiscoveryResponse live in schemas/telemetry-schema, a serde-only crate that telemetry builds its response from and this service parses into. One definition, so the producer and the consumer cannot disagree.
They did disagree. After the Rust migration telemetry emitted key and fields[].kind while this service parsed metricKey and fields[].type. MetricsDiscoveryResponse is untagged, so both variants failed, fetch_service_metrics swallowed it as “one bad service is skipped”, and the catalog came back empty — meaning no user could add or swap a dashboard column. A hand-maintained JSON contract file would not have caught it, because nothing would have read it. A shared struct turns it into a compile error.
There is no DELETE /preferences/{building_id}. When a building is removed from the digital-twin, its entry remains in both the DashMap and Mongo, and the tunnel keeps evaluating it. A deletion endpoint (called from the twin on removal) is the intended fix.