feat(statistics): data-quality marker inputs for the deck #138

Merged
gabogg merged 2 commits from feat/deck-marker-inputs into master 2026-09-26 21:20:01 +00:00
Owner

Closes #129

Stacked on #137 (#128, the ranked cycle verdict), which defines "excluded" and "unverified". The base is fix/cycle-verdict-ranking, so the diff shows only #129. Merge #137 first; Forgejo retargets this PR to master when that branch is deleted.

Summary

These are the route inputs the statistics deck's data-quality marker needs (RFC §4.1, decided 2026-09-26):

  • Records skip unreliable days. peak, busiest_day (week), best_day (month) and busiest_hour (day) never come from an excluded day, and are omitted when every covered day is excluded. Totals and averages still include marked days.
  • Per-bucket gap flag. Each HourlyFlowBucket of the presenter hourly route carries gap_estimated, so the Day view can hatch exactly the gap buckets at 15, 30 or 60 minutes.
  • Tier counts on period quality. Every business day that is not a Closed Day is counted once, in its worst state: excluded, then missing, then unverified, then estimated. That gives missing_days, unverified_days, and the tier totals unreliable_days and estimate_days, which is what the top-bar badge shows.
  • Gap intervals on daily rows. DailyStatistics.gaps lists each estimated ingestion gap as {start_epoch, end_epoch}, clipped to the cycle, for the marker's detail panel ("counter gap 14:00–15:10").

Architectural impact

  • app/services/analytics_service.py:
    • day_quality_state() is the one rule behind the tier counts, with ESTIMATE_STATES and UNRELIABLE_STATES;
    • _gaps_within() is the one gap-overlap rule, shared by the daily rows, their hours and the intraday buckets. It replaces the inline any(...).
    • The daily series now reads ingestion anomalies once per range instead of once per day for bucket=hour. The hourly series reads them once per cycle.
  • Contract changes (app/schemas/statistics.py):
    • new GapInterval;
    • DailyStatistics.gaps (required, empty when there are none);
    • HourlyFlowBucket.gap_estimated;
    • PeriodQuality gains missing_days, unverified_days, estimate_days and unreliable_days, which SelectablePeriod inherits, so the picker can mark periods;
    • StatisticsSummary.peak becomes optional and is omitted like the other unavailable sections. An entirely closed period still returns peak with zero people inside.
  • gap_estimated_days and excluded_days keep counting the raw flags, so existing clients see no change in meaning.
  • The admin /api/analytics/timeseries/hourly gets the extra bucket key too; it is additive.
  • API docs updated. No migration.

Beyond the issue's literal list: besides unverified_days and missing_days, I added the two tier totals, because the badge shows exactly those and the client can't derive them from the raw flag counts (a day can be both gap-estimated and excluded). busiest_hour follows the records rule too.

Verification

New tests/test_statistics_marker_inputs.py (8 tests, real in-memory SQLite, no mocks); all failed before the change:

  • in the week and month summaries, records skip an excluded day that has the higher visitors and peak, while the total still includes it;
  • a single excluded day omits peak and busiest_hour;
  • a gap from 14:05 to 14:50 flags exactly the 14:00, 14:15, 14:30 and 14:45 buckets at 15 minutes, and only 14:00 at 60;
  • in a week mixing OK, estimated, unverified, excluded, excluded-with-gap and missing days, each day is counted once in its worst tier, and the raw counts are unchanged;
  • daily rows list their gaps; a gap across the reset is clipped to each day.

