refactor(telemetry): deepen the backend streaming seam (Candidate 2; candidates 3 & 4 delivered) #14
Labels
No labels
blocked
bug
enhancement
high-priority
low-priority
needs-info
needs-triage
ready-for-agent
ready-for-human
referenced
research
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gabogg/hikcentral#14
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
286467f,781759a)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-552builds a full directional event (camera_index_code,camera_name,direction,count,timestamp_epoch,timestamp_formatted) and broadcasts it asrecent_event.PassengerFlowEvent(app/schemas/occupancy_models.py:234) already is the proposedPassageFluxVector; it also ships in bulk viaOccupancyLiveResponse.recent_passages(:285).TelemetryEngine.ingestMessageabsorbsrecent_eventinto_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:1938carries only a scalarevents_created.Candidate 2's second friction claim is true, and has a server-side twin —
app/static/js/app.js:524refetchesGET /api/occupancy/overviewon every push, andrecord_counting_event_asyncbroadcasts once per event with a fullget_live_occupancy_async()recomputation each time (:543-552).Candidate 3's premise is refuted. It describes a shallow split across
src/deck/command_deck.jsandsrc/components/*. Neither directory exists, and neither ever did. The actual tree issrc/telemetry/telemetry_engine.js,src/ui/command_deck_adapter.js,src/ui/cctv_matrix.js,src/ui/calibration_desk.js,src/utils.js.CommandDeckAdapteralready has exactly the prescribed interface —mount(target)(:161) andrender(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 isrequestAnimationFramebatching;render()writesinnerHTMLsynchronously.Candidate 4's target does not exist.
app/static/css/styles.cssis absent, andindex.htmlis 1628 lines, not the "1,917 lines of legacy HTML utilities" described. The standalone CLI seam is built:scripts/build-css.shdrivesscripts/tailwindcssto producetactical-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
app.js:524follow-up fetch; consumerecent_eventasTelemetryEnginealready does.record_counting_event_asyncinto one batched emission per sync tick.Spun out as separate issues: broadcasting
door_hardware_state_transitions;requestAnimationFramebatching; a pinned-checksum fetch for the gitignoredscripts/tailwindcssplus 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,timestampandcameraas 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)
app/services/monitor_service.py,app/services/occupancy_service.py,app/schemas/occupancy_models.pyoccupancy_updatepushes the client into a secondaryGET /api/occupancy/overviewround-trip; the tactical UI requires high-frequency directional vector streams and hardware contact transitions omitted from the broadcast payload.PassageFluxVector(camera, direction, count, timestamp) andDoorStateTransitionevents 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
app/static/js/src/deck/command_deck.js,app/static/js/src/components/*hud.js,portal_matrix.js,occupancy_tachometer.js,calibration_console.jsandvideo_wall.jscreates shallow modules with coordinated visual events leaking across multiple independent DOM writers.CommandDeckAdapterexposing onlymount(element)andrender(snapshot), coordinating the sectors in an atomicrequestAnimationFramebatch.Candidate 4: Standalone Zero-Build Tailwind CLI Tooling Seam
app/static/css/styles.css,app/static/index.html,docs/standards/ui-design-guidelines.mdCandidate 4 Delivery: Standalone Zero-Build Tailwind CLI Tooling Seam
Delivered via Phase 5 (Issue #19 / PR #13 / Commit
d4660b6):scripts/tailwindcss(v3.4.17 standalone Linux x64 executable) with zero npm, zero Node.js runtime, zeropackage.json, and zeronode_modulesdependencies (100% ADR 0002 compliance).scripts/build-css.shcompiling minified offlineapp/static/css/tactical-bundle.min.css(36KB) with--minify.#content-console,#content-probes,#content-logs) and shared brutalist UI components without unstyled layout collapse.cdn.tailwindcss.com,cdnjs.cloudflare.com,cdn.jsdelivr.net,unpkg.com,fonts.googleapis.com) fromapp/static/index.html.[HUD],[OCC],[ANL],[CCTV],[CLI],[LOG],[CAM],[CALC],[CFG],[✓],[✕],[!]).test_index_html_strictly_air_gapped_no_external_cdnsstrictly asserting zero externalhttp://orhttps://resource links in HTML.test_dom_invariants_zero_radius_and_tabular_numsassertingtabular-numson dynamic metrics and universal 0px border-radius enforcement.test_standalone_tailwind_cli_binary_executableverifying local CLI binary presence and execution permissions.Candidate 3 Delivery: Unified Command Deck Module (
CommandDeckAdapter)Delivered via Phase 3 & Phase 4 (Issue #17 / PR #13 / Commits
286467f,781759a):Deep Module View Adapter Architecture:
app/static/js/src/ui/command_deck_adapter.jsencapsulating the split-screen tactical operations deck behind a clean, deep interface:mount(rootElementOrOptions)andrender(snapshot).TelemetrySnapshotbroadcasts fromTelemetryEngine.[ 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.>>> IN,<<< OUT).Unidirectional Read Stream & Action Delegation:
[data-action="toggle_override"],[data-action="unlock"], and dispatches cleanly through theonActioncallback seam.getAuthToken).app.jsto authenticated API endpoints (authFetch,toggleDoorExclusion) and bound inindex.htmlviatelemetryEngine.subscribe(snapshot => commandDeckAdapter.render(snapshot)).Automated Verification Harness:
tests/frontend/test_command_deck_adapter.test.js(7 tests passing via Node 22 native test runner).mount()binding.pytest tests/test_frontend_modules.py.⛔ 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
PassageFluxVectoris a defectCandidate 2 proposes publishing atomic
PassageFluxVector (camera, direction, count, timestamp)in place of coarse aggregates:cameraresourceGroupRealTimeCountreturns counts per resource group, not per camera.occupancy_service.py:1903manufactures a per-camera breakdown withdelta_in // num_in. There is no per-camera signal in the source data to stream.cameracamera_index_code(:1826-1830). The vector would carry a group code in a field namedcamera.timestamp:1909,:1921). After any ingestion gap the entire backlog carries the single timestamp of the recovery poll.countdelta = 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,
PassageFluxVectoris correct as written; if it does not, the atom is aGroupFluxVectorand 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
CommandDeckAdapterincludes a Directional Flux Stream Feed rendering per-camera>>> IN/<<< OUTmarkers. It is fed by:evt["camera_name"]comes straight from the integer division, and membership ofin_cams/out_camsis decided by keyword matching on camera names (infer_camera_direction_and_zone,:341), whereACCESOmaps toENTRANCE.For a group named the way this facility names its portals —
ACCESO NORTE,ACCESO SUR,ESTACIONAMIENTO— the first two classify as entrance-only andout_camscollapses to[ESTACIONAMIENTO]. Every<<< OUTline 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_asynccallsget_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_asyncand emitted once per sync tick with a batch of vectors. That batching belongs in this candidate's scope.Suggested sequencing
Candidate 2 should stay open. It is blocked, not wrong.
refactor(architecture): telemetry streaming, command deck, and standalone CSS tooling deepeningto refactor(telemetry): deepen the backend streaming seam (Candidate 2; candidates 3 & 4 delivered)