feat(statistics-deck): Day view (Room, Desk, Laptop) (#134) #149
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!149
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/deck-day-view"
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 #134
Problem
The statistics deck shell (#133, PR #144) renders labelled placeholders for the Day tab. The Day period needs its real content (RFC #80 §4): six KPIs and four graphs about one closed business day, laid out separately for the Room, Desk and Laptop profiles, with the data-quality marker (RFC §4.1) and the comparison rules ("—" with a reason, never 0%).
Approach
app/static/js/src/ui/statistics_deck/views/day_view.js. It callsregisterView('day', renderDayView)when imported, and is imported once fromindex.htmlnext to the StatisticsDeck import.dayKpis,hourlyGraph,peopleInsideGraph,entranceShare,daypartSplit,dayTier,comparisonText, formatters).renderDayViewonly builds the DOM and the Chart.js graphs from those inputs.periods/day/{date}/summary,timeseries/hourly?baseline=usual_weekday,periods/day/{date}/entrances,dwell/dayparts?baseline=usual_weekday, plus the shell'sloadDays()for the day's daily row (tier, holiday). A profile only fetches what it shows (Laptop skips entrances and dayparts). Each route that fails marks only its own cells.DAY_LAYOUTS), each keeping the top of the ranking:deckQualityMarksplugin on "visitors by hour" and "people inside". An Unreliable day (excluded or missing) greys out every graph: muted series, and every bucket hatched. The badge is the shell's. No per-KPI marks, no margins (margin_upper/margin_lowerignored).peak.timestamp_epoch; nothing is formatted from the browser clock. Numbers use the deck language's locale, and so do the Chart.js axes.statisticsDeck.views.day.*andstatisticsDeck.comparison.reason.<ComparisonReason>(all 10 codes), es and en, at the end of eachstatisticsDeckblock.app/static/css/statistics-deck-day.css, view-specific rules only.Verification
node --test tests/frontend/*.test.js: 159 pass. The newtests/frontend/test_statistics_deck_day.test.jshas 33 tests. They cover every Acceptance item: the profile layouts and rankings, route URLs, KPI mapping, "—" with a reason for no last-week data, no usual-weekday baseline (KPI and hourly graph), no last-year data (entrance YoY note and per-camera reason) and zero/closed references, gap buckets hatched exactly on both hourly graphs (also end to end through the rendered chart configs), an excluded day greying every graph cell, closed days, failed routes, cleanup, and es/en key parity.TZ=America/Caracas pytest -q: 417 passed.ruff check .andruff format --check .clean.python3 scripts/check_docs.pyclean.Decisions
last_year(RFC: "last year only when it has data"). The summary route omits it when there is no data. When it is present without a change (e.g.CLOSED_PERIOD), it shows "—" with the reason. The "no last-year data" acceptance case shows on the entrance-share graph: one noteVS LAST YEAR — no data last yearwhen the route has no year data at all, and a per-camera "—" withNO_CAMERA_DATA_LAST_YEAR.UsualDaypart.label, per #134. A null label, or no baseline at all, shows no span.minmax(96px, 12fr)for the Day tab, so a label, the figure and two comparison lines fit on short screens (RFC §3.5 lets a view adjust its split). At 1080 and 960 it is the shell's 12 % as before.top_entranceon an unreliable day, as it doesbusiest_hour(app/services/analytics_service.py, with a test intests/test_statistics_marker_inputs.pyand a note indocs/api/README.md). The tile then shows "—" with the reason.Open questions
NO_DATA_LAST_YEAR; it dropslast_yearinstead. If the maintainer wants the Visitors tile to always carry a last-year line with "—", the view only needssummary.last_yearto be kept with its reason.Known duplication left for consolidation
These are local to
day_view.json purpose, per the coordination rules with #135 and #136:formatCount,formatPercent,formatChange,formatDwell,numberLocale;comparisonText,reasonText,baselineReasonText("—" with a reason);kpiCell;entranceShare,entranceList);Outside the view files, this PR changes one
<link>and oneimportinindex.html, adds the i18n blocks, and makes the server-side top-entrance records fix (decision 8). No shell module was edited. This branch is rebased on #150 (fix/deck-shell-view-support), whose shell APIs it uses (ctx.createChart,deckLocale, shell colours, the rejectingloadDays). Until #150 merges, this PR's diff against master includes #150's two commits.🤖 Generated with Claude Code
Code review: PR #149 (
feat/deck-day-view@59db50fvs merge base08b6d1c)Standards sources: AGENTS.md,
docs/standards/code-standards.md,docs/standards/ui-design-guidelines.md, ADR 0002, the deck shell's exported helpers, and the Fowler smell baseline. Duplication across the Day, Week and Month views is known and will be consolidated after merge, so it isn't flagged here. Spec: #134 and RFC §3–§5.Standards
Verified: 28/28 tests pass and
node --checkis clean; there is noinnerHTML. Facility times come from the server'sbucket_time_label, and every chart is destroyed in cleanup behind analiveguard. Request count is 4 plusloadDaysin Room and 2 plusloadDaysin Laptop.Promise.allSettled([...]).then(...)chain has no.catch, and cells are filled without a try/catch. A throwing mapper or chart leaves the remaining cells on LOADING and stops the rest, which contradicts the PR's "each route that fails marks only its own cells".period.business_days === 0and the view-owned!period/closed states re-implementisClosedPeriodand the shell's empty states (now shell-owned in #150).resolveViewColors()copies the shell's token read and adds a second cache. #150 adds cyan and primary.numberLocale(tr)hard-codes locales (#150 addsctx.locale). The daypart tick callback(v) => `${v}%`ignores the deck locale (12.5%in Spanish).<data>.fillGraph's id cascade, theid === 'inside' ? …source pick, andGRAPH_TITLEall branch on the graph id. Use oneGRAPHStable.`— ${reason}`appears three times, and the legend wrapper is built twice..sd-day-greyed .sd-day-bar ihard-codesrgba(124, 135, 151, 0.55), duplicating the shell hatch.Spec
Verified:
same_weekday_last_week; the last-year line only when the summary includes it.margin_*and no confidence bands; people inside fromcumulative_occupancy.comparisonTextsetsdailyAverage, butkpiCellnever renders it. Render it or drop it.DwellDaypart.label, an agent-made departure from the issue text. Follow the issue.reliableis empty (analytics_service.py:628). Per "Records come from the route", add the samereliableguard for top entrance on the server, with a test.TIER_UNRELIABLE(full-column hatch) look the same as a greyed Unreliable day. ThegapNotecaption mitigates this, so it's acceptable. No action.Maintainer decision (applies to Day, Week and Month): when there is no last-year data, hide the last-year line, per RFC §5.1 "last year only when it has data". Day already does this.
Summary. Standards: 8 findings, worst S-1 (an unhandled rejection leaves cells on LOADING, P1). Spec: 5 actionable findings, worst C-4/C-5 (daypart-label fallback against the issue text; top entrance on an Unreliable day). First pass, so all findings get fixed on this branch. S-2, S-3 and S-4 build on the shell-support PR #150.
🤖 Generated with Claude Code
59db50f7c55d3ba7e4f2Review fixes: all findings addressed in
5d3ba7eThe branch is rebased on #150 (
fix/deck-shell-view-support) and force-pushed with lease.allSettledchain ends in a.catchthat turns any cell still pending into "could not be loaded". The test uses a throwingpeakmapper and a throwing chart; the other tiles and graphs still fill and nothing stays on LOADING.views.day.closedkey); the shell owns them in #150.ctx.loadDays()now rejects on failure, and the marker then falls back to the period's counts (tested).resolveViewColors()is gone. Colours come fromcurrentDeckColors()(cyan,primary,muted).ctx.createChart, which sets the deck locale. Formatting uses the shell'sdeckLocale, and the oldnumberLocaleis gone. Daypart ticks useformatTickPercentin the deck locale ("12,5%" in Spanish, tested).withDatainto<data>elements (tested).GRAPHStable (source route plusfill) replaces the id cascade, the source pick andGRAPH_TITLE. KPIs use oneKPI_BUILDERStable.kpiHeader,missingText("— reason") andlegendhelpers..sd-swatch-unreliable.DAILY_AVERAGEchange is rendered as "+4.0% · DAILY AVG" (tested).UsualDaypart.label. A null label, or no baseline, shows no span.top_entrancewhen no day is reliable, as it doesbusiest_hour. There is a new backend test intests/test_statistics_marker_inputs.py, anddocs/api/README.mdis updated. The tile shows "— unreliable day, no records".Verification:
TZ=America/Caracas pytest -q: 417 passed.ruff check,ruff format --checkandcheck_docs.pyare clean.🤖 Generated with Claude Code
Code review, second pass: PR #149 (Day) @
5d3ba7eAll first-pass fixes are verified. 76 node tests pass, and
tests/test_statistics_marker_inputs.pypasses 9/9.reliableguard asbusiest_hour, a test that fails without the fix, anddocs/api/README.mdupdated.UsualDaypart.label.Standards
withData, which splits on\d+. "+5.2%" becomes<data value=5>+<data value=2>, and Spanish "1.234" becomes1+234. The test attest_statistics_deck_day.test.js:538locks in the wrong values.withDatais for whole counts; wrap formatted figures in one<data value={raw}>each.chart()keeps actx.Chart && ctx.createChart ? … : nullworkaround. If Chart.js is missing, the cell shows an empty box instead of the error text.daypartConfigre-spreadsdeckChartOptions().scales.y.ticksbecause the shell mergesscales.yshallowly. A deep tick merge in the shell would fix this for all views.fillGraphthrows, the header keeps its legend and notes over the error, andsd-day-greyedstays on.Spec
Verdict: mergeable after S2-1 and C2-1. The P3s are filed as a follow-up issue.
🤖 Generated with Claude Code
Second-pass fixes in
c00716c<data value={raw}>formatted</data>, built fromphrase()parts.withDatais no longer used. The test that locked in the split values now asserts raw values and text, e.g.['5.24', '+5.2%']and['11000.4', '11,000']. A new test checks that Spanish1.234stays one figure.yoyMissingkey is removed. A single entrance missing last year while others have it still shows "—" with its reason. Tests cover both the pure mapping and the rendered view.S2-2 to S2-4 are untouched (filed as an issue).
Checks:
TZ=America/Caracas pytest -q: 417 passed.ruff check,ruff format --checkandcheck_docs.pyare clean.🤖 Generated with Claude Code
- S2-1: formatted figures (changes, counts, the usual reference, YoY cells) render as one <data value={raw}> each instead of going through withData, which split "+5.2%" and "1.234" into digit runs. Copy with figures is built from `phrase()` parts. - C2-1: with no last-year data at all the entrance share shows neither the year-on-year column nor a note (RFC §5.1); a single entrance missing last year still shows "—" with its reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>cb4c8e7da8b500dde28aMaster merged in (
b500dde) after Month (#147) and Week (#148). I resolvedindex.html, keeping all three views' stylesheets and imports, andi18n.js: master'sviews.month/views.weekplus the canonical comparison-reason texts (Month wording, the maintainer's decision), with Day's finalviews.dayblock added. No Day key was lost, and es/en parity holds. The Day tests now expect the canonical texts. The full suite passes on top of master (417), and ruff and check_docs are clean.