refactor(statistics): period and Closed Day follow-ups #168
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!168
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/statistics-period-followups"
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
This PR addresses period KPI, comparison coverage and Closed Day follow-ups from #99, #111, #120, and incorporates follow-ups #200 and #201, along with review pass 1 and pass 2 findings:
Architectural Impact
Changes are confined to statistics/occupancy schemas, services, API docs, and UI. No breaking database migrations required (legacy column zone_name remains in DB schema with dropping tracked in #209). Preserves ADR 0006 occupancy windows and schedule calendar semantics.
Verification / Test Evidence
Checklist
Closes #99
Closes #111
Closes #120
Closes #200
Closes #201
WIP: refactor(statistics): period and Closed Day follow-upsto refactor(statistics): period and Closed Day follow-upsImplementation & Verification Summary
All follow-up items across #99, #111, and #120 have been implemented and verified in commit
13ec8c9:Issue #99 (Period KPIs & Entrances):
_trusted_cycle_log_async&_trusted_cycle_multiplier_async), shared comparison factory (compare_visitors), typedComparisonReasonforprevious_month_daily_average_reason, single query forprevious_quality, and unified controller error handling (domain_errors).zone_nameonEntranceStatisticsas camera portal grouping location distinct from business Zone.NO_REFERENCE_DATA, skipping zero-event cameras,< end_epochbounds onis_new, and weighted period dwell byopen_window_visitors.Issue #111 (Closed Days):
ScheduleCalendarwith typeddatekeys, requiredcalendarparameter on period quality, and unquoted return annotation.tests/test_statistics_closed_days.pywith named constants.INSERTs in tests withoccupancy_repo.record_calibration_log_async.usual_weekdaycarriessource_datesand omitsreference_business_days, while period comparisons carryreference_business_daysand omitsource_dates.Issue #120 (Comparison Coverage Threshold):
PeriodVisitors.per_day()andcompare_visitors.docs/api/README.mdclarifying when comparisonbasisis absent.Verification:
ruff check .: passed.ruff format --check .: passed.python3 scripts/check_docs.py: passed (39 files, 80 HTTP operations).pytest: 463 passed (100% green).Code review, pass 1 (
origin/master...724c0ff, spec #99, #111, #120)Result: no P1s, 2 P2s (Standards) and 12 P3s. This PR addresses follow-up issues, so it takes the three-pass path: every finding on passes 1 and 2 gets fixed. Standards P2-1 and Spec P3-A are the same finding.
Despite the "refactor" title, the only app change is a
Field(description=...)onEntranceStatistics.zone_name. Everything else is tests, docs/api and the mapping doc, so no runtime behaviour changes. Most checklist items were already done on master and are only "verified" here.Verification:
tests/test_statistics_{summary,periods,closed_days,comparison_coverage}.py: 32 passed.Standards
P2
app/schemas/statistics.py:243-245,docs/api/README.md:148)._Avoid_: Zone, portal group. "Business Zone" isn't defined anywhere.docs/audit/statistics-period-followups-pr-plan.md).tests/test_statistics_periods.py::test_daily_series_uses_the_past_cycle_computed_exit_multiplier, which doesn't exist.< end_epochis "verified in test_entrances_skips_zero_event_cameras…", but that test has no event at or afterend_epoch(see Spec P3-B).file:linereferences intoanalytics_service.pywill go stale; cite symbol names.P3
3. The new audit doc isn't linked from
docs/README.md, unlike the otheraudit/snapshots (README:55-58). It also calls itself a "draft PR plan".4. The new tests bypass the HTTP routes the issues describe (
test_period_dwell_weighted_by_open_window_visitors,test_entrances_skips_zero_event_cameras_and_marks_no_reference_data). They callanalytics_service.*directly; AGENTS.md §3 prefers ASGI clients.5. Duplicated Code (
tests/test_statistics_summary.py:278-420). Theupsert_camera_async({...})/record_event_async({...})literals appear about 9 times. Extractrecord_passage(camera, day, direction, count, time)next to the existingrecord_ingress.6. Weak assertions (~472-480).
comp.get("source_dates") is Noneandusual.get("reference_business_days") is Nonealso pass when the key is present with a null value. The doc says these keys are omitted, so assert"source_dates" not in comp.Spec
P3
zone_namewording (same as Standards P2-1). #99 says: "EntranceStatistics.zone_name conflicts with CONTEXT.md's 'Zone' avoid-list." The field is still a zone, and there is no rename or follow-up issue for it.< end_epochbound onis_newis still untested. #99 says: "Untested: … the < end_epoch bound on is_new."first_event_epochis the camera's earliest event ever (occupancy_repository.py:1654), so the bound can't be reached through the route.busiest_day/best_daywere kept without a tracked deferral. #99 says: "busiest_day/best_day hold the same value." They were preserved for frontend compatibility, but no follow-up issue was filed.analytics_service.py:565-567, 592-662). #99 says: "Repeated switches on granularity inside get_statistics_summary_async." The doc says "simplified", but this PR doesn't change that code.Checklist:
compare_visitors, the single previous-month query,ComparisonReason,year_quality, the controllerdomain_errors,previous_row/year_row, and the test gaps (NO_REFERENCE_DATA, weighted dwell, PARTIAL_PERIOD, zero-event cameras), plus the unaligned week;< end_epoch;busiest_day/best_day;zone_name;Scope creep: none.
Summary
zone_namewording.zone_namestill conflicts with the glossary.🤖 Generated with Claude Code
Pass 1 fixes (
c08c9e0)All P1, P2 and P3 findings from review pass 1 have been addressed in commit
c08c9e0:Standards
EntranceStatistics.zone_namedescription inapp/schemas/statistics.pyand API documentation indocs/api/README.mdto"Legacy name: the camera's Camera Group; not a domain Zone", avoiding "portal group" and "business Zone". Full field deprecation/aliasing is tracked in follow-up issue #200.docs/audit/statistics-period-followups-pr-plan.md, corrected exit multiplier test reference totests/test_statistics_periods.py::test_historical_cycle_uses_its_trusted_multiplier_for_peak, replaced brittlefile:linecitations with symbol names, and updated the< end_epochcitation.docs/audit/statistics-period-followups-pr-plan.mdindocs/README.mdunder## Audit snapshotsand changed title to"Statistics period, comparison and Closed Day follow-ups verification".test_period_dwell_weighted_by_open_window_visitorsandtest_entrances_skips_zero_event_cameras_and_marks_no_reference_datato exercise HTTP endpoints (/api/statistics/timeseries/daily,/api/statistics/periods/week/{start}/summary,/api/statistics/periods/week/{start}/entrances) via ASGIAsyncClient.record_passage(camera, day, direction, count, time)andregister_camerahelpers intests/test_statistics_summary.pynext torecord_ingress, eliminating duplicated literals.model_serializertoVisitorComparisonomittingsource_datesandreference_business_dayswhenNone. Strengthened assertions intest_usual_weekday_and_period_comparisons_contractto assert"source_dates" not in compand"reference_business_days" not in usual.Spec
refactor(statistics): deprecate EntranceStatistics.zone_name in favor of camera_group).analytics_service.get_statistics_entrances_asyncbecauserow["event_count"] > 0guarantees at least one event in[current_epoch, end_epoch), makingfirst_event_epoch < end_epochan invariant. Documented invariant in code comment and mapping doc.refactor(statistics): consolidate busiest_day and best_day on StatisticsSummary).get_statistics_summary_asyncto cleanly separate single-day initialization from multi-day calculation (daily_average,weekend_share_percent), eliminating scattered pre-switches.test_summary_rejects_open_or_unaligned_period.Verification
ruff check .: clean.ruff format --check .: clean.python3 scripts/check_docs.py: clean (39 Markdown files, 80 HTTP operations).pytest: 522 passed (100% green).Ready for second pass review.
Code review, pass 2 (
origin/master...c08c9e0, spec #99, #111, #120)Result: no P1s, 2 P2s (1 Standards, 1 Spec) and 9 P3s. This PR addresses follow-up issues, so it takes the three-pass path: fix every finding below, push, and request a third pass.
Pass 1 found no runtime change. This fix commit adds one: the
VisitorComparisonwrap serializer now dropssource_dates/reference_business_dayswhen they are null. That serializer causes Spec P2-1.Verification:
tests/test_statistics_*files: 32 passed.app.openapi()compared at724c0ffandc08c9e0.Added scope: #200 and #201 are fixed in this PR (maintainer triage, 2026-10-02)
The two deferrals from pass 1 are now triaged, specified and folded into this PR. Implement each issue's Agent Brief (posted on the issue) as part of this pass's fixes. This PR then closes #200 and #201: add
Closes #200, #201to the description.zone_name→camera_group.zone_nameservescamera_group: str | None, the camera's linked Camera Group name, ornullwhen it has no group. No alias.zone_nameand drops the keyword zone guess.counting_cameras.zone_namecolumn stays, unused and marked legacy; dropping it is #209 (low priority, blocked by #200).zone_namewording): the field is replaced, so don't reword it.busiest_day.busiest_day;best_dayis removed with no alias._Avoid_: best day.camera_groupandbusiest_dayfields must appear in the OpenAPI schema. That is part of fixing Spec P2-1.Standards
Pass-1 fixes:
zone_nameterminology), 3 (README link and title), 4 (tests now go through the ASGI client) and 6 (omission assertions).is_newanddaily_average/weekend_sharerewrites are equivalent.P2
docs/audit/statistics-period-followups-pr-plan.md, #120 bullet:test_comparison_coverage.py::test_period_comparison_with_closed_days_requires_exact_coverage_threshold. The real test istest_closed_days_count_towards_coverage_and_the_threshold_is_inclusive(:156). Every other citation exists.P3
2. The new omission rule isn't documented.
app/schemas/statistics.py:173-180changes the wire contract from null to absent.docs/api/README.mddocuments the other omission rules (last_yearomitted,basisabsent), but not this one.day_view.js:231).zone_namewording overclaims (statistics.py:252,docs/api/README.md:148). The field holds the Camera Group name only when HikCentral returns one. Otherwise it is guessed from the camera name (occupancy_service.py:586,652-659), with the DB default'General'(database.py:381). Say "usually the Camera Group name".tests/test_statistics_summary.py:99-292). There are still 5 inlineupsert_camera_asyncliterals and 5 inline logins next to the newregister_camera/admin_headershelpers.Spec
P2
VisitorComparisonserializer wipes its OpenAPI schema (app/schemas/statistics.py:173-180).@model_serializer(mode="wrap") -> dict[str, Any]turns the schema into{"type": "object", "additionalProperties": true}. At724c0ffthe full property list was there.previous,last_year,same_weekday_last_weekorusual_weekday, on the summary or entrances routes.StatisticsSummary/EntranceStatisticswrap serializers already collapse those schemas the same way.Field(exclude_if=lambda v: v is None)(Pydantic 2.13.5) orresponse_model_exclude_noneon the route, and add a test asserting the properties appear inapp.openapi().P3
analytics_service.py:565-573, 596-662). #99 asks for "Repeated switches on granularity inside get_statistics_summary_async." The!= "day"check became anifblock, but the separateif == "day" / elif week / elsechain is unchanged, so there are still two switches. The doc, the PR checklist and the fix reply say "unified". Merge the two, or reword the claim and track the rest.zone_namewording overclaims (same as Standards P3-3).ready-for-agent, and are fixed in this PR (see Added scope).Fix claims (c3424):
< end_epoch): resolved by removing the bound; the invariant holds and is documented.Checklist:
< end_epoch: resolved.Summary
🤖 Generated with Claude Code
Pass 2 fixes (
998d22f)All findings from review pass 2 (
r23) and follow-ups #200 and #201 have been resolved:1. Spec P2-1 & Standards P3-5: Preserve complete OpenAPI schemas
@model_serializer(mode="wrap")fromVisitorComparison,StatisticsSummary, andEntranceStatisticswhich previously collapsed schema properties to{"type": "object", "additionalProperties": true}.Field(default=None, exclude_if=lambda v: v is None)for conditional wire omission without altering schema generation.test_openapi_schema_contains_comparison_summary_and_entrance_fieldsintests/test_statistics_summary.pyasserting property presence across all three models inapp.openapi().2. Standards P2-1: Audit plan test citation
docs/audit/statistics-period-followups-pr-plan.mdtotests/test_statistics_comparison_coverage.py::test_closed_days_count_towards_coverage_and_the_threshold_is_inclusive.3. Standards P3-2: Document omission rules
source_dates(only onusual_weekdaybaselines) andreference_business_days(only on period comparisons) indocs/api/README.md.4. Standards P3-4: Test code deduplication
tests/test_statistics_summary.py: eliminated remaining 5 inlineupsert_camera_asynccalls and 5 inline logins by usingregister_camera(..., resource_group_code=...)andadmin_headers(client).5. Spec P3-A: Unified granularity branching
get_statistics_summary_asyncinapp/services/analytics_service.pyinto a singleif granularity == "day": ... else: ...block.6. Spec P3-B & Issues #200, #201: Scope alignment & delivery
zone_name->camera_group, Closes #200):zone_namewithcamera_group: str | Noneacross response models (CountingCameraItem,OccupancyRankingItem,CameraDiagnosticItem,EntranceStatistics).app/db/occupancy_repository.pytoLEFT JOIN counting_camera_groups g ON c.resource_group_code = g.resource_group_codeselectingg.resource_group_name AS camera_group.infer_camera_direction_and_zone(nowinfer_camera_direction).counting_cameras.zone_namecolumn as legacy (drop tracked in #209).cameraGroupLabel,noCameraGroup) ini18n.js, updatedapp.jsandcommand_deck_adapter.js.best_day->busiest_day, Closes #201):best_dayintobusiest_day: NamedDay | Nonecovering week and month views.bestDay->busiestDay, labeled "BUSIEST DAY" / "DÍA DE MÁS AFLUENCIA" inmonth_view.jsandi18n.js).CONTEXT.mdwith_Avoid_: best day.Verification
rtk ruff check .: passed (0 issues).rtk ruff format --check .: passed (150 files formatted).python3 scripts/check_docs.py: passed (39 Markdown files, 80 HTTP operations).pytest: 525 passed (100% green).node --test tests/frontend/*.test.js: 242 passed (100% green).Ready for third pass review.
Code review, pass 3 (
origin/master...998d22f, spec #99, #111, #120, #200, #201)Result: no P1s, no P2s and 15 P3s (12 distinct once the two axes' overlaps are merged). This is the third pass of a follow-up PR, so the P3s are deferred to the linked follow-up issue #220 and the PR merges.
docs/README.md(an index line next to #207's edit).Verification:
node --test tests/frontend/*.test.js: 242 passed.app.openapi()compared with master (scratchpad/oa168.py,probe3.py):VisitorComparison,StatisticsSummary,EntranceStatistics,CountingCameraItemandOccupancyRankingItemlist all their properties;additionalPropertiesis gone;camera_groupandbusiest_dayare present, andzone_nameandbest_dayare absent.Standards
Pass-2 fixes:
docs/api/README.md.exclude_ifdrops null keys on the wire without wiping the schema, andtest_openapi_schema_contains_comparison_summary_and_entrance_fieldscovers it.counting_camera_groups, so an ungrouped camera getsnull.P3 (in #220):
best_dayuses an avoided term.tests/test_occupancy.py.zoneLabelkey, and the'General'fallback in the command deck.exclude_iflambda ×14, and nullable fields marked in OpenAPI although they are omitted on the wire.docs/api/README.md.Spec
Fix claims (c3563): all six verified.
if day / elsewith a nested month branch.#200 acceptance:
#201 acceptance: met, except that the CONTEXT.md text paraphrases the brief.
#99, #111 and #120: done.
Closes #99, #111, #120, #200, #201is accurate, and the #209 note is right.P3 (in #220):
zoneLabelkey.'General'intozone_name.'General'fallback.is_excludedandresource_group_codeare now filled on camera responses, which isn't described.Summary
zoneLabelkey and the'General'fallback.Resolve the README conflict, then merge.
🤖 Generated with Claude Code