feat(statistics-deck): deck shell — zero-scroll frame, top bar, picker, profiles, keyboard, data-quality marker (#133) #144
No reviewers
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!144
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/statistics-deck-shell"
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?
Closes #133.
Problem
#content-statisticswas a placeholder. The period views (#134 Day, #135 Week, #136 Month) need the shared deck shell from RFC #80 before they can be built: the zero-scroll frame, the 38 px top bar, the period picker, layout profiles, language, numpad control, and the data-quality marker frame.Approach
The shell lives in its own modules under
app/static/js/src/ui/statistics_deck/, not inapp.js:statistics_deck.jsregisterView(granularity, render)hook the period views plug into. Until a view is registered, each profile shows its placeholder grid.periods.jsWEEK MON 14 – SUN 20 SEP 2026 W38, including cross-month and cross-year weeks). Also the latest-complete default,-/+stepping, and#statistics/<granularity>/<start>deep links.quality.jsday_quality_state(). Also produces the badge text (spelled out in Room and Desk, glyphs and counts in Laptop) and the plain-language detail lines, which contain no internal codes.keyboard.jsKeyboardEvent.code, so it works with NumLock on or off.charts.jsautoSkip, no label rotation) and a plugin that draws the Estimate dashed amber outline and the Unreliable grey hatching, for bars and for gap columns on line graphs. There are no confidence bands.translate.jsstatisticsDeck.*keys ini18n.js.Frame.
statistics-deck.cssmakes the deck a fixed100dvh,overflow: hiddenframe:minmax(0, Nfr): Room 12/55/33, Desk 17/50/33, Laptop 26/74.Behaviour:
PerformanceNavigationTiming.type === 'reload') returns to the latest period.*detail panel fetches the period's daily rows lazily. Views reuse them throughctx.loadDays().Navigation:
#statistics/...hash takes them straight to the deck.Scope notes
analyticstab (F5) and get the deck on F8 until then.registerView; the shell renders placeholder panels in each profile's grid until they land.Verification
tests/frontend/test_statistics_deck.test.js, run by pytest:code, differentkey)hide()restoring the chromepytest416 passed, including the i18n parity and key-resolution tests;ruffclean;check_docs.pyclean.endis inclusive, while periods carry an exclusiveend, so the detail panel now requestsend − 1. A test covers it.🤖 Generated with Claude Code
Viewer checked in a real browser. I added a
vieweruser to this worktree's copy of the dev DB (the shared dev DB is untouched) and ran Chromium via Playwright:That run found that
hide()left the deck section without itshiddenclass; it was invisible only because its parent was hidden. Fixed in91cda04, and the unit test now asserts it.🤖 Generated with Claude Code
Code review: PR #144 (
feat/statistics-deck-shell@91cda04vsmaster@6f4791b)Standards sources: AGENTS.md,
docs/standards/code-standards.md,docs/standards/ui-design-guidelines.md, ADR 0002,git-and-workflow.md, and the Fowler smell baseline. Spec: issue #133 and the accepted RFCdocs/architecture/rfc-statistics-deck-display-model.md(§3, §4.1, §5.1, §5.2).Standards
Verified:
node --checkclean.createElement/textContentonly, noinnerHTML.gap: 1pxgrids, tabular-nums, token vars,transition: none, no CDN. Help uses<kbd>.show()calls don't stack it.<data value>. The badge counts, the picker coverage (5 / 7 DAYS), the period marks and the calendar day numbers are plain text.[ … ]), while the fullscreen label is.charts.jsDECK_COLORShard-codes hex copies of the CSS tokens (possible Duplicated Code). Read them from the computed style instead.hide()never runsviewCleanup, and logout drops the deck. Once #134–#136 register views, their Chart instances leak on every logout, and hidden decks keep live charts.loadPeriods.catch(() => [])shows "NO CLOSED PERIODS WITH DATA" during an outage, andselectGranularitythen discards the deep-link period.show()that resolves afterhide()or logout still runsafterSelection→renderViewon a hidden deck. Guard onvisibleafter the await.setLanguagedoesn't re-render an open panel, so a picker or help panel stays in the old language.renderPanelneither awaits nor catchesrenderQualityDetail, so an error becomes an unhandled rejection.periodMarks()and the calendar's glyph ternary repeatbadgeText's Laptop branch. ExtractperiodGlyphs()intoquality.js.statisticsDeckTitleis still used). No action.handleKey'sswitch (action.type)is a second dispatch table besideKEY_ACTIONS.Spec
Verified:
/help shows the diagram and the legend.day_quality_state(). Badge text matches the §4.1 examples.limit=100is the route's maximum, so the Day calendar and-/+stop about 100 days back, and older deep links silently fall back to the latest period.#statisticshash at login. No action.getHours()), while the server usesfacility_zone(). A presenter laptop in another zone shows wrong times.business_days == 0) "so the picker shows it as closed". The picker shows no closed indication.Summary. Standards: 10 actionable findings, worst S-4 (view charts leak on hide and logout). Spec: 5 actionable findings, worst C-4 (gap times in the browser's zone instead of the facility's). First pass, so all findings get fixed on this branch.
🤖 Generated with Claude Code
Review fixes:
962f465Every finding from the review is addressed, except S-2, which is kept on purpose (reason below).
Standards
withData()wraps every number in<data value>: badge counts, picker coverage, period marks, calendar day numbers. Tested.◆ 2 OF 7 DAYS UNRELIABLE · 1 ESTIMATED, Laptop◆ 2 · ◇ 1), and #133's acceptance ties to it. The RFC is the more specific spec, so it outranks the general §5.1 framing rule. The badge is still visually framed by its 1 px amber border.resolveDeckColors()reads--color-warning-amber,--color-text-mutedand--color-border-gridfrom the computed style; the hex values remain only as fallbacks. Tested.disposeView()runs the view cleanup on every re-render and inhide()(logout callshide()).registerView(g, null)unregisters a view. Tested with a registered view.tab.errorset and returnsnull, and it is not cached. The top bar, view and picker show "PERIODS COULD NOT BE LOADED · PICK THE TAB TO RETRY"; the deep-linked period is kept, and picking the tab retries. Tested (fail, then recover).selectGranularityandstepcheckvisibleafter each await, andafterSelectionreturns early when hidden. Tested (show()resolving afterhide()writes nothing).setLanguagere-renders the open panel. Tested.renderPanelcatchesrenderQualityDetailfailures and shows the "could not be loaded" line.periodGlyphs()andworstGlyph()inquality.jsare used by the badge (Laptop), the picker marks and the calendar.periodMarks()is gone. Tested.ACTION_HANDLERSmaps eachkeyboard.jsaction type to a deck method; the switch is gone.Spec
analyticsleaves the admin decks, F5 opens the deck, and no admin tool becomes unreachable. So "Closes #133" leaves nothing untracked.GET /api/statistics/periodstakesbefore=<start>and returns periods older than it (backend test added). The deck pages in older periods from the picker (LOAD OLDER), when−steps past the oldest loaded period, and for deep links beyond the first page (up to 10 pages). Tested with 105 days.GapIntervalgains computedstart_label/end_label, formatted by the server infacility_zone()(DST-safe, the same approach as hourlylabel). The deck prints those labels and no longer callsgetHours(). Backend test: a 10:00 ± 60 s gap →09:59–10:01.business_days == 0periods show CLOSED in the list, get a struck-through calendar day with a title, and show a CLOSED notice in the view. Tested.flex: 0 0 auto; the period range shrinks with an ellipsis instead. Re-checked in Chromium with the dev data's marked week (◆ 7 DE 7 DÍAS NO FIABLES).Verification:
pytest: 416 passed, including the extendedtest_statistics_periods.py.ruffandcheck_docsare clean.🤖 Generated with Claude Code
Code review, second pass: PR #144 (
feat/statistics-deck-shell@962f465vsmaster@6f4791b)Both axes confirm that the first-pass fixes (S-1, S-3–S-11, C-1–C-6) are correct and introduced no regressions. S-2 (kept on purpose) was not re-raised.
Standards
Verified:
node --checkis clean.loadPeriodsandloadOldersharetab.loadingsafely:loadOlderreturns early until the first page exists, so neither gets the other's promise back with the wrong shape.visible.ACTION_HANDLERScovers all 10 action types.beforeis not checked for period alignment.?granularity=week&before=2026-09-24(a Thursday) builds Thursday–Wednesday "weeks" with the wrongiso_week. The deck only sends aligned starts, but the route is public.−past the oldest period fails silently:loadOlder.catch(() => null)sets no error state. This is the same pattern S-5 fixed for the first page.computed_field, with an import offacility_time). The siblingHourlyVisitors.labelis formatted in the service.currentDeckColors()andcachedHatchare cached once per page. That holds only while there is no theme switch; the assumption should be stated.loading,exhausted,error) is spread across two loaders that share one slot, relying on the unstated invariant "periods === null⇔ first page".Spec
Verified:
beforepaging reaches older periods from the picker, from−, and from deep links (up to about 1,100 days on the Day tab).facility_now(), clipped to the reset boundary.docs/api/README.mddoesn't mention the gapstart_label/end_labelfields or thebeforequery param.weekbeforethat isn't a Monday reaches_week_period_endand raisesValueError, which comes back as a 500 becauselist_periodslackshandle_controller_errors(). Month and day are unaffected.Summary. Standards: 5 findings, worst S2-1 (unvalidated
beforealignment, P2). Spec: 2 findings, both P3; worst C2-2 (the same root, surfacing as a 500).Next steps, per the review policy: fix S2-1/C2-2 on this branch. The remaining P3s (S2-2, S2-3, S2-4, S2-5, C2-1) move to a follow-up issue.
🤖 Generated with Claude Code
beforeon the periods route (#144 review)S2-1 / C2-2 fixed in
6dcb956. The service validates thatbeforeis a period start viastatistics_period_end(), andlist_periodsnow runs insidehandle_controller_errors(), so a non-Monday week or a month cursor not on the 1st gets 422VALIDATION_ERRORinstead of shifted weeks or a 500. There's a new test for both; the full suite passes (416).The remaining P3s (S2-2, S2-3, S2-4, S2-5, C2-1) are filed as a follow-up issue.