refactor(telemetry): deepen the backend streaming seam (Candidate 2; candidates 3 & 4 delivered) #14

Closed
opened 2026-09-09 15:57:45 +00:00 by gabogg · 3 comments
Owner

Corrected 2026-09-22 after a grilling session checked this issue against master (1bb097d). Two of the three candidates had already shipped, and the remaining candidate's stated friction did not match the code. The original text is preserved under "Original framing" below; the corrections are recorded in "What the code actually shows".

Remaining scope: Candidate 2 only, and only two of its four items. Tracked in the telemetry-streaming PR.

Originating context: spun off from the architectural review of PR #13 (WIP: docs(ui): industrial brutalist frontend redesign and tactical telemetry guidelines).


Status of each candidate

Candidate Status
2 — Deepen the backend telemetry streaming seam Partly live, partly refuted. Two real items remain; see below
3 — Unified Command Deck module ✅ Delivered 2026-09-10 (Issue #17, PR #13, commits 286467f, 781759a)
4 — Standalone zero-build Tailwind CLI seam ✅ Delivered 2026-09-10 (Issue #19, PR #13, commit d4660b6)

What the code actually shows

Candidate 2's first friction claim is false. It states the broadcast "drops individual directional passage vector events and emits coarse aggregates." At master:

  • app/services/occupancy_service.py:528-552 builds a full directional event (camera_index_code, camera_name, direction, count, timestamp_epoch, timestamp_formatted) and broadcasts it as recent_event.
  • PassengerFlowEvent (app/schemas/occupancy_models.py:234) already is the proposed PassageFluxVector; it also ships in bulk via OccupancyLiveResponse.recent_passages (:285).
  • The client already models it — TelemetryEngine.ingestMessage absorbs recent_event into _recentFluxVectors (telemetry_engine.js:564-572, :605-615) with no follow-up fetch.

What is genuinely discarded: the group-level delta_in/delta_out (:1898-1899) and _last_group_readings (:1925) never leave the function, and the terminal sync broadcast at :1938 carries only a scalar events_created.

Candidate 2's second friction claim is true, and has a server-side twin — app/static/js/app.js:524 refetches GET /api/occupancy/overview on every push, and record_counting_event_async broadcasts once per event with a full get_live_occupancy_async() recomputation each time (:543-552).

Candidate 3's premise is refuted. It describes a shallow split across src/deck/command_deck.js and src/components/*. Neither directory exists, and neither ever did. The actual tree is src/telemetry/telemetry_engine.js, src/ui/command_deck_adapter.js, src/ui/cctv_matrix.js, src/ui/calibration_desk.js, src/utils.js. CommandDeckAdapter already has exactly the prescribed interface — mount(target) (:161) and render(snapshot) (:215) — with 1675 lines behind ~3 exports, so "an interface nearly as large as its implementation" does not describe it. The one item not delivered is requestAnimationFrame batching; render() writes innerHTML synchronously.

Candidate 4's target does not exist. app/static/css/styles.css is absent, and index.html is 1628 lines, not the "1,917 lines of legacy HTML utilities" described. The standalone CLI seam is built: scripts/build-css.sh drives scripts/tailwindcss to produce tactical-bundle.min.css, with zero CDN references remaining. Its "zero Node.js" framing is also contradicted by ADR 0002:34, which mandates a Node 22 test harness.


Remaining scope

  1. Delete the app.js:524 follow-up fetch; consume recent_event as TelemetryEngine already does.
  2. Hoist the broadcast out of record_counting_event_async into one batched emission per sync tick.

Spun out as separate issues: broadcasting door_hardware_state_transitions; requestAnimationFrame batching; a pinned-checksum fetch for the gitignored scripts/tailwindcss plus an ADR 0002 amendment.

No longer blocked by #26 / #31. The 2026-09-21 blocking analysis applied to a scope that would have published count, timestamp and camera as an operator-facing vector stream. The remaining two items publish nothing new.


Original framing (2026-09-09) — retained for history

Candidate 2: Deepen the Backend Telemetry Streaming Seam (Ports & Adapters)

  • Target modules: app/services/monitor_service.py, app/services/occupancy_service.py, app/schemas/occupancy_models.py
  • Friction: the WebSocket broadcast drops individual directional passage vector events and emits coarse aggregates; every occupancy_update pushes the client into a secondary GET /api/occupancy/overview round-trip; the tactical UI requires high-frequency directional vector streams and hardware contact transitions omitted from the broadcast payload.
  • Solution: publish atomic PassageFluxVector (camera, direction, count, timestamp) and DoorStateTransition events alongside cycle timestamps and clock-skew references; client ingestion absorbs these without follow-up REST requests.

Candidate 3: Consolidate Tactical Operations into a Unified Command Deck Module

  • Target modules: app/static/js/src/deck/command_deck.js, app/static/js/src/components/*
  • Friction: slicing the deck into separate hud.js, portal_matrix.js, occupancy_tachometer.js, calibration_console.js and video_wall.js creates shallow modules with coordinated visual events leaking across multiple independent DOM writers.
  • Solution: a single deep CommandDeckAdapter exposing only mount(element) and render(snapshot), coordinating the sectors in an atomic requestAnimationFrame batch.

Candidate 4: Standalone Zero-Build Tailwind CLI Tooling Seam

  • Target modules: app/static/css/styles.css, app/static/index.html, docs/standards/ui-design-guidelines.md
  • Friction: PR #13 forces a choice between thousands of lines of handcrafted CSS and heavy Node.js/npm dependencies that violate ADR 0002.
  • Solution: an offline compilation seam using the official standalone Tailwind CLI binary, compiling a minified tactical stylesheet with zero npm and zero Node.js.
> **Corrected 2026-09-22** after a grilling session checked this issue against `master` (`1bb097d`). Two of the three candidates had already shipped, and the remaining candidate's stated friction did not match the code. The original text is preserved under "Original framing" below; the corrections are recorded in "What the code actually shows". > > **Remaining scope: Candidate 2 only, and only two of its four items.** Tracked in the telemetry-streaming PR. **Originating context**: spun off from the architectural review of PR #13 (*WIP: docs(ui): industrial brutalist frontend redesign and tactical telemetry guidelines*). --- ## Status of each candidate | Candidate | Status | | :--- | :--- | | **2** — Deepen the backend telemetry streaming seam | **Partly live, partly refuted.** Two real items remain; see below | | **3** — Unified Command Deck module | ✅ **Delivered** 2026-09-10 (Issue #17, PR #13, commits `286467f`, `781759a`) | | **4** — Standalone zero-build Tailwind CLI seam | ✅ **Delivered** 2026-09-10 (Issue #19, PR #13, commit `d4660b6`) | --- ## What the code actually shows **Candidate 2's first friction claim is false.** It states the broadcast *"drops individual directional passage vector events and emits coarse aggregates."* At `master`: - `app/services/occupancy_service.py:528-552` builds a full directional event (`camera_index_code`, `camera_name`, `direction`, `count`, `timestamp_epoch`, `timestamp_formatted`) and broadcasts it as `recent_event`. - `PassengerFlowEvent` (`app/schemas/occupancy_models.py:234`) already **is** the proposed `PassageFluxVector`; it also ships in bulk via `OccupancyLiveResponse.recent_passages` (`:285`). - The client already models it — `TelemetryEngine.ingestMessage` absorbs `recent_event` into `_recentFluxVectors` (`telemetry_engine.js:564-572`, `:605-615`) with no follow-up fetch. What *is* genuinely discarded: the group-level `delta_in`/`delta_out` (`:1898-1899`) and `_last_group_readings` (`:1925`) never leave the function, and the terminal sync broadcast at `:1938` carries only a scalar `events_created`. **Candidate 2's second friction claim is true**, and has a server-side twin — `app/static/js/app.js:524` refetches `GET /api/occupancy/overview` on every push, and `record_counting_event_async` broadcasts once per event with a full `get_live_occupancy_async()` recomputation each time (`:543-552`). **Candidate 3's premise is refuted.** It describes a shallow split across `src/deck/command_deck.js` and `src/components/*`. **Neither directory exists, and neither ever did.** The actual tree is `src/telemetry/telemetry_engine.js`, `src/ui/command_deck_adapter.js`, `src/ui/cctv_matrix.js`, `src/ui/calibration_desk.js`, `src/utils.js`. `CommandDeckAdapter` already has exactly the prescribed interface — `mount(target)` (`:161`) and `render(snapshot)` (`:215`) — with 1675 lines behind ~3 exports, so "an interface nearly as large as its implementation" does not describe it. The one item not delivered is `requestAnimationFrame` batching; `render()` writes `innerHTML` synchronously. **Candidate 4's target does not exist.** `app/static/css/styles.css` is absent, and `index.html` is 1628 lines, not the "1,917 lines of legacy HTML utilities" described. The standalone CLI seam is built: `scripts/build-css.sh` drives `scripts/tailwindcss` to produce `tactical-bundle.min.css`, with zero CDN references remaining. Its "zero Node.js" framing is also contradicted by ADR 0002:34, which *mandates* a Node 22 test harness. --- ## Remaining scope 1. Delete the `app.js:524` follow-up fetch; consume `recent_event` as `TelemetryEngine` already does. 2. Hoist the broadcast out of `record_counting_event_async` into one batched emission per sync tick. Spun out as separate issues: broadcasting `door_hardware_state_transitions`; `requestAnimationFrame` batching; a pinned-checksum fetch for the gitignored `scripts/tailwindcss` plus an ADR 0002 amendment. **No longer blocked by #26 / #31.** The 2026-09-21 blocking analysis applied to a scope that would have *published* `count`, `timestamp` and `camera` as an operator-facing vector stream. The remaining two items publish nothing new. --- <details> <summary><strong>Original framing (2026-09-09) — retained for history</strong></summary> ### Candidate 2: Deepen the Backend Telemetry Streaming Seam (Ports & Adapters) - **Target modules**: `app/services/monitor_service.py`, `app/services/occupancy_service.py`, `app/schemas/occupancy_models.py` - **Friction**: the WebSocket broadcast drops individual directional passage vector events and emits coarse aggregates; every `occupancy_update` pushes the client into a secondary `GET /api/occupancy/overview` round-trip; the tactical UI requires high-frequency directional vector streams and hardware contact transitions omitted from the broadcast payload. - **Solution**: publish atomic `PassageFluxVector` (camera, direction, count, timestamp) and `DoorStateTransition` events alongside cycle timestamps and clock-skew references; client ingestion absorbs these without follow-up REST requests. ### Candidate 3: Consolidate Tactical Operations into a Unified Command Deck Module - **Target modules**: `app/static/js/src/deck/command_deck.js`, `app/static/js/src/components/*` - **Friction**: slicing the deck into separate `hud.js`, `portal_matrix.js`, `occupancy_tachometer.js`, `calibration_console.js` and `video_wall.js` creates shallow modules with coordinated visual events leaking across multiple independent DOM writers. - **Solution**: a single deep `CommandDeckAdapter` exposing only `mount(element)` and `render(snapshot)`, coordinating the sectors in an atomic `requestAnimationFrame` batch. ### Candidate 4: Standalone Zero-Build Tailwind CLI Tooling Seam - **Target modules**: `app/static/css/styles.css`, `app/static/index.html`, `docs/standards/ui-design-guidelines.md` - **Friction**: PR #13 forces a choice between thousands of lines of handcrafted CSS and heavy Node.js/npm dependencies that violate ADR 0002. - **Solution**: an offline compilation seam using the official standalone Tailwind CLI binary, compiling a minified tactical stylesheet with zero npm and zero Node.js. </details>
Author
Owner

Candidate 4 Delivery: Standalone Zero-Build Tailwind CLI Tooling Seam

Delivered via Phase 5 (Issue #19 / PR #13 / Commit d4660b6):

  1. Standalone Tailwind CLI Tooling:
    • Single standalone binary scripts/tailwindcss (v3.4.17 standalone Linux x64 executable) with zero npm, zero Node.js runtime, zero package.json, and zero node_modules dependencies (100% ADR 0002 compliance).
    • Build automation script scripts/build-css.sh compiling minified offline app/static/css/tactical-bundle.min.css (36KB) with --minify.
    • Complete compilation covering secondary legacy tabs (#content-console, #content-probes, #content-logs) and shared brutalist UI components without unstyled layout collapse.
  2. Permanent CDN Severance:
    • Eliminated all external runtime CDNs (cdn.tailwindcss.com, cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, fonts.googleapis.com) from app/static/index.html.
    • Replaced all FontAwesome icon classes across the application with monospaced tactical ASCII badges ([HUD], [OCC], [ANL], [CCTV], [CLI], [LOG], [CAM], [CALC], [CFG], [✓], [✕], [!]).
  3. Automated Air-Gapped Quality Gates:
    • test_index_html_strictly_air_gapped_no_external_cdns strictly asserting zero external http:// or https:// resource links in HTML.
    • test_dom_invariants_zero_radius_and_tabular_nums asserting tabular-nums on dynamic metrics and universal 0px border-radius enforcement.
    • test_standalone_tailwind_cli_binary_executable verifying local CLI binary presence and execution permissions.
    • 100% offline verification across 115 pytest tests and 24 Node unit tests.
### Candidate 4 Delivery: Standalone Zero-Build Tailwind CLI Tooling Seam Delivered via Phase 5 (Issue #19 / PR #13 / Commit `d4660b6`): 1. **Standalone Tailwind CLI Tooling**: - Single standalone binary `scripts/tailwindcss` (v3.4.17 standalone Linux x64 executable) with zero npm, zero Node.js runtime, zero `package.json`, and zero `node_modules` dependencies (100% ADR 0002 compliance). - Build automation script `scripts/build-css.sh` compiling minified offline `app/static/css/tactical-bundle.min.css` (36KB) with `--minify`. - Complete compilation covering secondary legacy tabs (`#content-console`, `#content-probes`, `#content-logs`) and shared brutalist UI components without unstyled layout collapse. 2. **Permanent CDN Severance**: - Eliminated all external runtime CDNs (`cdn.tailwindcss.com`, `cdnjs.cloudflare.com`, `cdn.jsdelivr.net`, `unpkg.com`, `fonts.googleapis.com`) from `app/static/index.html`. - Replaced all FontAwesome icon classes across the application with monospaced tactical ASCII badges (`[HUD]`, `[OCC]`, `[ANL]`, `[CCTV]`, `[CLI]`, `[LOG]`, `[CAM]`, `[CALC]`, `[CFG]`, `[✓]`, `[✕]`, `[!]`). 3. **Automated Air-Gapped Quality Gates**: - `test_index_html_strictly_air_gapped_no_external_cdns` strictly asserting zero external `http://` or `https://` resource links in HTML. - `test_dom_invariants_zero_radius_and_tabular_nums` asserting `tabular-nums` on dynamic metrics and universal 0px border-radius enforcement. - `test_standalone_tailwind_cli_binary_executable` verifying local CLI binary presence and execution permissions. - 100% offline verification across 115 pytest tests and 24 Node unit tests.
Author
Owner

Candidate 3 Delivery: Unified Command Deck Module (CommandDeckAdapter)

Delivered via Phase 3 & Phase 4 (Issue #17 / PR #13 / Commits 286467f, 781759a):

  1. Deep Module View Adapter Architecture:

    • Implemented app/static/js/src/ui/command_deck_adapter.js encapsulating the split-screen tactical operations deck behind a clean, deep interface: mount(rootElementOrOptions) and render(snapshot).
    • Replaces fragile, multi-writer DOM cascades with a single declarative render pass executing upon frozen TelemetrySnapshot broadcasts from TelemetryEngine.
    • Atomically coordinates the four operational sectors:
      • Master Tactical HUD Ribbon (52px fixed): WS connection state, latency, server clock skew (\Delta t), business cycle phase, midnight reset countdown, instantaneous headcount O(t), residual error margin \pm \epsilon, active exit multiplier k, and dynamic alarm counter.
      • Left Wing Tactical Portal Matrix (60%): ASCII-framed door tiles ([ P-XX ]), hardware contact states ([ CLOSED ], [ OPEN ], [ ALARM ], [ OFFLINE ]), sensor wiring categories (VERIFIED_SENSOR, SENSORLESS_OPEN, SENSORLESS_JUMPERED), open elapsed duration timers, and dynamic alarm hoisting to top.
      • Right Wing Live Occupancy Tachometer (40%): Instantaneous headcount, Little's Law dwell capacity envelope (W_max) with proportional meter bar, macroscopic gross inflow/outflow tallies, net delta flux, and multiplier k.
      • Directional Flux Stream Feed: Reverse-chronological traversal feed calibrated to facility UTC offset (-240m), with high-contrast directional vector markers (>>> IN, <<< OUT).
  2. Unidirectional Read Stream & Action Delegation:

    • Container-level event delegation intercepts [data-action="toggle_override"], [data-action="unlock"], and dispatches cleanly through the onAction callback seam.
    • Decoupled from global scope with configurable authentication token resolution (getAuthToken).
    • Wired in app.js to authenticated API endpoints (authFetch, toggleDoorExclusion) and bound in index.html via telemetryEngine.subscribe(snapshot => commandDeckAdapter.render(snapshot)).
  3. Automated Verification Harness:

    • Complete unit test coverage in tests/frontend/test_command_deck_adapter.test.js (7 tests passing via Node 22 native test runner).
    • Validates formatting helpers, HUD rendering, matrix alarm hoisting, tachometer envelope calculations, flux log parsing, callback action dispatching, and mount() binding.
    • Formally gated within the offline repository test suite via pytest tests/test_frontend_modules.py.
### Candidate 3 Delivery: Unified Command Deck Module (`CommandDeckAdapter`) Delivered via Phase 3 & Phase 4 (Issue #17 / PR #13 / Commits `286467f`, `781759a`): 1. **Deep Module View Adapter Architecture**: - Implemented `app/static/js/src/ui/command_deck_adapter.js` encapsulating the split-screen tactical operations deck behind a clean, deep interface: `mount(rootElementOrOptions)` and `render(snapshot)`. - Replaces fragile, multi-writer DOM cascades with a single declarative render pass executing upon frozen `TelemetrySnapshot` broadcasts from `TelemetryEngine`. - Atomically coordinates the four operational sectors: - **Master Tactical HUD Ribbon (52px fixed)**: WS connection state, latency, server clock skew (\Delta t), business cycle phase, midnight reset countdown, instantaneous headcount O(t), residual error margin \pm \epsilon, active exit multiplier k, and dynamic alarm counter. - **Left Wing Tactical Portal Matrix (60%)**: ASCII-framed door tiles (`[ P-XX ]`), hardware contact states (`[ CLOSED ]`, `[ OPEN ]`, `[ ALARM ]`, `[ OFFLINE ]`), sensor wiring categories (`VERIFIED_SENSOR`, `SENSORLESS_OPEN`, `SENSORLESS_JUMPERED`), open elapsed duration timers, and dynamic alarm hoisting to top. - **Right Wing Live Occupancy Tachometer (40%)**: Instantaneous headcount, Little's Law dwell capacity envelope (W_max) with proportional meter bar, macroscopic gross inflow/outflow tallies, net delta flux, and multiplier k. - **Directional Flux Stream Feed**: Reverse-chronological traversal feed calibrated to facility UTC offset (-240m), with high-contrast directional vector markers (`>>> IN`, `<<< OUT`). 2. **Unidirectional Read Stream & Action Delegation**: - Container-level event delegation intercepts `[data-action="toggle_override"]`, `[data-action="unlock"]`, and dispatches cleanly through the `onAction` callback seam. - Decoupled from global scope with configurable authentication token resolution (`getAuthToken`). - Wired in `app.js` to authenticated API endpoints (`authFetch`, `toggleDoorExclusion`) and bound in `index.html` via `telemetryEngine.subscribe(snapshot => commandDeckAdapter.render(snapshot))`. 3. **Automated Verification Harness**: - Complete unit test coverage in `tests/frontend/test_command_deck_adapter.test.js` (7 tests passing via Node 22 native test runner). - Validates formatting helpers, HUD rendering, matrix alarm hoisting, tachometer envelope calculations, flux log parsing, callback action dispatching, and `mount()` binding. - Formally gated within the offline repository test suite via `pytest tests/test_frontend_modules.py`.
Author
Owner

⛔ Candidate 2 is blocked — the per-camera granularity it streams does not exist upstream

A data-veracity audit of the ingestion pipeline on master (filed as #26–#36) lands directly on this candidate. Candidate 3 and Candidate 4 are delivered and unaffected; Candidate 2 as specified is not implementable today, and one of its findings also applies to something already shipped.


Every field of PassageFluxVector is a defect

Candidate 2 proposes publishing atomic PassageFluxVector (camera, direction, count, timestamp) in place of coarse aggregates:

Field Blocked by Why
camera #28 HikCentral's resourceGroupRealTimeCount returns counts per resource group, not per camera. occupancy_service.py:1903 manufactures a per-camera breakdown with delta_in // num_in. There is no per-camera signal in the source data to stream.
camera #29 When a group exposes no member resources, the resource-group code is written as camera_index_code (:1826-1830). The vector would carry a group code in a field named camera.
timestamp #31 Events are stamped with poll time, not traversal time (:1909, :1921). After any ingestion gap the entire backlog carries the single timestamp of the recovery poll.
count #26 delta = max(0, artemis_total - local_total) (:1898) clamps to zero for the whole remainder of a cycle after any upstream counter reset, emitting nothing and logging nothing.

The deepening itself is right — the aggregate-only broadcast is the wrong seam. But streaming 11:34:02 >>> IN +3 (CAM-01) from this pipeline publishes a fabricated camera at a fabricated second with a count that can silently be zero. It would make an artifact look like an observation, at 1 Hz, on a wall display.

#28 is a precondition for this candidate, not a parallel workstream. Its resolution also decides Candidate 2's shape: if this HikCentral version exposes per-camera passenger flow, PassageFluxVector is correct as written; if it does not, the atom is a GroupFluxVector and the candidate needs rewriting before implementation.


The live flux stream is already affected

This is the part that is not hypothetical. Candidate 3's delivered CommandDeckAdapter includes a Directional Flux Stream Feed rendering per-camera >>> IN / <<< OUT markers. It is fed by:

# app/services/occupancy_service.py:540-552
await self.repo.record_event_async(evt)
live_overview = await self.get_live_occupancy_async()
if ws_manager.active_connections:
    await ws_manager.broadcast({
        "type": "occupancy_update",
        "live": live_overview.model_dump(),
        "recent_event": evt,          # <- carries camera_name from the even split
        ...
    })

evt["camera_name"] comes straight from the integer division, and membership of in_cams / out_cams is decided by keyword matching on camera names (infer_camera_direction_and_zone, :341), where ACCESO maps to ENTRANCE.

For a group named the way this facility names its portals — ACCESO NORTE, ACCESO SUR, ESTACIONAMIENTO — the first two classify as entrance-only and out_cams collapses to [ESTACIONAMIENTO]. Every <<< OUT line currently scrolling in the operator flux stream is attributed to the parking camera, and the two main doors show ingress only.

So #28 is not only a blocker for future work; it is a live defect in a shipped view. Worth raising its priority above the rest of the #26–#36 set for that reason.


A server-side twin of Candidate 2's second friction point

Candidate 2 correctly identifies the client-side push-then-pull query storm. There is a matching one on the server, in the same code path:

record_counting_event_async calls get_live_occupancy_async() and broadcasts per event, not per sync tick. Because the even split converts a single group delta into N per-camera events, one 3-second poll triggers N full live-occupancy recomputations and N WebSocket fan-outs where one of each would do.

Fixing #28 — ingesting one event per group instead of N fabricated per-camera events — reduces that by a factor of N on its own. If per-camera ingestion does turn out to be available, the broadcast should still be hoisted out of record_counting_event_async and emitted once per sync tick with a batch of vectors. That batching belongs in this candidate's scope.


Suggested sequencing

  1. #28 — settle whether per-camera passenger flow is available upstream. Decides the atom.
  2. #29, #26, #31 — make the count, the camera and the timestamp trustworthy.
  3. Then implement Candidate 2, with the broadcast hoisted to one batched emission per sync tick.

Candidate 2 should stay open. It is blocked, not wrong.

## ⛔ Candidate 2 is blocked — the per-camera granularity it streams does not exist upstream A data-veracity audit of the ingestion pipeline on `master` (filed as #26–#36) lands directly on this candidate. Candidate 3 and Candidate 4 are delivered and unaffected; **Candidate 2 as specified is not implementable today**, and one of its findings also applies to something already shipped. --- ### Every field of `PassageFluxVector` is a defect Candidate 2 proposes publishing atomic `PassageFluxVector (camera, direction, count, timestamp)` in place of coarse aggregates: | Field | Blocked by | Why | | :--- | :--- | :--- | | `camera` | **#28** | HikCentral's `resourceGroupRealTimeCount` returns counts **per resource group**, not per camera. `occupancy_service.py:1903` manufactures a per-camera breakdown with `delta_in // num_in`. There is no per-camera signal in the source data to stream. | | `camera` | **#29** | When a group exposes no member resources, the **resource-group code** is written as `camera_index_code` (`:1826-1830`). The vector would carry a group code in a field named `camera`. | | `timestamp` | **#31** | Events are stamped with poll time, not traversal time (`:1909`, `:1921`). After any ingestion gap the entire backlog carries the single timestamp of the recovery poll. | | `count` | **#26** | `delta = max(0, artemis_total - local_total)` (`:1898`) clamps to zero for the whole remainder of a cycle after any upstream counter reset, emitting nothing and logging nothing. | The deepening itself is right — the aggregate-only broadcast *is* the wrong seam. But streaming `11:34:02 >>> IN +3 (CAM-01)` from this pipeline publishes a fabricated camera at a fabricated second with a count that can silently be zero. It would make an artifact look like an observation, at 1 Hz, on a wall display. **#28 is a precondition for this candidate, not a parallel workstream.** Its resolution also decides Candidate 2's shape: if this HikCentral version exposes per-camera passenger flow, `PassageFluxVector` is correct as written; if it does not, the atom is a `GroupFluxVector` and the candidate needs rewriting before implementation. --- ### The live flux stream is already affected This is the part that is not hypothetical. Candidate 3's delivered `CommandDeckAdapter` includes a **Directional Flux Stream Feed** rendering per-camera `>>> IN` / `<<< OUT` markers. It is fed by: ```python # app/services/occupancy_service.py:540-552 await self.repo.record_event_async(evt) live_overview = await self.get_live_occupancy_async() if ws_manager.active_connections: await ws_manager.broadcast({ "type": "occupancy_update", "live": live_overview.model_dump(), "recent_event": evt, # <- carries camera_name from the even split ... }) ``` `evt["camera_name"]` comes straight from the integer division, and membership of `in_cams` / `out_cams` is decided by keyword matching on camera names (`infer_camera_direction_and_zone`, `:341`), where `ACCESO` maps to `ENTRANCE`. For a group named the way this facility names its portals — `ACCESO NORTE`, `ACCESO SUR`, `ESTACIONAMIENTO` — the first two classify as entrance-only and `out_cams` collapses to `[ESTACIONAMIENTO]`. **Every `<<< OUT` line currently scrolling in the operator flux stream is attributed to the parking camera**, and the two main doors show ingress only. So #28 is not only a blocker for future work; it is a live defect in a shipped view. Worth raising its priority above the rest of the #26–#36 set for that reason. --- ### A server-side twin of Candidate 2's second friction point Candidate 2 correctly identifies the client-side push-then-pull query storm. There is a matching one on the server, in the same code path: `record_counting_event_async` calls `get_live_occupancy_async()` and broadcasts **per event**, not per sync tick. Because the even split converts a single group delta into N per-camera events, one 3-second poll triggers **N full live-occupancy recomputations and N WebSocket fan-outs** where one of each would do. Fixing #28 — ingesting one event per group instead of N fabricated per-camera events — reduces that by a factor of N on its own. If per-camera ingestion does turn out to be available, the broadcast should still be hoisted out of `record_counting_event_async` and emitted once per sync tick with a batch of vectors. That batching belongs in this candidate's scope. --- ### Suggested sequencing 1. #28 — settle whether per-camera passenger flow is available upstream. Decides the atom. 2. #29, #26, #31 — make the count, the camera and the timestamp trustworthy. 3. Then implement Candidate 2, with the broadcast hoisted to one batched emission per sync tick. Candidate 2 should stay open. It is blocked, not wrong.
gabogg changed title from refactor(architecture): telemetry streaming, command deck, and standalone CSS tooling deepening to refactor(telemetry): deepen the backend streaming seam (Candidate 2; candidates 3 & 4 delivered) 2026-09-22 16:31:09 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gabogg/hikcentral#14
No description provided.