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!89
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/statistics-periods-daily"
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 #82 and #84 so the statistics deck can list selectable closed business periods and read daily totals, including the hourly week heatmap data absorbed from #87. PR #80 is blocked on these contracts.
Architectural impact
Adds an authenticated read-only
/api/statistics/router with Pydantic response models.AnalyticsServiceowns period and daily aggregation;OccupancyRepositoryowns counted-camera, audit-quality, and gap queries. No schema migration or application write path is added.Verification
pytest -q: full suite passed after review fixes.python3 scripts/check_docs.py: 33 Markdown files and 68 HTTP operations.Checklist
bucket=hourwithin seven-day ranges, gap-estimated cells.Refs #80, #82, #84, #87.
Review follow-up
Picker labels are now formatted by the client from structured dates and coverage. Period quality uses one range query per period, and reports excluded cycles. Daily rows use their historical trusted multiplier and expose the occupancy cutover. Regression tests cover peak, dwell, audit scores, day/month boundaries, and limits.
WIP: feat(statistics): closed periods and daily series (#82, #84)to feat(statistics): closed periods and daily series (#82, #84)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 layering is clean. SQL stays in
OccupancyRepository, the controller is thin, contracts sit inapp/schemas/statistics.py, and tests cover the 422 and hourly paths. No hard breach of a documented standard.P2: should fix before merge
app/services/analytics_service.py:135-144formats labels withstrftime("%a %d %b %Y").upper()and appendsf" ({covered} of {business} days)". CONTEXT.md:224 says the UI has "full dual-language support (English / Spanish) with dynamic client-side switching". Labels here are always English, and%a/%bdepend on the process locale, so on the Windows prod host output can change with the service account's locale. Return structured fields (start,end,covered_days) and let the client format the label.analytics_service.py:92-103and:172-184repeat the same per-day loop:facility_cycle_bounds_for_date→get_counted_day_flow_async→get_cycle_quality_async. Withgranularity=month&limit=24that is ~730 days × 2 queries, plus every empty period back tofirst_day. Move the shared shape into one range-based "cycle summary" helper, ideally one grouped repository query.P3: minor
analytics_service.py:161declaresbucket: str = "day", while the controller andDailySeriesResponseuseLiteral["day", "hour"]. Add aBucket = Literal[...]alias inschemas/statistics.py, likeGranularity.granularity(analytics_service.py:36-66) and the label cascade at:135-142switches again. A small per-granularity strategy map would keep them together.get_cycle_quality_async:start_epoch < ? AND end_epoch >= ?) and again in Python (analytics_service.py:228-232); they can drift.analytics_service.py:131:start <= active_dayis always true, sincestartbegins at the latest closed period and only moves backwards.statistics_controller.py:34usesstatus_code=422; docs/standards/code-standards.md §3.1 example usesstatus.HTTP_*constants.cfg.get("daily_reset_time", "04:00")duplicated atanalytics_service.py:123and:164(possible Data Clumps:cfg/resettravel together).get_counted_day_flow_asyncreturns an unnamed(int, int, bool)tuple andget_cycle_quality_asyncreturns(bool, int | None, int | None). ANamedTupleeach would make call sites readable.No findings
docs/api/README.md,app/main.py, and the PR description (follows git-and-workflow.md §2 template).Spec
Overall this matches the triage decisions: separate
/api/statistics/router,require_auth, closed periods only, the counted-camera rule,bucket=hourinstead of a new route, nocompare=, and the 7-day and 62-day limits. Quarantined events live in a separate table, so counted-camera queries already exclude them.(a) Missing or partial
PeriodQualityhas no field for excluded cycles, andget_cycle_quality_async(app/db/occupancy_repository.py:1239) never readsis_trustedortrust_status = 'AUTO_EXCLUDED'. The picker cannot mark an excluded cycle.occupancy_definition_cutover_at] for chart annotation." The daily series does not expose it, so a 62-day range crossing the cutover gets no annotation.(b) Scope creep
Nothing significant.
min_cycle_completenessgoes beyond the sketch but follows from "Data Trust and Cycle Completeness for the cycles involved".endis exclusive, as in the sketch.(c) Implemented but looks wrong
app/services/analytics_service.py:170. Closed historical days use today'sactive_exit_multiplierfor peak and dwell. Master's timeseries path uses the cycle's loggedcomputed_exit_multiplier, so the same day can show different peak/dwell on the two surfaces — against ADR 0006's point that past calibration is not reconstructible. Commit1a3f89bfixes it in stacked PR #92. Pull that fix into this PR, or state in the description that #89 is not correct without it.app/schemas/statistics.py:15. The sketch shows"min_data_trust": 0.91, but the code returns an int 0–100. Sound choice, not documented indocs/api/README.md.analytics_service.py:231.item.end_epoch >= hour_startmarks the next hour as gap-estimated when a gap ends exactly on the hour boundary.Open design points
bucket=hourvs its own routerequire_auth, separate router)partial=covered_days < business_days, label "N of M days"iso_weekfieldVALIDATION_ERROR:168)Test coverage (
tests/test_statistics_periods.py)Covered: 401 without login, excluded camera ignored, partial week with label, open-cycle rejection with
error_code, gap marking on hourly cells, 7-day hourly limit.P2, not tested:
data_trust/cycle_completenessvalues (the #84 row contract)granularity=dayandmonth, month boundaries, newest-first ordering,iso_week,gap_estimated_daysstart > endThe PR claims "open-window dwell, peak, holidays, and quality" for #84, but no test checks any of them.
Summary: Standards 9 (2×P2, 7×P3) — worst: server-built English/locale-dependent period labels bypass i18n. Spec 6 (3×P2, 3×P3) — worst: excluded cycles not surfaced in period quality (#82 triage), plus closed days using today's multiplier until
1a3f89blands.🤖 Generated with Claude Code
Code review — pass 2 (two-axis)
Re-reviewed at head
e4e4cff. 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
Pass-1 findings
1f1c688dropslabelfromSelectablePeriod(app/schemas/statistics.py) and removes the strftime cascade;docs/api/README.md:129documents structured fields for client formatting.01348e8: period picker does oneget_counted_cycle_quality_range_asyncread per period (occupancy_repository.py:1238,analytics_service.py:99). The daily-series loop (analytics_service.py:162-247) still runs ~7–9 queries per day and now addsget_calibration_log_for_cycle_async— ~500 queries for a 62-day range.bucket: strBucketaliasschemas/statistics.py:9, used in controller andanalytics_service.py:153.analytics_service.py:37-67) still switch separately.analytics_service.py:134status_code=422literalstatistics_controller.py:35usesstatus.HTTP_422_UNPROCESSABLE_ENTITYdaily_reset_timedefault repeatedanalytics_service.py:126,:156.occupancy_manager.get_daily_reset_time_async()already exists.get_counted_day_flow_async/get_cycle_quality_asyncunchanged; new method returns untypedlist[dict[str, Any]].New findings
get_counted_cycle_quality_range_async(occupancy_repository.py:1268-1269) and the hourly Python check (analytics_service.py:232) now useg.end_epoch > start(the pass-1 hour-boundary fix).get_cycle_quality_async(occupancy_repository.py:~1289), still called by the daily series atanalytics_service.py:169, still usesend_epoch >= ?and lacks thesuperseded_by IS NULLfilter the range query adds. A gap ending exactly on a cycle boundary showsgap_estimated: truein/dailywhile/periodscounts 0gap_estimated_days. Fix: have the daily series use the range query (one call for the whole range also clears the remaining perf P2), then deleteget_cycle_quality_async.get_counted_cycle_quality_range_asyncreturnslist[dict[str, Any]]read by string key (row["has_gap"],row["is_excluded"] or 0). The old method int-cast values; the new path relies on SQLite typing.NamedTuple/TypedDict.cycles: list[tuple[str, float, float]]is an anonymous (date, start, next_start) triple whose order the SQL relies on;params.extend((cycles[0][0], cycles[-1][0]))also relies on the caller passing it sorted.multiplier = float(cfg.get("active_exit_multiplier", 1.0))moved inside the per-day loop (analytics_service.py:172), re-reading the same config each day. Hoist it out as the fallback.app tests;tests/test_statistics_periods.pypassed in a throwaway worktree. DynamicVALUESplaceholders stay well under SQLite's limit (31-day month = 95 params); SQL stays in the repository.Merge readiness (this axis): Not ready. One P2 blocks: gap/audit predicates diverge between
/periodsand/daily. Moving the daily series onto the range query fixes it and closes the remaining pass-1 perf P2. P3s → follow-up issue.Spec
Head
e4e4cff(fix commits1f1c688,01348e8,e4e4cff).Pass-1 findings
excluded_days(app/schemas/statistics.py:16,analytics_service.py:104), but the SQL only matchestrust_status = 'AUTO_EXCLUDED'(occupancy_repository.py:1271), so manually excluded cycles are missed. See N1.occupancy_definition_cutover_aton the daily series (ADR 0006)statistics.py:56,analytics_service.py:259; reads the REAL config column like other surfaces. No test.analytics_service.py:172-181uses the cycle's trustedcomputed_exit_multiplier, matching master's timeseries path (is_trusted+ non-null multiplier), including the fallback for excluded days.docs/api/README.md:129("0–100 scale").analytics_service.py:232uses>.e4e4cffchecks peak + time,is_holiday, trust, completeness,iso_week, exclusiveend, day newest-first order, month bounds, 62-day limit,start > end. Still untested:gap_estimated_days, a true holiday, the cutover field, and the historical-multiplier path (test log hasis_trusted=0, and with only IN events peak doesn't depend on the multiplier). Dwell check is onlyis not None.New findings
app/db/occupancy_repository.py:1271:a.trust_status = 'AUTO_EXCLUDED' AS is_excluded. #82 triage: "Bad-data periods (gap estimates, low Data Trust, excluded cycles) are included with their quality fields so the picker can mark them." An operator's trust toggle setsMANUAL_EXCLUDED(occupancy_repository.py:1719), andANOMALOUS_QUARANTINEalso exists (occupancy_models.py:55); both yieldexcluded_days = 0. Confirmed by switching the test fixture toMANUAL_EXCLUDED: the test fails. Usea.is_trusted = 0, or match every*_EXCLUDED/quarantine status, and add a manual-exclusion test case.g.end_epoch > p.start_epoch(new range query) and hourly cells>, but the daily row'sgap_estimatedstill usesend_epoch >= ?inget_cycle_quality_async. A gap ending exactly at 04:00 flags the daily row but not the picker'sgap_estimated_days. Use one predicate. (Standards axis rates the same divergence P2.)Open design points
bucket=hourinstead of own route,require_auth, separate router, range limitslabel; documented in the description anddocs/api/README.md:129.tests/test_statistics_periods.pypasses at head (throwaway worktree).Merge readiness (this axis): Not ready. N1 (P2) must be fixed. N2, N3 and remaining test gaps → follow-up issue.
Summary: Blocking (P2): Standards — gap/audit predicates diverge between
/periods(range query) and/daily(get_cycle_quality_async); Spec —excluded_daysonly countsAUTO_EXCLUDED, missingMANUAL_EXCLUDED/quarantine. Rest P3. Worst per axis: Standards → predicate drift; Spec → manual exclusions invisible to the picker.🤖 Generated with Claude Code