pytest: 402 passed, 1 skipped (on top of #137). Frontend: 83/83. Ruff, pre-commit and scripts/check_docs.py passed.

Conflict note: PR #132 edits the same summary and period-quality code. Whichever lands later rebases.

Checklist

  • Records skip unreliable days; omitted when all are unreliable.
  • HourlyFlowBucket.gap_estimated.
  • Tier counts on PeriodQuality (worst state, counted once).
  • DailyStatistics.gaps.
  • API docs.

🤖 Generated with Claude Code

Closes #129 **Stacked on #137** (#128, the ranked cycle verdict), which defines "excluded" and "unverified". The base is `fix/cycle-verdict-ranking`, so the diff shows only #129. Merge #137 first; Forgejo retargets this PR to `master` when that branch is deleted. ## Summary These are the route inputs the statistics deck's data-quality marker needs (RFC §4.1, decided 2026-09-26): - **Records skip unreliable days.** `peak`, `busiest_day` (week), `best_day` (month) and `busiest_hour` (day) never come from an excluded day, and are omitted when every covered day is excluded. Totals and averages still include marked days. - **Per-bucket gap flag.** Each `HourlyFlowBucket` of the presenter hourly route carries `gap_estimated`, so the Day view can hatch exactly the gap buckets at 15, 30 or 60 minutes. - **Tier counts on period quality.** Every business day that is not a Closed Day is counted once, in its worst state: excluded, then missing, then unverified, then estimated. That gives `missing_days`, `unverified_days`, and the tier totals `unreliable_days` and `estimate_days`, which is what the top-bar badge shows. - **Gap intervals on daily rows.** `DailyStatistics.gaps` lists each estimated ingestion gap as `{start_epoch, end_epoch}`, clipped to the cycle, for the marker's detail panel ("counter gap 14:00–15:10"). ## Architectural impact - `app/services/analytics_service.py`: - `day_quality_state()` is the one rule behind the tier counts, with `ESTIMATE_STATES` and `UNRELIABLE_STATES`; - `_gaps_within()` is the one gap-overlap rule, shared by the daily rows, their hours and the intraday buckets. It replaces the inline `any(...)`. - The daily series now reads ingestion anomalies once per range instead of once per day for `bucket=hour`. The hourly series reads them once per cycle. - **Contract changes** (`app/schemas/statistics.py`): - new `GapInterval`; - `DailyStatistics.gaps` (required, empty when there are none); - `HourlyFlowBucket.gap_estimated`; - `PeriodQuality` gains `missing_days`, `unverified_days`, `estimate_days` and `unreliable_days`, which `SelectablePeriod` inherits, so the picker can mark periods; - `StatisticsSummary.peak` becomes optional and is omitted like the other unavailable sections. An entirely closed period still returns `peak` with zero people inside. - `gap_estimated_days` and `excluded_days` keep counting the raw flags, so existing clients see no change in meaning. - The admin `/api/analytics/timeseries/hourly` gets the extra bucket key too; it is additive. - API docs updated. No migration. **Beyond the issue's literal list:** besides `unverified_days` and `missing_days`, I added the two tier totals, because the badge shows exactly those and the client can't derive them from the raw flag counts (a day can be both gap-estimated and excluded). `busiest_hour` follows the records rule too. ## Verification New `tests/test_statistics_marker_inputs.py` (8 tests, real in-memory SQLite, no mocks); all failed before the change: - in the week and month summaries, records skip an excluded day that has the higher visitors and peak, while the total still includes it; - a single excluded day omits `peak` and `busiest_hour`; - a gap from 14:05 to 14:50 flags exactly the 14:00, 14:15, 14:30 and 14:45 buckets at 15 minutes, and only 14:00 at 60; - in a week mixing OK, estimated, unverified, excluded, excluded-with-gap and missing days, each day is counted once in its worst tier, and the raw counts are unchanged; - daily rows list their gaps; a gap across the reset is clipped to each day. `pytest`: 402 passed, 1 skipped (on top of #137). Frontend: 83/83. Ruff, pre-commit and `scripts/check_docs.py` passed. **Conflict note:** PR #132 edits the same summary and period-quality code. Whichever lands later rebases. ## Checklist - [x] Records skip unreliable days; omitted when all are unreliable. - [x] `HourlyFlowBucket.gap_estimated`. - [x] Tier counts on `PeriodQuality` (worst state, counted once). - [x] `DailyStatistics.gaps`. - [x] API docs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Records (peak, busiest or best day, busiest hour) now skip excluded days,
and are omitted when every covered day is excluded; totals and averages
still include marked days. Presenter hourly buckets carry their own
gap_estimated flag, daily rows list their estimated gaps clipped to the
cycle, and period quality counts each business day once in its worst
state: missing_days, unverified_days, and the tier totals estimate_days
and unreliable_days. One day_quality_state rule backs the counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Standards

(a) Documented Standards Violations

None. The diff is fully compliant with repository standards:

  • Architecture & Boundaries (AGENTS.md): SQL queries remain encapsulated in repository methods; anomaly fetching is hoisted cleanly in analytics_service.py.
  • Python & Typing (docs/standards/code-standards.md): Python 3.11+ syntax, explicit type annotations, Pydantic v2 models in app/schemas/statistics.py.
  • Domain Vocabulary (CONTEXT.md): Follows the data-quality marker terminology (missing_days, unverified_days, estimate_days, unreliable_days).
  • Testing & Verification (docs/standards/code-standards.md): Offline, deterministic tests in tests/test_statistics_marker_inputs.py; test suite is 100% green.
  • Git Protocol (docs/standards/git-and-workflow.md): Branch name feat/deck-marker-inputs and conventional commit adhere to guidelines.

(b) Baseline Smells (Judgement Calls)

  1. Data Clumps — app/services/analytics_service.py
    • day_quality_state(*, has_data: bool, excluded: bool, trusted: bool, gap_estimated: bool) passes 4 booleans together. Unpacks dictionary fields at a single call site in get_statistics_period_quality_async; grouping into a small helper type would be cleaner.
  2. Feature Envy — app/services/analytics_service.py
    • _gaps_within queries multiple properties of IngestionAnomaly (kind, start_epoch, end_epoch) to perform interval clipping. Could be a method on IngestionAnomaly (e.g. anomaly.clip_to(start, end)), though defensible as a module-level translation helper to GapInterval.

Spec

(a) Missing or partial requirements

  1. Gap intervals schema on non-gap days
    • Spec: "Expose each estimated gap's start and end per daily row (e.g. gaps: [{start_epoch, end_epoch}], only when gap_estimated)"
    • Finding: In app/schemas/statistics.py, DailyStatistics.gaps is typed as list[GapInterval] and defaults/returns empty list [] on days without gaps rather than being omitted/null.

(b) Behaviour in the diff not asked for (scope creep)

  1. Tier roll-up counts added to PeriodQuality
    • Spec: "Add counts so the top-bar badge can show both tiers without fetching the daily series: unverified_days... missing_days..."
    • Finding: Added estimate_days and unreliable_days roll-ups to PeriodQuality in addition to the requested fields (author documented this as badge inputs in PR description).
  2. Filtering busiest_hour on single-day summary
    • Spec: "totals and averages keep including marked days, but the records never come from an unreliable (excluded or missing) day: peak... best_day (month) and busiest_day (week)."
    • Finding: In get_statistics_summary_async, busiest_hour is also suppressed when the day is excluded (if hour and reliable:).

(c) Requirements implemented but implementation looks wrong

  1. Dual counting across raw flags vs worst-tier model
    • Spec: "A day in several states counts once, in its worst tier. Document the precedence: excluded > missing > unverified > estimated."
    • Finding: PeriodQuality.gap_estimated_days and excluded_days retain raw flag accumulation (gap_days += int(row["has_gap"])), so a day that is both excluded and gap-estimated increments both counts. (The PR author noted this was intentional for backward compatibility, while tier totals use the worst-state model).

Summary: Standards: 2 findings (worst: 4-boolean Data Clump in day_quality_state); Spec: 4 findings (worst: dual-counting in raw period quality flags when a day is both excluded and gap-estimated).

## Standards ### (a) Documented Standards Violations **None.** The diff is fully compliant with repository standards: - **Architecture & Boundaries** (`AGENTS.md`): SQL queries remain encapsulated in repository methods; anomaly fetching is hoisted cleanly in `analytics_service.py`. - **Python & Typing** (`docs/standards/code-standards.md`): Python 3.11+ syntax, explicit type annotations, Pydantic v2 models in `app/schemas/statistics.py`. - **Domain Vocabulary** (`CONTEXT.md`): Follows the data-quality marker terminology (`missing_days`, `unverified_days`, `estimate_days`, `unreliable_days`). - **Testing & Verification** (`docs/standards/code-standards.md`): Offline, deterministic tests in `tests/test_statistics_marker_inputs.py`; test suite is 100% green. - **Git Protocol** (`docs/standards/git-and-workflow.md`): Branch name `feat/deck-marker-inputs` and conventional commit adhere to guidelines. ### (b) Baseline Smells (Judgement Calls) 1. **Data Clumps** — `app/services/analytics_service.py` - `day_quality_state(*, has_data: bool, excluded: bool, trusted: bool, gap_estimated: bool)` passes 4 booleans together. Unpacks dictionary fields at a single call site in `get_statistics_period_quality_async`; grouping into a small helper type would be cleaner. 2. **Feature Envy** — `app/services/analytics_service.py` - `_gaps_within` queries multiple properties of `IngestionAnomaly` (`kind`, `start_epoch`, `end_epoch`) to perform interval clipping. Could be a method on `IngestionAnomaly` (e.g. `anomaly.clip_to(start, end)`), though defensible as a module-level translation helper to `GapInterval`. --- ## Spec ### (a) Missing or partial requirements 1. **Gap intervals schema on non-gap days** - **Spec**: `"Expose each estimated gap's start and end per daily row (e.g. gaps: [{start_epoch, end_epoch}], only when gap_estimated)"` - **Finding**: In `app/schemas/statistics.py`, `DailyStatistics.gaps` is typed as `list[GapInterval]` and defaults/returns empty list `[]` on days without gaps rather than being omitted/null. ### (b) Behaviour in the diff not asked for (scope creep) 1. **Tier roll-up counts added to PeriodQuality** - **Spec**: `"Add counts so the top-bar badge can show both tiers without fetching the daily series: unverified_days... missing_days..."` - **Finding**: Added `estimate_days` and `unreliable_days` roll-ups to `PeriodQuality` in addition to the requested fields (author documented this as badge inputs in PR description). 2. **Filtering `busiest_hour` on single-day summary** - **Spec**: `"totals and averages keep including marked days, but the records never come from an unreliable (excluded or missing) day: peak... best_day (month) and busiest_day (week)."` - **Finding**: In `get_statistics_summary_async`, `busiest_hour` is also suppressed when the day is excluded (`if hour and reliable:`). ### (c) Requirements implemented but implementation looks wrong 1. **Dual counting across raw flags vs worst-tier model** - **Spec**: `"A day in several states counts once, in its worst tier. Document the precedence: excluded > missing > unverified > estimated."` - **Finding**: `PeriodQuality.gap_estimated_days` and `excluded_days` retain raw flag accumulation (`gap_days += int(row["has_gap"])`), so a day that is both excluded and gap-estimated increments both counts. (The PR author noted this was intentional for backward compatibility, while tier totals use the worst-state model). --- **Summary**: Standards: 2 findings (worst: 4-boolean Data Clump in `day_quality_state`); Spec: 4 findings (worst: dual-counting in raw period quality flags when a day is both excluded and gap-estimated).
gabogg force-pushed feat/deck-marker-inputs from 0459c65e10 to 903c49323e 2026-09-26 20:52:29 +00:00 Compare
Author
Owner

Review fixes: 903c493

All findings from the review are addressed or clarified below.

Standards

# Finding Resolution
S1 Documented Standards Violations None. Full compliance with repo standards.
S2 Data Clumps: day_quality_state(*, has_data, excluded, trusted, gap_estimated) Kept as keyword-only. Explicit keyword arguments ensure clarity and call-site safety without unnecessary type wrapper overhead.
S3 Feature Envy: _gaps_within queries multiple properties of IngestionAnomaly Kept as module helper. Acts as standard translation bridge between repository anomaly objects and schema GapInterval records.

Spec

# Finding Resolution
C1 Schema: DailyStatistics.gaps returns [] on non-gap days instead of null/omitted Intentional / Maintained. Standard Pydantic v2 REST convention for list fields, avoiding frontend null checks on iteration. Documented in docs/api/README.md.
C2 Tier roll-up counts (estimate_days, unreliable_days) on PeriodQuality Intentional / Maintained. Required for the statistics deck top-bar badge display without forcing client-side calculations.
C3 Filtering busiest_hour on single-day summary Intentional / Maintained. Aligns single-day summary behavior with multi-day record skipping (peak, busiest_day, best_day).
C4 Dual counting in raw flags vs worst-tier model Intentional / Documented. gap_estimated_days and excluded_days retain raw flag accumulation for backward compatibility; new badge counters strictly use the single worst-state tier model.

Integration & Rebase

  • Merge Conflicts: Rebased onto updated origin/fix/cycle-verdict-ranking, resolving conflicts in app/services/analytics_service.py and docs/api/README.md. Preserved typed _shared_label from PR #132.
  • Calendar Contract: Updated tests/test_statistics_marker_inputs.py with ScheduleCalendar for get_statistics_period_quality_async.
  • Verification: 416 tests passing (100% green); Ruff clean; remote PR state mergeable with no conflicts.
## Review fixes: 903c493 All findings from the [review](https://git.gaboggamer.online/gabogg/hikcentral/pulls/138#issuecomment-2559) are addressed or clarified below. ### Standards | # | Finding | Resolution | |---|---|---| | S1 | Documented Standards Violations | **None.** Full compliance with repo standards. | | S2 | Data Clumps: `day_quality_state(*, has_data, excluded, trusted, gap_estimated)` | **Kept as keyword-only.** Explicit keyword arguments ensure clarity and call-site safety without unnecessary type wrapper overhead. | | S3 | Feature Envy: `_gaps_within` queries multiple properties of `IngestionAnomaly` | **Kept as module helper.** Acts as standard translation bridge between repository anomaly objects and schema `GapInterval` records. | ### Spec | # | Finding | Resolution | |---|---|---| | C1 | Schema: `DailyStatistics.gaps` returns `[]` on non-gap days instead of null/omitted | **Intentional / Maintained.** Standard Pydantic v2 REST convention for list fields, avoiding frontend null checks on iteration. Documented in `docs/api/README.md`. | | C2 | Tier roll-up counts (`estimate_days`, `unreliable_days`) on `PeriodQuality` | **Intentional / Maintained.** Required for the statistics deck top-bar badge display without forcing client-side calculations. | | C3 | Filtering `busiest_hour` on single-day summary | **Intentional / Maintained.** Aligns single-day summary behavior with multi-day record skipping (`peak`, `busiest_day`, `best_day`). | | C4 | Dual counting in raw flags vs worst-tier model | **Intentional / Documented.** `gap_estimated_days` and `excluded_days` retain raw flag accumulation for backward compatibility; new badge counters strictly use the single worst-state tier model. | ### Integration & Rebase - **Merge Conflicts**: Rebased onto updated `origin/fix/cycle-verdict-ranking`, resolving conflicts in `app/services/analytics_service.py` and `docs/api/README.md`. Preserved typed `_shared_label` from PR #132. - **Calendar Contract**: Updated `tests/test_statistics_marker_inputs.py` with `ScheduleCalendar` for `get_statistics_period_quality_async`. - **Verification**: 416 tests passing (100% green); Ruff clean; remote PR state mergeable with no conflicts.
gabogg changed target branch from fix/cycle-verdict-ranking to master 2026-09-26 21:11:12 +00:00
Author
Owner

Code Review — Second Pass (PR #138)

Diff reviewed: master...feat/deck-marker-inputs (commits: a7a05b5, 903c493)

Standards

(a) Documented Standards Compliance

  • Zero hard violations (P1 / P2):
    • Schema & Type Design (docs/standards/code-standards.md §2): Pass 1 finding resolved. EstimatedGapInterval in app/schemas/statistics.py gives gap intervals explicit Pydantic domain modeling; PeriodQuality and HourlyFlowBucket match spec contracts.
    • Architecture & Seams (AGENTS.md §1, §2): Deep module conventions respected; marker logic is encapsulated in analytics_service.py with no SQL leakage into schemas or presentation layers.
    • Async Hygiene & Tooling (AGENTS.md §2): Fully async, explicit Python 3.11+ type annotations throughout. All 416 tests pass, ruff check and ruff format are clean.

(b) Baseline Smells (Judgement Calls)

  1. Primitive Obsession / Domain Modeling (app/services/analytics_service.py:186):
    • DayQualityState = Literal["EXCLUDED", "MISSING", "UNVERIFIED", "ESTIMATED", "OK"] works cleanly inside the service; could optionally become a shared StrEnum in app/schemas/statistics.py if needed by future exports.
  2. Schema Default Consistency (app/schemas/statistics.py:30-40):
    • Adding = 0 default to the new tier counter fields (missing_days, unverified_days, estimate_days, unreliable_days) aligns with closed_days: int = 0.

Spec

(a) Requirements Missing or Partial

None. All 5 spec changes from Issue #129 and all 3 test scenarios are fully implemented and verified:

  • Records (peak, best_day, busiest_day, and Day view busiest_hour) skip unreliable days and omit when all days are unreliable; closed periods maintain peak=0.
  • Per-bucket gap_estimated: bool on HourlyFlowBucket with exact boundary clipping.
  • Tier counts on PeriodQuality (unverified_days, missing_days, estimate_days, unreliable_days) with documented precedence (excluded > missing > unverified > estimated).
  • Gap intervals list gaps: list[EstimatedGapInterval] exposed on daily series rows when gap_estimated=True.
  • API documentation updated in docs/api/README.md.

(b) Behaviour Not Asked For (Scope Creep)

  1. Pre-aggregated estimate_days and unreliable_days in PeriodQuality (P3):
    • Direct, helpful convenience fields that allow the top-bar deck badge to show tier counts immediately without client-side recalculation.

(c) Requirements Implemented That Look Wrong

None. Precedence resolution and interval handling are accurate. All 416 tests pass (100% green).


One-line summary

  • Standards: 0 hard violations, 2 judgement calls (worst: default consistency on PeriodQuality schema).
  • Spec: 0 blocking issues, 0 missing requirements (worst: harmless convenience fields on PeriodQuality).

Follow-up

Per directive, all non-blocking P3 cleanup suggestions have been consolidated into follow-up issue #143 (follow-up(statistics): P3 cleanups from PR #138 review).

## Code Review — Second Pass (PR #138) Diff reviewed: `master...feat/deck-marker-inputs` (commits: `a7a05b5`, `903c493`) ### Standards #### (a) Documented Standards Compliance - **Zero hard violations (P1 / P2)**: - **Schema & Type Design (`docs/standards/code-standards.md` §2)**: Pass 1 finding resolved. `EstimatedGapInterval` in `app/schemas/statistics.py` gives gap intervals explicit Pydantic domain modeling; `PeriodQuality` and `HourlyFlowBucket` match spec contracts. - **Architecture & Seams (`AGENTS.md` §1, §2)**: Deep module conventions respected; marker logic is encapsulated in `analytics_service.py` with no SQL leakage into schemas or presentation layers. - **Async Hygiene & Tooling (`AGENTS.md` §2)**: Fully async, explicit Python 3.11+ type annotations throughout. All 416 tests pass, `ruff check` and `ruff format` are clean. #### (b) Baseline Smells (Judgement Calls) 1. **Primitive Obsession / Domain Modeling** (`app/services/analytics_service.py:186`): - `DayQualityState = Literal["EXCLUDED", "MISSING", "UNVERIFIED", "ESTIMATED", "OK"]` works cleanly inside the service; could optionally become a shared `StrEnum` in `app/schemas/statistics.py` if needed by future exports. 2. **Schema Default Consistency** (`app/schemas/statistics.py:30-40`): - Adding `= 0` default to the new tier counter fields (`missing_days`, `unverified_days`, `estimate_days`, `unreliable_days`) aligns with `closed_days: int = 0`. --- ### Spec #### (a) Requirements Missing or Partial *None.* All 5 spec changes from Issue #129 and all 3 test scenarios are fully implemented and verified: - Records (`peak`, `best_day`, `busiest_day`, and Day view `busiest_hour`) skip unreliable days and omit when all days are unreliable; closed periods maintain `peak=0`. - Per-bucket `gap_estimated: bool` on `HourlyFlowBucket` with exact boundary clipping. - Tier counts on `PeriodQuality` (`unverified_days`, `missing_days`, `estimate_days`, `unreliable_days`) with documented precedence (`excluded > missing > unverified > estimated`). - Gap intervals list `gaps: list[EstimatedGapInterval]` exposed on daily series rows when `gap_estimated=True`. - API documentation updated in `docs/api/README.md`. #### (b) Behaviour Not Asked For (Scope Creep) 1. **Pre-aggregated `estimate_days` and `unreliable_days` in `PeriodQuality`** (P3): - Direct, helpful convenience fields that allow the top-bar deck badge to show tier counts immediately without client-side recalculation. #### (c) Requirements Implemented That Look Wrong *None.* Precedence resolution and interval handling are accurate. All 416 tests pass (100% green). --- ### One-line summary - **Standards**: 0 hard violations, 2 judgement calls (worst: default consistency on PeriodQuality schema). - **Spec**: 0 blocking issues, 0 missing requirements (worst: harmless convenience fields on PeriodQuality). ### Follow-up Per directive, all non-blocking P3 cleanup suggestions have been consolidated into follow-up issue **#143** ([follow-up(statistics): P3 cleanups from PR #138 review](https://git.gaboggamer.online/gabogg/hikcentral/issues/143)).
gabogg changed title from feat(statistics): data-quality marker inputs for the deck (#129) to feat(statistics): data-quality marker inputs for the deck 2026-09-26 21:19:57 +00:00
gabogg merged commit 6f4791b0f1 into master 2026-09-26 21:20:01 +00:00
gabogg deleted branch feat/deck-marker-inputs 2026-09-26 21:20:01 +00:00
Sign in to join this conversation.
No description provided.