NVR Fleet OTA
signed releases · mesh-boundThe union of three sources: mirror-poll (boards whose OTA agent polls /deploy/region), mesh (device-mesh peers, exported by the host timer), and clickhouse (devices with a recent ingest heartbeat). A device in the mesh that never adopted the agent says so on its row; a dead source degrades to a chip below the table, never this table.
Version is the board's OWN reported version (board-confirmed > board-reported > heartbeat) — never a version the board did not report; a DRIFT chip beside it means the board's installed config disagrees with git (hover it for which of deps / env / unit / funnel). Temp / load is the board's own telemetry — it greys out past ~5 min and shows an em-dash on an agent that predates it, never zeros. Admin Update still GPG-verifies, health-gates and auto-reverts on the board.
| Board | Mesh | Channel | Temp / load | Ingest / poll | Version | Update |
|---|---|---|---|---|---|---|
| loading… | ||||||
Per board, whether each producing pipeline is shipping data: analytics (the always-on detection heartbeat into ClickHouse, last 24h — count · age, source in the cell tooltip), learn (hard-example frames night-trickled to the ingest store), brand (brand observations swept into the central store) and vehicle (role=vehicle observations routed to the learner's tree).
A leg is flowing when fresh, stale when data exists but is past its cadence window, empty when the source is healthy but the board has produced nothing, and unknown when the source itself is down (for vehicle: never armed until the first board stages an observation). A dead source degrades to a chip below the table, never this table.
| Device | Analytics | Learn (hard examples) | Brand | Vehicle | Overall |
|---|---|---|---|---|---|
| loading… | |||||
Every board state transition (up → degraded → offline → recovered) is recorded with a classified reason — site-power / uplink-loss / mesh-flap / backend-crash / ota-halt / camera-dark / cloud-leg-dark / unknown — from signals the console already collects (heartbeat age, detection counts, per-camera frame recency, mesh peer last-seen, OTA apply status).
The AI analysis line is the pluggable diagnosis hook: with a BYO-LLM wired it summarizes the ranked report; with none it is a deterministic template (no external calls). A missing store shows an honest empty report, never an error.
| Board | Current state | Failures (window) | Reasons | Since |
|---|---|---|---|---|
| loading… | ||||
| Reason | Count | Severity | Boards |
|---|
| Board | Failures | Episodes | Leading reason | MTBF | Current streak | Cause |
|---|
Per board, the motion-gate's own counters (cumulative since board start): seen motion frames, static skipped (gated as no-motion), processed on-board, backfilled to the VPS and dropped — dropped > 0 means the bounded backfill budget was exhausted and real motion frames were LOST (alarm-worthy; includes disk-lost staged frames, also shown separately).
Plus the thermal shed state (a board shedding drops detector frames while otherwise looking healthy), per-detector health, and the Alert FIFO — the analytics engine's bounded alert queue, where dropped > 0 means alert folds were LOST on-board (evidence loss, not lag; hover for offered count and fold-lag p50).
Storage is the flash-media probe's read of the SD card the whole board runs from. A board whose agent predates these counters shows not reported by this board version — never zeros.
| Board | Motion seen | Static skipped | Processed | Backfilled | Dropped | Disk lost | Thermal | Detectors | Alert FIFO | Storage |
|---|---|---|---|---|---|---|---|---|---|---|
| loading… | ||||||||||
Checks run on the VPS against data the boards already ship — heartbeats, detections, disk, TLS certs. Thresholds are hysteretic, so alerts do not flap, and each detector clears on its own asymmetric threshold.
A check whose source is down shows as a blind source below, never as a false alert. Critical alerts sort first. Read-only: the boards' hot path is untouched.
Cut canary
main → release-canary · signed
Promote
canary → fleet · fast-forward only
Cut fleet
main → fleet (naroda) · two-person
Sync models
model blobs → mirror
Every signed release commit on release-canary and fleet, newest first across both. A release is cut from main — the two channels are signed siblings, not a fast-forward line, which is why Cut fleet exists beside Promote.
An UNSIGNED row is the exception worth seeing: every board GPG-verifies against its pinned key before applying, so an unsigned tip is a release no board will take.
| Channel | Commit | When | Subject |
|---|---|---|---|
| loading… | |||
Every model blob on the mirror cross-checked against the board manifests. MISSING = a manifest names it and the mirror does not have it (boards cannot fetch it). size mismatch = the blob is there but not the size the manifest declares. orphan = on the mirror, in no manifest. declared — unbuilt = an all-zero sentinel sha: the build has not been cut yet, so nothing is expected on the mirror.
Precision is the manifest's own 5th column, shown as a suffix on the model name — never guessed for a row that has no manifest entry.
| Model | Role | Boards | Status | Size |
|---|---|---|---|---|
| loading… | ||||
Mirror storage
Frames already on the VPS (collector + night-trickle ingest) — nothing is pulled off a board.
Boxes = class + confidence, from ingest hard-example frames. Collector frames carry no detector output.
Zones / crop come from each board's CAM_N_EXCLUDE_ZONES / CAM_N_CROP.
Box placement is best-effort — ingest frames are downscaled on the VPS.
Images load only while this tab is open.
Mirror inventory
| Board | Channel | Last seen |
|---|---|---|
| loading… | ||
Every board's agent is gated: a new signed release is recorded as pending and applied only when the approved sha names it exactly. Approve & apply writes that approval for the board's current channel tip; the board picks it up over the mesh on its next tick (≤60s + fetch), verifies the GPG signature against its pinned key, health-dwells, and auto-reverts on failure.
Once the board reports back, this table shows the board-confirmed status — never a console guess. It says up to date only when the board's own deployed_healthy_sha equals the approved sha.
Update all outdated approves every polling board that is behind its channel tip; per-board gates are unchanged and non-polling / off-channel boards are skipped. Clear approval (behind ⋯) stops future fetches but cannot recall an approval a board already pulled — it is not a kill-switch.
Lifecycle (behind ⋯) drives deploy/fork-unify/migrate-rock3b.sh and deploy/mesh/mesh-acl.sh grant over the mesh, exactly as run from the VPS shell. swap and rollback are the only disruptive stages and get an extra confirm; swap is health-gated with automatic rollback to the fork. One job runs at a time; PASS/FAIL is the job's exit code in the drawer.
| Board | Release | Status | |
|---|---|---|---|
| loading… | |||
Not adopted
| Device | Seen | Lifecycle |
|---|---|---|
| loading… | ||
Each learning run's position on the contract state machine (ARRIVED → LABELED → REVIEWED → CURATED → TRAINED → GATE_* → DEV_PROVEN → PROMOTED).
Train now runs python -m training.pipeline.run_cycle --run <id> in the teacher venv and advances AUTO edges only. The two hard stops are never bypassed from here: the human review edge is crossed only by saving a review in Review frames, and past the eval gate a candidate still needs the dev-board proof and an explicit human promote-fleet on the CLI.
force-train bypasses the frame-accumulate threshold and nothing else.
Fine-tune → quantize → beat-incumbent gate → emit. A PASS emits a candidate; nothing here auto-promotes.
A candidate becomes promoted only once its sha lands in a board's models.int8.manifest and its blob is on the mirror — an explicit human step on the CLI.
| Candidate | Role · target | Gate | Cand vs incumbent | Stages | Promotion |
|---|---|---|---|---|---|
| loading… | |||||
Four steps, top to bottom, in the numbered panels on this tab:
- Collect — the A0 collector grabs one live frame per camera every ~10 min, per site/camera and sun phase.
- Label frames — a big YOLO teacher draws boxes on every collected frame into a new run. (The first real run downloads the teacher weights once.)
- Review each frame — look at the drawn boxes and Keep or Reject each, fix a wrong class from the box's dropdown, drag in a box the teacher missed (misses are the highest-value signal), then Save review.
- Apply review → curate — assembles the curated ground-truth set from the frames you kept. Rejected and un-reviewed frames are left out.
Nothing auto-promotes. Curated sets are what the fine-tune → gate → promote pipeline scores against — a human always decides which boxes are true.
Auto-labels a new run with teacher_label.py on the VPS. Pick a site/cam subset or label ALL raw frames.
The dedicated teacher venv (/opt/nvr-teacher-venv) is wired in; the first REAL run downloads yolo11x.pt once. If ultralytics is still missing the tool falls back to its DRY-RUN stub and the job log says so.
Click Keep if the boxes are on the objects, Reject if they are wrong or misplaced.
If a box has the wrong class (an autorickshaw the COCO teacher called truck), fix it from that box's class dropdown. If the teacher missed an object, click + Add missed box, drag a box around it and pick its class — misses are the highest-value training signal. The × on a box removes a misdraw. Then Save review.
The standalone spot-check sheet is still available as a fallback from the runs table below.
Each labeled run and where it stands. Review frames opens it in the reviewer above; Apply review assembles the curated set from the frames you kept.
Open sheet and Upload review JSON (behind ⋯) are the standalone-sheet fallback path.
Runs whose reviewers rejected a large share of the teacher's boxes are archived — hidden from the reviewer dropdown and this list by default. Nothing is deleted; tick show archived to see them.
| Run | Frames | Labeled | Stage | |
|---|---|---|---|---|
| loading… | ||||
Hard-example intake
Hard-example frames the boards night-trickle to the VPS (low-confidence / tracker-gap / random), as counted at the receiving end. Board-side sampler counters are not shipped here; the histograms are over the newest frames per device.
| Device | Frames | On disk | Last received | Cameras | Keep reasons | Confidence bands | Hard examples |
|---|---|---|---|---|---|---|---|
| loading… | |||||||
Status is a background health poll — this page never probes. up = whole recent window green · flaky = intermittent · down = whole window red · unknown = probeable, no sample yet · not probed = deliberately not polled.
The poller only GETs an unauthenticated /health on a live surface with a configured URL; it never sends a credential and never touches a design surface. Deployment (live running · opt-in operator-enabled · design specified-not-deployed) rides the name.
Spec: docs/design/api-funnel-standard-2026-07.md
| Name | Status | Reason | Via |
|---|---|---|---|
| loading… | |||
Boards observe — during quiet hours each board OCRs wordmarks and night-trickles a tiny crop + the text it read for any confident brand match. Those land here as observations.
Review each observation — look at the crop, the OCR text and the brand it proposed. Accept (the match is right — the text it read is added as an alias), Correct (pick the real brand, or type a new one), or Reject (not a brand / wrong). Then Save review.
Curate the list — the brand-list editor is the source of truth: add/remove aliases, add a brand no board has seen yet, and assign boards to customers so a brand can be per-customer or general.
Publish — publish the curated list as ONE signed central artifact. Cut-canary → promote carries it to every board; a board that never saw a brand still gets it.
Nothing auto-publishes — a human always decides. Quality-first: prefer Reject over shipping a wrong brand. Stub-OCR observations are badged and never silently curated.
Boards observe → you review → curate the list → publish one signed list to the fleet.
The central list, general + per-customer. Add or remove aliases on a brand, or add a brand no board has seen yet (covers brands you know about ahead of time). This is what gets published to the fleet.
An accepted observation from a board assigned to a customer goes under that customer's list; unassigned boards feed the general list.
Board → customer
| Board | Customer | |
|---|---|---|
| no boards seen yet | ||
Publishes the curated list as one signed central artifact (models/brandlist.json) and bumps the board manifests. It self-validates through the on-board consumer and refuses to ship a list the fleet couldn't load. Then use Releases → Cut canary → Promote to carry it to the boards.
The logo gallery is the VISUAL half of the loop (embeddings of accepted crops + reference logos). Rebuild is idempotent + data-gated; without real CLIP weights it honestly rebuilds a STUB (badged) — a real, publishable gallery needs weights provisioned on the VPS.
| Published version |
|---|
| never published |
Intake
| Device | Observations | Pending | Reviewed | Last received | Cameras |
|---|---|---|---|---|---|
| loading… | |||||
Recognition events
The gated on-board brand-recognition analytics leg (NVR_BRAND_RECOGNITION) names product/logo brands on detected objects and writes analytics_brand rows to ClickHouse. This is a SEPARATE surface from the file-store teach loop above. It stays empty until the leg is armed on a board AND a real gallery/OCR is provisioned — honest awaiting/degrade states, never fabricated rows.
Brand wedge counts analytics_brand rows carried by the on-board below-floor persistence wedge (NVR_BRAND_WEDGE) rather than a direct floor read.
| Device | Brand | Events | Real | Stub | Wedge | Last seen |
|---|---|---|---|---|---|---|
| loading… | ||||||
Class columns and Folds are raw detection-frame folds — one count per detected object per detector pass, so they show relative activity, not vehicle counts. Tracks counts unique vehicles and is the number Analytics sums. autorickshaw is a fleet-custom class; 0 means the board's model does not emit it.
Raw detection folds per class · tracks are a separate, smaller unit
| Device | Tracks | Folds | Last |
|---|---|---|---|
| loading… | |||
The MAKE/model the analytics worker's symbol-gallery + rear-nameplate-OCR pass emits off the real-time hot path (backend/analytics/vehicle.py), read from analytics_vehicle_brand events in nvr_monitor.anomaly_events. Every make is a model estimate (provenance: symbol+ocr), never ground truth — a human curates makes on the Brand tab. Confidence is the fused symbol+OCR score (uncalibrated).
The pass is gated + inert until a vehicle-symbol gallery and OCR weights are provisioned: an empty table with a healthy source is the honest "detection running, nothing recognized yet" state, not an error.
| Make | Model | Class | Obs | Devices | Avg conf | Last seen |
|---|---|---|---|---|---|---|
| loading… | ||||||
Review flags for human triage; nothing here triggers an automated response. Known limitations + DPDP note: docs/design/safety-analytics-2026-07.md
| Sev | Time | Device / camera | Type | Subject | Ev | |
|---|---|---|---|---|---|---|
| loading… | ||||||
Internal review flags, never an automated action. Computed on-board from the person/pose/face tracks the boards already produce. Gender is an estimate. Internal, mesh-only surface — customers see anonymized aggregates only.
Camera-minute rows shipped to ClickHouse (nvr_monitor.detections) in the window. People / Vehicles are summed average-concurrent counts — an activity proxy, not unique individuals.
| Device | Camera | Records | Last | People | Vehicles |
|---|---|---|---|---|---|
| loading… | |||||
| Username | Role | Status | Last login | |
|---|---|---|---|---|
| loading… | ||||
Display-only label. It shows next to the id everywhere but does NOT rename the board — use Device ID rename for that.
A site is the operator-declared grouping the command rail folds into per-site rollups (Country › State › City); boards with no site group under “Unassigned”.
| Board | Site | Channel | Last seen | |
|---|---|---|---|---|
| loading… | ||||
Re-attributes the board's cloud rows from this instant. Coordinate with a tools/deviceid_migrate run (freeze window) so the site analytics timeline stays continuous. Applied fail-safe on the next agent tick; the board restarts its backend once.
Re-attributes cloud rows from this instant. Applied on the next agent tick; backend restarts once.
| Serial | Assigned DEVICE_ID |
|---|---|
| loading… | |
| When | User | Action | Details | Result |
|---|---|---|---|---|
| loading… | ||||
These rows are IDENTIFYING by design (real visitor ids + per-sighting visit history). Internal use only — never shown to a customer. Every read is audited. The face-identity service is off by default; when it is off this panel says so.
The stored 512-d vectors are never displayed. Margin is the matcher's best-minus-second gap at match time — a risky badge flags a merge under 0.10; pre-#167 marks an older observation written before the margin was persisted. Person / track is the detection linkage: person_id plus an exact ByteTrack track N once the K2 track key is threaded through the payload (absent today, rendered the moment it lands, never fabricated).
Verify pairs: near-threshold pairs mined from the galleries. Your Same / Different / Can't tell verdict is stored as training ground truth (append-only — a later click overrides). Face crops are not stored yet, so pairs show as metadata cards.
| Visitor id | Site | Visits | Days | First seen | Last seen | Gallery | Enrolled q. | |
|---|---|---|---|---|---|---|---|---|
| Load to query the P-FACE service. | ||||||||
| When | Device | Quality | Match conf | Margin | Person / track | Gender | Age |
|---|---|---|---|---|---|---|---|
| Select a visitor above. | |||||||
Is this the same person?
Per-browser; overrides the container defaults NVR_FLEET_UI_THEME / NVR_FLEET_UI_LAYOUT.