fix(telemetry): Doors Opened the Longest shows card ID instead of cardholder name for card-opened doors #46
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#46
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?
📌 Problem Statement
In the Tactical Operations Deck (
DUAL_OPS_DECK), a door opened by a card credential shows the card ID instead of the cardholder name in the Doors Opened the Longest ranking panel (#tactical-open-longest-panel), rendering the tier-2 credential line asTARJETA: 1049283with noPERSONA:.Confirmed on production: the Live View activity stream shows the cardholder name correctly for the same events, while Doors Opened the Longest shows only the card ID. So the name is present in the data — the ranking panel is losing it, not the ingestion.
This is a follow-up to #24 (which introduced the tier-2 opening-method / credential display).
🔍 Root Cause Analysis (preliminary)
The two panels resolve person identity from different sources, and only one of them carries the name:
Live View —
CommandDeckAdapter.renderLiveActivityStreamrenderssnapshot.recentActivity, which the backend fills fromcycle_repo.get_recent_sync(50)(app/services/door_service.py:1259area). That list includes the named access/card event, so the name shows.Doors Opened the Longest —
CommandDeckAdapter.renderOpenLongestrenderssnapshot.openLongest, whosepersonNameis resolved inget_door_overviewfrom the single active-open cycle:app/services/door_service.py:1133-1141get_active_for_door_syncreturns the most recent cycle withis_open = 1for that door (app/db/cycle_repository.py:58-70). That active-open cycle carries the card number but an emptyperson_name, so the ranking item ends up withcardNoand nopersonName. The frontend then correctly renders card-only (command_deck_adapter.js:864-878showsPERSONA:only whenpersonNameis non-empty).In other words: the cardholder name and the active-open state live in two different cycle rows for the same door. Live View sees the whole list and shows the named one; the ranking keys off the active-open row alone, which is the one without the name.
🧭 Likely Direction (deferred — needs its own design pass)
Not scoping the fix here. Candidate approaches, to be evaluated in the follow-up:
get_door_overview, when the active-open cycle hascardNobut blankpersonName, backfill the name from the correlated card event for the same door (match bydoor_index_codewithin a short time window).personNameonto theis_open = 1row so a single lookup already has it.openLongestresolve identity the same way Live View does, from the recent-cycle list, rather than the lone active cycle.Option 2 is the most robust (fixes the data, not the view), but touches the aggregator (
access_cycle_aggregator.py) and event ingestion; option 1 is the smallest change. The right call depends on whether the card and open events are guaranteed to correlate cleanly — that's the design question the follow-up needs to answer.Reproduction
TARJETA: <id>with no name.Scope note
The frontend needs no change — it renders card-only precisely because
personNamearrives blank. The fix is entirely backend identity resolution for the open-longest ranking.✅ Acceptance Criteria
PERSONA: <name>) in#tactical-open-longest-panel, alongside or in place of the card ID, consistent with what Live View shows for the same door.open_longestitem carriespersonName.✅ Design settled (grilling session)
Approach chosen — resolve from HikCentral by
personId, not by correlating local rows. The earlier read-time local-match /door/eventsre-query fallback is dropped: it built robust correlation logic to work around a key we had simply chosen not to capture. Simpler to capture the key.Root cause (confirmed)
The named card event and the
is_open = 1cycle are two different rows: the OpenAPI poll writes the card event as a standaloneis_open = 0row viacycle_repo.upsert_sync, bypassing the aggregator's existing name-backfill (access_cycle_aggregator.py:82-88). The active-open cycle keepscardNobut nopersonName. We currently parse onlypersonName,cardNo,picUri,readerNamefromdoor/events— no person id.Settled spec
personId(orpersonCode) fromacs/v1/door/eventsduring ingestion, and propagate it onto the cycle rows the same waycardNois propagated. (Implementer validates the exact field name in the live payload — the catalog has bothperson/personId/personInfoandperson/personCode/personInforesolvers.)get_door_overview, when the active-open cycle has apersonIdbut blankpersonName, call HikCentral/api/resource/v1/person/personId/personInfo(orpersonCode/personInfo).door/eventsre-query fallback — all obviated by carrying the id.PERSONA:wheneverpersonNameis non-empty.Acceptance criteria (supersede the "Likely Direction" section)
personId/personCodecaptured fromdoor/eventsand carried onto cycle rows alongsidecardNo.person/personId/personInfo, cached onto the cycle.PERSONA: <name>in#tactical-open-longest-panel, matching Live View.Re-tagged
ready-for-agent.