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!92
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/statistics-summary-entrances"
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?
Summary
Implements #83 and #86: a selected-period KPI strip with previous and last-year comparisons, and counted-camera entrance shares with explicit new-camera and estimated-attribution states. This PR is stacked on #90, which is stacked on #89.
Architectural impact
Adds two authenticated, read-only
/api/statistics/routes and typed response contracts. Aggregation remains inAnalyticsService; counted per-camera SQL remains inOccupancyRepository. No schema migration or write path.Verification
pytest -q: full suite passed after review fixes.Checklist
master.Depends on #90 and #89. Refs #80, #83, #86.
Review follow-up
A partial current or reference week/month returns
reason: PARTIAL_PERIODwith no percentage change. Last-year alignment and comparison-null semantics are documented in the API reference.WIP: feat(statistics): period KPIs and entrance comparisons (#83, #86)to feat(statistics): period KPIs and entrance comparisons (#83, #86)Code review — pass 1 (two-axis)
Reviewed the PR's own diff (
git diff <base>...<head>); Standards and Spec ran as independent passes and are reported separately, not reranked. Severity: P1 must fix · P2 fix before merge · P3 minor. Per review policy, pass 1 findings are all to be fixed on the branch.Standards
The diff breaks no documented standard. Layering holds: SQL stays in
OccupancyRepository, controllers stay thin, and errors useX-Error-Codepercode-standards.md§3. Both new routes have tests.Bugs / correctness
analytics_service.py:210-219, 175-183. The current visitors total counts only covered days, butchange_percentcompares it with the full reference period. A month with missing cycles shows a false drop. The partial-period denominator is used only fordaily_average. Scale the comparison to covered days or suppress it whenquality.covered_daysis less than the period length.analytics_service.py:346-351.is_newchecks onlyfirst_event_epoch >= current_epoch. A camera whose first event falls after the period is shown asNEWwith 0 visitors. Also check the first event is before the period end.analytics_service.py:481-491vsget_dwell_dayparts_async(~589). Daily-series dwell and peak now use each cycle's trusted multiplier;/statistics/dwell/daypartsstill usesactive_exit_multiplier, so the same day can show two dwell values.Documented-standard notes
WIP:prefix (git-and-workflow.md§2.1, AGENTS.md §4).schemas/statistics.py:176. New contract fieldEntranceStatistics.zone_name; CONTEXT.md lists "Zone" under Avoid for Camera Group. Follows the existing column, so low priority.Baseline smells (judgement calls)
change_percent=round(100 * (x / ref - 1), 1) if ref else None, reason="ZERO_REFERENCE_VISITORS" if ref == 0 else Noneappears four times (analytics_service.py:181-182, 268-269, 342-343, 364-366). Extract a singleVisitorComparisonfactory.:481-491repeats the logic at:773-780.ValueError→422 / LookupError→404try/except (statistics_controller.py:40-47, 58-65).schemas/statistics.py:106.VisitorComparison.reason: strholds ~seven fixed codes (NEW_CAMERA,NO_DATA_LAST_YEAR, …), whileattributionandstatusin the same file useLiteral. MakereasonaLiteral.analytics_service.py:225-228, 250-300.get_statistics_summary_async(~110 lines) checksgranularityin two!= "day"conditions then again in anif/elif/else. One helper per granularity would shorten it.schemas/statistics.py:148-149.busiest_day(week) andbest_day(month) hold the same value from the same variable. Use one name.analytics_service.py:334.old,prior,topare unclear;previous_row/previous_comparisonwould read better.Spec
Nothing blocks merge. Both routes, every field the mockup lists,
require_auth, the closed-period check, counted-camera filtering and the triage decisions are in place.(a) Missing or partial
P2 · No tests for several claimed behaviours (
tests/test_statistics_summary.py). No test for:busiest_day, week weekend share);NO_DATA_PREVIOUS_PERIODreason;NO_REFERENCE_DATAstatus;The spec requires "vs same period last year (only when last year has data…)", but only the omitted branch is tested.
P3 · Spec: "Each comparison returns
nullwith a reason". The code returns aVisitorComparisonobject withreference_visitors: nullplusreason, not a literalnull. Fine if the deck expects the object; document it.(b) Scope creep
analytics_service.py:476-490:/timeseries/dailypeaks now use each cycle's trusted multiplier. Changes #84's route (not asked for here, though sound — and #89's spec review asks for exactly this).get_dwell_dayparts_async(:589) still uses the active multiplier, so dwell for the same day can differ between the summary and the dayparts route.:350: adds a third status,NO_REFERENCE_DATA, beyond theNEWrule in the triage. Reasonable, but new contract surface.(c) Implemented but questionable
:170,:216). Triage: "Partial periods: figures cover only days with data". The current total covers only its covered days, but the reference sum is the raw total of the reference period. A 2-of-30-day month compared with a full month shows roughly −90%. Normalise by covered days or return the reference period'scovered_days.occupancy_repository.py:1259,:331). Triage: "a camera that disappeared is not listed". An active camera with zero events this period is still listed asEXISTINGat −100%. If "disappeared" means "no events", this contradicts the rule. Clarify.:326).has_year_datalooks only at current cameras. If last year's data came only from cameras since removed,last_yearis omitted even though the summary route shows it.tests/test_statistics_summary.py:139: the unaligned-week check is skipped whenever the test runs on a Monday.Open design points
NEW; inactive cameras droppedattribution: ESTIMATED:94). UndocumentedINSUFFICIENT_USUAL_WEEKDAYSwithsource_datesSummary: Standards 12 (1×P2, 11×P3) — worst: comparisons pit covered-days-only totals against full reference periods. Spec 8 (2×P2, 6×P3) — worst: same partial-period comparison flaw, plus missing tests for last-year-present and week paths.
🤖 Generated with Claude Code
Code review — pass 2 (two-axis)
Re-reviewed at head
028c491. Each axis verified every pass-1 finding against the code (not the commit messages), then reviewed the fix commits for new problems. Verdicts: ✅ fixed · ◐ partial · ❌ not addressed · ↺ regressed · ⊘ declined with documented reason. Per review policy, from pass 2 on P1/P2 block merge; P3s go to one follow-up issue for this PR.Standards
Head
028c491. ruff check/format clean; statistics tests (10) pass and full suite passes (295 passed, 1 skipped) in a throwaway worktree.Pass-1 findings
fbbec92:statistics_visitor_comparison_asyncreturnsPARTIAL_PERIODwith nochange_percentwhen either side is partial (analytics_service.py:180-192); entrances likewise (:374-382,:401-409). Summary path tested (test_statistics_summary.py:198); documented indocs/api/README.md.is_newignores period endfbbec92:385-388:current_epoch <= first_event < end_epochget_dwell_dayparts_asyncstill usesactive_exit_multiplier(:631); daily series uses the cycle's trusted multiplier (:510-524). Undocumented.WIP:title vs "draft" wordingzone_namevs CONTEXT.md "Zone" avoid-listEntranceStatistics.zone_nameunchanged.change_percent/reasonternary is now a nested three-way ternary with a partial-period check, repeated at:370-382and:397-409; the helper at:162-198holds the same rule a third time.ValueError→422 / LookupError→404.reason: strprimitivea7aa664addsComparisonReasonLiteral;StatisticsSummary.previous_month_daily_average_reasonis stillstr | Noneholding the same code (NO_DATA_PREVIOUS_PERIOD).granularityget_statistics_summary_asyncstill branches at:180,:239-243,:262-328.busiest_day/best_daynaming:316-318old/prior/topnames:363-392,:305New findings
analytics_service.py:510-524). This PR replaces #89's1f1c688version with its own, differing in: calibration lookup windowbounds.endvs #89'sbounds.next_start(this one arguably more correct), andcomputed_exit_multiplier is not Nonevs #89's truthiness test.get_calibration_audit(:836-839) still uses truthiness withnext_start, so a trusted multiplier of0.0is accepted by the daily series but falls back to default in the audit. Land this version in #89 and extract one_trusted_cycle_multiplier(day, bounds, default)helper, or state in the description that #92 supersedes #89's version.:180-192,:370-382,:397-409). Onecompare_visitors(current, reference, partial)factory clears this and the pass-1 duplication finding.:182,:320):get_statistics_period_quality_asyncruns inside the comparison helper and again forprevious_month_daily_average. Return the reference quality from the helper.year_qualityis Optional (:356-360, read at:402,:405) as.partialwithout narrowing. Safe at runtime (guarded byhas_year_data), but pyright/mypy would flag it.PARTIAL_PERIODand the newis_newupper bound; the onlyPARTIAL_PERIODassertion is on the summary route.Merge readiness (this axis): Ready. The only P2 is fixed; nothing new at P1/P2. Remaining P3s (including multiplier-selection drift) → follow-up issue.
Spec
Head
028c491; own commitsfbbec92,3560dcf,a7aa664,028c491vs pass-1 head1a3f89b. Throwaway worktree:pytest tests/test_statistics_{summary,periods,baselines}.py13 passed.Pass-1 findings
busiest_day, weekend share); last year present on both routes; ISO-week and Feb-29 alignment;NO_DATA_PREVIOUS_PERIOD; the 404. Still untested:NO_REFERENCE_DATA, and period dwell weighted byopen_window_visitors(analytics_service.py:215-229).null028c491,docs/api/README.md:132/timeseries/dailyscope creep; dayparts dwell uses a different multiplierget_dwell_dayparts_asyncstill usesactive_exit_multiplier(analytics_service.py:631); same day can show two dwell values.NO_REFERENCE_DATAstatusfbbec92,docs/api/README.md:132fbbec92::182-192summary,:375-382/:402-409entrances. Month test checksPARTIAL_PERIOD(test_statistics_summary.py:197-198).fbbec92:363(if not row["event_count"]: continue)has_year_datachecks only current camerasfbbec92:342-348: whole-period counted flow, matching the summary route.fbbec92,test_statistics_summary.py:137-141docs/api/README.md:132+ unit testtest_statistics_summary.py:26New findings
:186,:376,:403;partial = covered < business_daysat:467). Spec: "Partial periods: figures cover only days with data; the response carriescovered_days/business_days." Suppressing is honest — the reference sum is raw and there's no referencecovered_days, so the deck couldn't normalise anyway. But a scheduled-closed holiday (occupancy_service.py:235, "Cerrado") with no counted events makes the whole week/month partial, so every month containing a closure loses its previous-period and last-year comparison, and so does the month after it. This rule was agent-decided, not user-decided: file aneeds-triagefollow-up (exclude scheduled-closed days frombusiness_days, or compare daily averages).PARTIAL_PERIODon the entrances route; skipping zero-event cameras (:363); the new< end_epochbound onis_new(:387).test_statistics_summary.py:137): 422 for either reason (not Monday, or not closed), so it doesn't isolate alignment. Use a past Tuesday.Nothing new at P1/P2. Spec coverage of #83 and #86 remains complete.
Merge readiness (this axis): Ready — no P1/P2. File the P3s plus the dwell-weighting and
NO_REFERENCE_DATAtest gaps as a follow-up issue.Summary: Ready on both axes (no P1/P2). Worst per axis: Standards → trusted-multiplier copies drifted between #89 and #92 (P3, resolve when #89 lands); Spec →
PARTIAL_PERIODsuppression triggers on scheduled closures (P3, agent-made decision → needs-triage follow-up).🤖 Generated with Claude Code
gabogg referenced this pull request2026-09-25 23:07:50 +00:00