cctally 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/bin/_cctally_dashboard.py +256 -10
- package/bin/_cctally_db.py +290 -21
- package/bin/_cctally_record.py +616 -32
- package/bin/_cctally_tui.py +217 -12
- package/bin/_lib_blocks.py +33 -6
- package/bin/_lib_doctor.py +79 -0
- package/bin/_lib_render.py +20 -0
- package/bin/cctally +841 -29
- package/dashboard/static/assets/index-DhCnIFq9.js +18 -0
- package/dashboard/static/assets/index-Dv5Dzag5.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-BgpoazlS.js +0 -18
- package/dashboard/static/assets/index-nJdUaGys.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.7.3] - 2026-05-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `cctally record-usage`: detect in-place 5h credits on a `>=5.0pp` drop within the same canonical `five_hour_window_key` while `prior_5h_resets_at > now_utc`; write a `five_hour_reset_events` row with the credit moment floored to a 10-minute slot on `effective_reset_at_utc` (supports stacked credits across DISTINCT 10-min slots up to ~30 per 5h block; same-slot collisions silently absorbed by `INSERT OR IGNORE` per spec §2.3 documented cap — an intentional bound, not a bug); force-write `hwm-5h` via the credit-only escape hatch since the normal monotonic-up write at `bin/_cctally_record.py:1722-1731` would refuse to decrease it; then DELETE stale-replica snapshots scoped to `(five_hour_window_key = ?, captured_at_utc >= effective_iso, round(five_hour_percent, 1) = round(prior_5h_pct, 1))` so `claude-statusline`'s in-memory replay of pre-credit `--percent` values across the credit moment cannot poison the post-credit clamp segment. Mirrors the v1.7.2 weekly credit-detection path parallel-not-identical; threshold is 5pp (not 25) because intra-block percents are smaller, and `effective_reset_at_utc` floor is 10 minutes (not the hour) so distinct intra-hour stacked credits stay reachable.
|
|
12
|
+
- schema: new `five_hour_reset_events` table with columns `(detected_at_utc, five_hour_window_key, prior_percent, post_percent, effective_reset_at_utc)` and `UNIQUE(five_hour_window_key, effective_reset_at_utc)`. Payload diverges from the weekly precedent (`prior_percent` + `post_percent` instead of boundary keys) because the 5h variant has a stable canonical window key and only the percent moves — storing both lets renderers show `−Δpp` without re-querying snapshots and supports the rare stacked-credit chain across distinct 10-minute slots. Created adjacent to `week_reset_events` in `_apply_schema` (both are parallel concepts at different cadences); no FK on `five_hour_window_key` per the CLAUDE.md documentation-only-FK gotcha; no `_backfill_five_hour_reset_events` call (forward-only ship per spec Q5; historical backfill deferred).
|
|
13
|
+
- migration `006_five_hour_milestones_reset_event_id`: adds `reset_event_id INTEGER NOT NULL DEFAULT 0` to `five_hour_milestones` and reshapes UNIQUE from `(five_hour_window_key, percent_threshold)` to `(five_hour_window_key, percent_threshold, reset_event_id)` so post-credit threshold crossings re-fire as distinct rows instead of being silently absorbed by `INSERT OR IGNORE` against the pre-credit row at the same threshold. SQLite can't ALTER a UNIQUE constraint in place — the handler uses the rename-recreate-copy idiom inside its own `BEGIN/COMMIT`. Live DDL at `bin/cctally:3902` is updated in lockstep with the migration handler so the dispatcher's fresh-install fast-stamp path lands the correct shape without invoking the handler; the `PRAGMA table_info` probe stamps the marker without redoing the rename when the column is already present (covers fresh-install + partial-failure-retry cases). Mirrors weekly migration 005's pattern. Sentinel `0` = pre-credit / no-event; existing rows backfill to 0 via the column DEFAULT.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 5h DB clamp at `bin/_cctally_record.py:1521-1527` becomes reset-aware via JOIN on `five_hour_reset_events` keyed on the canonical `five_hour_window_key`; the `COALESCE`-to-epoch-zero default keeps legacy clamp behavior byte-identical on DBs without any event rows, while a credited window's `MAX(five_hour_percent)` over the post-credit segment correctly excludes pre-credit snapshots so a fresh post-credit OAuth value (e.g. 4%) lands instead of being held back by stale pre-credit history (e.g. 28%). `unixepoch()` wraps both sides of the captured-vs-effective comparison so the `Z`-vs-`+00:00` offset mix in stored values doesn't break the bound under lex compare — same rule as the production weekly clamp from v1.7.2.
|
|
17
|
+
- renderers: `cctally five-hour-blocks` shows an inline `⚡ credited −Xpp @ HH:MM` chip beside the block-start cell on credited block rows (multiple credits in one block concatenate as `⚡ −Xpp, −Ypp` across distinct 10-min slots); text + JSON envelope (new `credits[]` field per block) + share-output (`--format md` / `html` / `svg`) all carry the annotation via `_build_five_hour_blocks_snapshot`'s `__credits` side-channel piggy-backed on the existing `block_start` cell formatting. `cctally five-hour-breakdown` interleaves a `⚡ CREDIT −Xpp @ HH:MM` divider row between pre-credit and post-credit milestones (merged stream ordered by `captured_at_utc`); JSON gains `credits[]` parallel to `milestones[]` and each milestone now carries `resetEventId` (sentinel 0 = pre-credit, positive integer = post-credit segment id). Dashboard 5h panel chip + `CurrentWeekModal.tsx` 5h milestones section (new envelope key `current_week.five_hour_milestones` parallel to weekly's `current_week.milestones`) + dashboard alerts list row-identity widening (alert id becomes `five_hour:{window_key}:{threshold}:{reset_event_id}` mirroring the weekly precedent at `bin/_cctally_dashboard.py:2597` — NOT a filter, both segments stay in the list as distinct rows) + TUI 5h tile `[⚡ −Xpp]` badge.
|
|
18
|
+
- migration `003_merge_5h_block_duplicates_v1`: defensive update to the milestone dedup loop — widens the dedup key from `percent_threshold` alone to `(percent_threshold, reset_event_id)` via a `PRAGMA table_info` probe so an operator-triggered re-run after migration 006 (`cctally db unskip 003_merge_5h_block_duplicates_v1`, fresh-DB-from-corrupted-backup, future tooling) doesn't silently collapse legitimately distinct pre/post-credit milestone rows at the same threshold inside one physical block. Byte-identical on the legacy upgrade path where the column doesn't yet exist (003 runs before 006 in migration order; `has_seg=False` collapses the key tuple to `(threshold, 0)`).
|
|
19
|
+
- `cctally record-usage`: credit pivots (HWM force-write + stale-replica DELETE) now run UNCONDITIONALLY when a credit is detected on either axis — gating them on `INSERT OR IGNORE`'s `rowcount` (5h branch) or on the `already is None` pre-check (weekly branch) wedged the system permanently on pre-credit HWM + stale-replica rows if a prior invocation committed the event row but crashed before the pivots could run (memory `project_dedup_must_not_gate_side_effects.md`: "Skipping a no-op INSERT must NOT skip milestones/rollups/alerts; prior run may have died mid-flight"). The event-row INSERT stays gated against duplicates; pivots are individually idempotent so re-running them on the recovery tick is safe. Regressions: `tests/test_in_place_5h_credit_detection.py::test_pivots_run_when_event_row_already_committed` + `tests/test_in_place_credit_detection.py::test_weekly_pivots_run_when_event_row_already_committed`.
|
|
20
|
+
- `cctally record-usage`: round-4 follow-up — 5h credit-detection dedup pre-check refined from a single-field `post_percent` compare to a pair-check against BOTH `prior_percent` AND `post_percent` of the most-recent stored event row, AND the post-detection pivots (HWM force-write + stale-replica DELETE) hoisted OUT of the `if not is_dup:` block so they fire on every detection entry. The round-3 single-field predicate false-positived on a legitimate second credit when the user was idle between credits (Credit 1 lands prior=20/post=5; Credit 2 arrives with new CLI percent=0 while prior_5h_pct still reads 5 from the post-Credit-1 snapshot — `5 == 5` matched the stored `post_percent` so the second credit was silently swallowed, no event row written, no HWM force-write, no DELETE). Pair-check disambiguates: a genuine replay matches BOTH fields; a new credit-with-idle matches at most ONE. Hoisting the pivots is the second half of the same fix — a recovery tick where the pair-check legitimately dedupes (event row already durable from a crashed prior invocation, snapshot rolled back so prior_5h_pct still reads the pre-credit value, pair `(20,5)` matches stored `(20,5)`) still needs to force HWM down and DELETE stale replicas; pre-fix those pivots sat INSIDE `if not is_dup:` and got skipped on every recovery tick, leaving the system wedged on the pre-credit HWM forever. Both pivots are individually idempotent (file overwrite, DELETE on stable predicate) so re-running on a replay or recovery tick is always safe. Codex r4 P1 finding ([PR #46 comment 3252721643](https://github.com/omrikais/cctally-dev/pull/46#discussion_r3252721643), issue [#43](https://github.com/omrikais/cctally-dev/issues/43)). Regressions: `tests/test_in_place_5h_credit_detection.py::test_consecutive_credits_with_idle_between` (proves pair-check lets Credit 2 land) + `tests/test_in_place_5h_credit_detection.py::test_replay_with_pair_match_still_runs_pivots` (proves pivots fire when pair-check dedupes).
|
|
21
|
+
- `cctally record-usage`: 5h self-heal probe at `bin/_cctally_record.py:1958+` becomes segment-aware via `_resolve_active_five_hour_reset_event_id`, mirroring the weekly Probe 1 precedent at `bin/_cctally_record.py:1880-1908`. Without segment scoping a credited block's `MAX(percent_threshold)` over the whole ledger reads the pre-credit ceiling (e.g. 28%) and silently suppresses the post-credit ledger's heal even when it has zero rows; with scoping, post-credit threshold crossings inside a stale-`last_observed_at_utc` slot finally trigger heal. Regression: `tests/test_in_place_5h_credit_detection.py::test_5h_self_heal_probe_scoped_to_active_segment`.
|
|
22
|
+
- dashboard alerts envelope: weekly alerts `context` block now exposes `reset_event_id` parallel to the 5h shape — pre-Round-3 the weekly path widened the row identity (`id` includes `:{reset_event_id}`) but did NOT include the segment in `context`, while the 5h path did. Round-3 adds `context.reset_event_id` to weekly so both axes mirror the same shape and downstream consumers (panel, modal, third-party) can discriminate pre- vs post-credit crossings of the same (week, threshold) without scraping the opaque `id` string. Regression: `tests/test_5h_milestone_segment_threading.py::test_weekly_alerts_context_exposes_reset_event_id`.
|
|
23
|
+
- React component coverage: new credit-chip tests in `dashboard/web/__tests__/CurrentWeekPanel.test.tsx` (single + stacked deltas, `data-credit-count` analytics attr, in-row placement) + new 5h-milestone-section tests in `CurrentWeekModal.test.tsx` (section visibility gate, count pill, `⚡ CREDIT` divider row with `colspan=5`, React row key disambiguation by `reset_event_id` so post-credit threshold repeats render as distinct rows). Server-side envelope coverage extended in `tests/test_five_hour_block_envelope.py` (new `_seed_credit_event` helper + populated `credits[]` + empty-array contract + ascending-order chain across stacked credits). Share-output goldens: new `tests/fixtures/share/five-hour-blocks-credit-{md,html,svg}/` fixtures (one in-place credit seeded in `stats.db` via `bin/build-share-fixtures.py`) assert the `⚡ -20pp` chip carries uniformly through all three share-render formats. Dashboard CSS: new `.credit-chip` + `.mcw-5h-credit-row` + `.mcw-5h-credit-cell` + `.m-sec.sec-5h` rules in `dashboard/web/src/index.css` so the credit annotations match the rest of the panel/modal's accent-amber treatment.
|
|
24
|
+
|
|
25
|
+
## [1.7.2] - 2026-05-16
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `cctally record-usage`: detect Anthropic-issued in-place weekly credits (utilization drops while `resets_at` stays unchanged) and emit a `week_reset_events` row + force-write `hwm-7d` + seed a post-credit snapshot so dashboard / forecast / report / percent-breakdown / TUI stop freezing at the pre-credit high-water mark. Fires on a `>=25.0pp` drop, the same threshold as the existing boundary-shift path that catches Anthropic-shifted `resets_at` advances mid-week. Deduped via belt-and-suspenders: a pre-check `SELECT 1 FROM week_reset_events WHERE new_week_end_at = ?` short-circuits before any INSERT attempt, the `UNIQUE(old_week_end_at, new_week_end_at)` DDL constraint absorbs any race that slips past, and the post-credit seed snapshot brings prior_pct down to ~current_pct so the next tick's drop predicate is naturally below threshold (single trigger per credit). HWM file `hwm-7d` is force-written via the credit-only escape hatch since the normal monotonic-up write path at `bin/_cctally_record.py:1511-1525` would refuse to decrease it; force-write lands AFTER `conn.commit()` of the event row so a concurrent reader doesn't see the new HWM before the durable signal of the credit.
|
|
29
|
+
- `cctally record-usage`: the monotonic 7d DB clamp now joins against `week_reset_events`, so post-credit `MAX(weekly_percent)` filters to samples captured at-or-after `effective_reset_at_utc`. Fresh OAuth values land naturally instead of being held back by pre-credit history. No-op when no event row exists for the week (`COALESCE` defaults the filter to epoch-zero, so the regression-guard `test_reset_aware_clamp_without_event_preserves_legacy_behavior` confirms byte-identical legacy clamp behavior on un-credited weeks).
|
|
30
|
+
- `cctally`: `_backfill_week_reset_events` extended to detect historical in-place weekly credits in existing DBs via the same predicate as the live path — parallel `elif prior_end == cur_end` branch inside the existing scan loop, same `prior_end_dt > captured_dt` + `>=25pp` drop gate, same `_floor_to_hour` for the effective moment. Idempotent via `UNIQUE(old_week_end_at, new_week_end_at)` + `INSERT OR IGNORE`; the existing boundary-shift branch is byte-identical to v1.7.1 so existing user DBs synthesize event rows for past credits without affecting prior backfill output.
|
|
31
|
+
- `cctally percent_milestones`: schema migration 005 adds a `reset_event_id` column (default 0 = pre-credit segment / no-event sentinel) and reshapes the UNIQUE constraint from `(week_start_date, percent_threshold)` to `(week_start_date, percent_threshold, reset_event_id)` so post-credit threshold crossings land as separate rows from any pre-credit ones at the same threshold. SQLite can't ALTER a UNIQUE constraint in place — the handler uses the rename-recreate-copy idiom inside its own `BEGIN/COMMIT`; fast-path probe stamps the marker without re-doing the rename when the column is already present (covers fresh-install + partial-failure-retry cases). Existing rows backfill to `reset_event_id = 0` via the column DEFAULT; the migration's per-migration goldens at `tests/fixtures/migrations/per-migration/005_percent_milestones_reset_event_id/{pre,post}.sqlite` are the first lazy-adopted entries under that directory pattern.
|
|
32
|
+
- `cctally percent-breakdown` + dashboard milestone panel + TUI percent-milestones panel: now filter milestone rows by the active `week_reset_events` segment for the queried week (the latest event keyed on the canonical hour-floored `week_end_at`). A credited week's header (which already reflects the post-credit window via the canon-boundary rewrite) is now coherent with its body — pre- and post-credit crossings read as independent ledgers. An empty post-credit segment renders a distinct "(post-credit segment, no milestones crossed yet)" hint so the user can distinguish a freshly-credited week from a genuinely silent one; without this, a fresh-credited week would render "No percent milestones recorded for this week" while the pre-credit ledger is still intact in the DB.
|
|
33
|
+
- `cctally` milestone writer (`maybe_record_milestone` + helpers): now stamps the active `week_reset_events.id` into `percent_milestones.reset_event_id` so post-credit threshold crossings land as separate rows; `get_max_milestone_for_week`, `get_milestone_cost_for_week`, the `alerted_at` UPDATE inside the writer, and the post-INSERT cumulative-cost SELECT for the alert payload all gained a `reset_event_id` filter. Active-segment resolution uses `unixepoch()` on both sides of the `<=` comparison to absorb the `+00:00` vs `Z` offset mix between `week_reset_events.effective_reset_at_utc` (stored as `+00:00`) and a snapshot's `captured_at_utc` (stored as `Z`). Combined with the new UNIQUE shape this means a credited week sees post-credit 1% / 2% / 3% alerts fire fresh even if the pre-credit ledger already crossed those thresholds; the self-heal probe in the dedup-no-insert bail-out path is now also segment-scoped so the post-credit ledger doesn't get silently suppressed by a high pre-credit MAX.
|
|
34
|
+
- `cctally doctor`: new `data.post_credit_milestones` check warns when a credited week (`week_reset_events` row with effective < now) has `latest_weekly_percent >= 1.0` AND zero post-credit milestone rows. Informational WARN (no remediation), since the next `record-usage` tick at >=1% will self-heal via the segment-aware probe — surfaces the upgrade-window gap between when the credit lands and when the user accumulates enough usage to cross the post-credit 1% threshold. The `weekly_percent < 1.0` short-circuit prevents false-positive warns immediately after a credit when the user simply hasn't started using the new segment yet.
|
|
35
|
+
- `cctally record-usage`: round-3 user-test follow-up — defensive cleanup in the in-place credit detection branch. Between the moment Anthropic credits the user and `cctally record-usage` firing, the external `claude-statusline` tool can replay stale pre-credit `--percent` values (its in-memory HWM cache hasn't refreshed yet) — those replays land `captured_at_utc >= effective_reset_at_utc` and poison the reset-aware clamp's MAX over the post-credit segment, blocking legitimate fresh OAuth values from landing. The credit branch now runs a narrow DELETE pass scoped to `(week_start_date = ?, captured_at_utc >= effective_iso, round(weekly_percent, 1) = round(prior_pct, 1))` after writing the event row + force-writing `hwm-7d`. Strict-equality predicate avoids deleting legitimate post-credit climbs. Reported by user on the v1.7.2 dev branch with manual recovery already applied to the production DB; fix prevents recurrence.
|
|
36
|
+
- `cctally report` / `weekly`: round-3 user-test follow-up — credited weeks now render as TWO trend rows (pre-credit segment closed at `effective_reset_at_utc` AND post-credit segment opening at `effective_reset_at_utc`). Previously only the post-credit segment surfaced and the pre-credit segment's usage + cost (the bulk of the week's spend in the originating incident: 67% / $1484 across 6 days) was silently dropped from the trend table. `_apply_reset_events_to_weekrefs` synthesizes the pre-credit ref alongside the post-credit one for events whose row shape is `old_week_end_at == effective_reset_at_utc` (the in-place credit marker — boundary-shift events stay single-ref). `cmd_report`'s per-trend-row usage lookup now passes `as_of_utc = ref.week_end_at` for credited weeks so each segment renders its own latest snapshot (the shared `week_start_date` lookup key would otherwise return the post-credit value for both rows); non-credit weeks still use the unfiltered lookup so existing test fixtures that seed snapshots outside the API-derived week window keep finding their rows.
|
|
37
|
+
- `cctally blocks`: round-3 user-test follow-up — `_load_recorded_five_hour_windows` now overlays canonical anchors from `five_hour_blocks.five_hour_resets_at` (heavy-weight = 1000 per row) on top of the existing `weekly_usage_snapshots.five_hour_resets_at` source. The canonical rollup table holds the API-anchored 5h reset moment after `_canonical_5h_window_key` has absorbed Anthropic's seconds-level jitter; the heavy weight ensures that whenever the rollup table sees a window, the rollup's anchor always wins over any jittered raw snapshot value at the same 10-minute-floored key. Symptom this fixes: after an in-place credit, `cctally blocks` showed the ACTIVE row with the heuristic `~HH:MM` prefix while `cctally five-hour-blocks` correctly showed `⚡ HH:MM` (API-anchored). Both views now agree on the API anchor whenever the rollup table has it.
|
|
38
|
+
- `cctally report`: round-4 user-test follow-up — the "current week" summary box no longer renders the PRE-credit row (the closed segment) for credited weeks. Round-3's pre-credit ref synthesis in `_apply_reset_events_to_weekrefs` left both refs sharing `WeekRef.key`, so the match predicate `week_ref.key == current_ref.key` matched BOTH refs in `cmd_report`'s `current_row` loop and last-write-wins picked whichever was processed last. `current_ref` is now routed through `_apply_reset_events_to_weekrefs` itself so its `week_start_at` reflects the post-credit segment's effective start, and the row-match tightens to require BOTH `key` AND `week_start_at` equality — the pre-credit ref (original `week_start_at`) no longer overwrites the post-credit row's selection. Non-credited weeks are unaffected (`_apply_reset_events_to_weekrefs` is a no-op without an event row). On the user's live DB: summary box now correctly shows post-credit `4%` instead of pre-credit `67%`.
|
|
39
|
+
- `cctally blocks`: round-4 user-test follow-up — the ACTIVE 5h row now swaps to the API-anchored window when a canonical `five_hour_blocks` row exists for the current `five_hour_window_key`. Round-3's anchor-overlay in `_load_recorded_five_hour_windows` only fires when heuristic and canonical fall in the same 10-minute floor bucket; the user's heuristic ACTIVE anchor at `23:00 IDT` and the API-anchored `20:50 IDT` are 130 minutes apart (different floor buckets) so the swap didn't trigger. A new post-pass helper `_maybe_swap_active_block_to_canonical` looks up the live key from `weekly_usage_snapshots`, joins to `five_hour_blocks`, and — when the canonical window is still open relative to `now` — rewrites the active block's `start_time` / `end_time` to the canonical pair and flips `anchor` to `"recorded"` so the renderer drops the `~` prefix. Skips cleanly when no canonical row matches or the canonical window has already closed (then the heuristic anchor reflects genuine ongoing activity in a later window). `cctally blocks` ACTIVE row + `cctally five-hour-blocks` ACTIVE row now agree on the API anchor.
|
|
40
|
+
- `cctally blocks`: round-5 user-test follow-up — the active-block canonical swap now ALSO re-aggregates token / cost totals over the canonical interval (Bug F). The round-4 swap only rewrote the displayed timestamps; the underlying entries were still grouped against the heuristic anchor's `[heuristic_start, heuristic_end)` interval, so a displayed `20:50 IDT → 01:50 IDT` window could show cost from the heuristic's `23:00 → 04:00` group instead — on live data the user saw the swapped window with a $45.42 total when the canonical window's real cost was $128+. `_maybe_swap_active_block_to_canonical` now takes the `all_entries` list, filters to `[canonical_block_start, canonical_block_end)`, and rebuilds the block via `_build_activity_block(...)` so every total stays in one code path (no field-by-field assignment that could drift if the dataclass grows). The displayed timestamps and totals are now coherent on every active-block swap.
|
|
41
|
+
- `cctally` dashboard envelope `trend.weeks[]` (and `cctally weekly-history`): round-5 user-test follow-up — credited weeks now render as TWO trend rows with correct per-segment `used_pct` values (Bug G). Round-3 fixed `cmd_report`'s trend table but `_tui_build_trend` (in `bin/_cctally_tui.py`) — which feeds the dashboard's `trend.weeks[]` envelope, the dashboard share modal's trend panel, and `weekly-history` — still used a key-only `get_latest_usage_for_week(conn, week_ref)` lookup that returned the SAME post-credit snapshot for both segments (both refs share `WeekRef.key`). User saw "May 09 4%" and "May 15 4%" side-by-side in the dashboard's `$/1% Trend` panel — both segments collapsed to the post-credit value. The fix mirrors `cmd_report`'s pattern: detect split-keys (where multiple refs in `week_refs` share `key`), pin `as_of_utc = week_ref.week_end_at` for those refs so each segment finds its own latest snapshot, and route the current_ref through `_apply_reset_events_to_weekrefs` so the `is_current` predicate can disambiguate by BOTH `key` AND `week_start_at` (not just key). Non-credit weeks keep the legacy unfiltered lookup so existing fixtures stay byte-stable.
|
|
42
|
+
- `cctally` dashboard Weekly panel: round-5 user-test follow-up — credited weeks now show TWO rows in the Weekly panel (pre-credit + post-credit segments) instead of silently dropping the pre-credit interval (Bug K). `_apply_reset_events_to_subweeks` shifts the credited SubWeek's `start_ts` to `effective_reset_at_utc`, so `_aggregate_weekly`'s bucket for that week covers ONLY the post-credit interval; the bulk of the week's cost (the user's $1491 of pre-credit spend) was invisible in the panel — only the $134 post-credit segment showed up. `_dashboard_build_weekly_periods` now post-processes its bucket-built rows: for each in-place credit event (`old_week_end_at == effective_reset_at_utc` shape) whose post-credit SubWeek end_ts matches a built row, the dashboard re-walks the entries list filtered to `[original_start, effective)`, re-aggregates cost / tokens / per-model via `_calculate_entry_cost`, looks up `weekly_percent` capped to `captured_at_utc <= effective_reset_at_utc` (the pre-credit peak), and inserts a synthesized pre-credit `WeeklyPeriodRow` BEFORE the post-credit row. Pre-credit row's label uses the original start date, post-credit's label keeps the effective reset date; `is_current` only fires on the post-credit row (the live segment). No-op on non-credit weeks.
|
|
43
|
+
- `cctally blocks`: round-5 user-test follow-up — eliminated the phantom heuristic "~" block that appeared between two canonical blocks after an in-place credit (Bug J). Anthropic's credit creates two overlapping canonical 5h windows: the pre-credit `[block_start_at, original_resets_at]` (e.g. 15:50 → 20:50 UTC) and the post-credit `[block_start_at, new_resets_at]` (e.g. 17:50 → 22:50 UTC). `_select_non_overlapping_recorded_windows` enforces the 5h-disjoint invariant by dropping ONE of the overlapping anchors — leaving the dropped block's entries unanchored and rendered as a heuristic "~" row at $45 sandwiched between the two real canonical rows (visible on the user's `cctally blocks` output). `_load_recorded_five_hour_windows` now detects overlapping canonical pairs where an in-place credit moment falls inside the overlap, truncates the EARLIER block's R to the credit moment (10-min floor), and records `(truncated_R → original_block_start)` in a new `block_start_overrides` map returned alongside the anchor list. Truncated anchors bypass the weighted scheduler (their non-overlap is guaranteed by construction, but the scheduler treats every R as the end of a full 5h window and would still flag them as colliding with the adjacent earlier block). `_group_entries_into_blocks` accepts `block_start_overrides` and uses it both for entry partitioning (the bucket's lower bound becomes the override, not `R - 5h`) and display (the recorded block's `start_time` becomes the override, not `R - 5h` which would be hours before the real start). Result: the truncated block displays correctly with its real `block_start_at` and its real ~2-hour duration instead of the misleading 5h heuristic window. Threaded through `cmd_blocks`, `_dashboard_build_blocks_panel`, and the dashboard `/api/block/:start_at` handler so all three callers see the same anchor set.
|
|
44
|
+
|
|
8
45
|
## [1.7.1] - 2026-05-15
|
|
9
46
|
|
|
10
47
|
### Fixed
|
|
@@ -1955,6 +1955,176 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
1955
1955
|
week_end_at=sw.end_ts,
|
|
1956
1956
|
))
|
|
1957
1957
|
|
|
1958
|
+
# Bug K (v1.7.2 round-5): synthesize a pre-credit segment row for
|
|
1959
|
+
# each in-place credit event. Without this the credited week shows
|
|
1960
|
+
# ONLY the post-credit segment ($134 on live data) and the bulk of
|
|
1961
|
+
# the week's cost (~$372 in entries before the credit moment) is
|
|
1962
|
+
# invisible to the user.
|
|
1963
|
+
#
|
|
1964
|
+
# _apply_reset_events_to_subweeks shifts the credited SubWeek's
|
|
1965
|
+
# start_ts to ``effective_reset_at_utc``, so _aggregate_weekly's
|
|
1966
|
+
# bucket for that SubWeek already covers ONLY the post-credit
|
|
1967
|
+
# interval. We rebuild the pre-credit bucket here by filtering the
|
|
1968
|
+
# same ``entries`` list to ``[original_start, effective)`` and
|
|
1969
|
+
# re-aggregating cost / tokens / per-model.
|
|
1970
|
+
#
|
|
1971
|
+
# The pre-credit row's ``used_pct`` comes from the
|
|
1972
|
+
# weekly_usage_snapshots row captured at-or-before the credit
|
|
1973
|
+
# moment (the pre-credit peak the user reached); fall back to None
|
|
1974
|
+
# if no snapshot was recorded before the credit fired.
|
|
1975
|
+
in_place_credits = conn.execute(
|
|
1976
|
+
"SELECT new_week_end_at, effective_reset_at_utc "
|
|
1977
|
+
"FROM week_reset_events "
|
|
1978
|
+
"WHERE old_week_end_at = effective_reset_at_utc"
|
|
1979
|
+
).fetchall()
|
|
1980
|
+
if in_place_credits:
|
|
1981
|
+
_lib_pricing = sys.modules.get("_lib_pricing")
|
|
1982
|
+
if _lib_pricing is None:
|
|
1983
|
+
import importlib.util as _ilu, pathlib as _pl
|
|
1984
|
+
_p = _pl.Path(__file__).resolve().parent / "_lib_pricing.py"
|
|
1985
|
+
_spec = _ilu.spec_from_file_location("_lib_pricing", _p)
|
|
1986
|
+
_lib_pricing = _ilu.module_from_spec(_spec)
|
|
1987
|
+
sys.modules["_lib_pricing"] = _lib_pricing
|
|
1988
|
+
_spec.loader.exec_module(_lib_pricing)
|
|
1989
|
+
_calc = _lib_pricing._calculate_entry_cost
|
|
1990
|
+
|
|
1991
|
+
insertions: list[tuple[int, WeeklyPeriodRow]] = []
|
|
1992
|
+
for ev in in_place_credits:
|
|
1993
|
+
try:
|
|
1994
|
+
eff_dt = parse_iso_datetime(
|
|
1995
|
+
ev["effective_reset_at_utc"], "credit.eff"
|
|
1996
|
+
)
|
|
1997
|
+
new_end_dt = parse_iso_datetime(
|
|
1998
|
+
ev["new_week_end_at"], "credit.new_end"
|
|
1999
|
+
)
|
|
2000
|
+
except ValueError:
|
|
2001
|
+
continue
|
|
2002
|
+
# Find the SubWeek whose end_ts equals new_week_end_at (the
|
|
2003
|
+
# post-credit segment); its start_ts has already been
|
|
2004
|
+
# shifted to ``effective`` by _apply_reset_events_to_subweeks.
|
|
2005
|
+
post_sw = None
|
|
2006
|
+
for w in weeks:
|
|
2007
|
+
try:
|
|
2008
|
+
w_end = parse_iso_datetime(w.end_ts, "sw.end")
|
|
2009
|
+
except ValueError:
|
|
2010
|
+
continue
|
|
2011
|
+
if w_end == new_end_dt:
|
|
2012
|
+
post_sw = w
|
|
2013
|
+
break
|
|
2014
|
+
if post_sw is None:
|
|
2015
|
+
continue
|
|
2016
|
+
|
|
2017
|
+
# Original start instant: take the EARLIEST recorded
|
|
2018
|
+
# week_start_at for this week_start_date. The post-credit
|
|
2019
|
+
# SubWeek's start_ts is the shifted value (= effective); the
|
|
2020
|
+
# MIN over weekly_usage_snapshots gives us the original
|
|
2021
|
+
# API-derived start before the override fired.
|
|
2022
|
+
orig_row = conn.execute(
|
|
2023
|
+
"SELECT MIN(week_start_at) AS ws "
|
|
2024
|
+
"FROM weekly_usage_snapshots "
|
|
2025
|
+
"WHERE week_start_date = ? AND week_start_at IS NOT NULL",
|
|
2026
|
+
(post_sw.start_date.isoformat(),),
|
|
2027
|
+
).fetchone()
|
|
2028
|
+
if orig_row is None or orig_row["ws"] is None:
|
|
2029
|
+
continue
|
|
2030
|
+
try:
|
|
2031
|
+
original_start_iso = str(orig_row["ws"])
|
|
2032
|
+
original_start_dt = parse_iso_datetime(
|
|
2033
|
+
original_start_iso, "credit.original_start"
|
|
2034
|
+
)
|
|
2035
|
+
except ValueError:
|
|
2036
|
+
continue
|
|
2037
|
+
if original_start_dt >= eff_dt:
|
|
2038
|
+
# No pre-credit interval to aggregate.
|
|
2039
|
+
continue
|
|
2040
|
+
|
|
2041
|
+
# Aggregate entries in [original_start, effective).
|
|
2042
|
+
pre_input = pre_output = pre_cc = pre_cr = 0
|
|
2043
|
+
pre_cost = 0.0
|
|
2044
|
+
pre_models: dict[str, float] = {}
|
|
2045
|
+
pre_entry_count = 0
|
|
2046
|
+
for e in entries:
|
|
2047
|
+
if original_start_dt <= e.timestamp < eff_dt:
|
|
2048
|
+
usage = e.usage
|
|
2049
|
+
pre_input += usage.get("input_tokens", 0)
|
|
2050
|
+
pre_output += usage.get("output_tokens", 0)
|
|
2051
|
+
pre_cc += usage.get("cache_creation_input_tokens", 0)
|
|
2052
|
+
pre_cr += usage.get("cache_read_input_tokens", 0)
|
|
2053
|
+
c = _calc(
|
|
2054
|
+
e.model, usage, mode="auto", cost_usd=e.cost_usd,
|
|
2055
|
+
)
|
|
2056
|
+
pre_cost += c
|
|
2057
|
+
pre_models[e.model] = pre_models.get(e.model, 0.0) + c
|
|
2058
|
+
pre_entry_count += 1
|
|
2059
|
+
if pre_entry_count == 0 and pre_cost <= 0:
|
|
2060
|
+
# No measurable pre-credit activity — skip insertion.
|
|
2061
|
+
continue
|
|
2062
|
+
|
|
2063
|
+
# Pre-credit used_pct: latest snapshot at-or-before the
|
|
2064
|
+
# credit moment for this week_start_date.
|
|
2065
|
+
pre_usage = conn.execute(
|
|
2066
|
+
"SELECT weekly_percent FROM weekly_usage_snapshots "
|
|
2067
|
+
"WHERE week_start_date = ? "
|
|
2068
|
+
" AND unixepoch(captured_at_utc) <= unixepoch(?) "
|
|
2069
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
2070
|
+
(post_sw.start_date.isoformat(), ev["effective_reset_at_utc"]),
|
|
2071
|
+
).fetchone()
|
|
2072
|
+
pre_used_pct: float | None = None
|
|
2073
|
+
if pre_usage is not None and pre_usage["weekly_percent"] is not None:
|
|
2074
|
+
pre_used_pct = float(pre_usage["weekly_percent"])
|
|
2075
|
+
pre_dpp = (
|
|
2076
|
+
pre_cost / pre_used_pct
|
|
2077
|
+
if pre_used_pct and pre_used_pct > 0 else None
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
pre_total = pre_input + pre_output + pre_cc + pre_cr
|
|
2081
|
+
pre_model_breakdowns = [
|
|
2082
|
+
{"modelName": m, "cost": c}
|
|
2083
|
+
for m, c in sorted(pre_models.items(), key=lambda kv: -kv[1])
|
|
2084
|
+
]
|
|
2085
|
+
pre_label = original_start_dt.strftime("%m-%d")
|
|
2086
|
+
pre_row = WeeklyPeriodRow(
|
|
2087
|
+
label=pre_label,
|
|
2088
|
+
cost_usd=pre_cost,
|
|
2089
|
+
total_tokens=pre_total,
|
|
2090
|
+
input_tokens=pre_input,
|
|
2091
|
+
output_tokens=pre_output,
|
|
2092
|
+
cache_creation_tokens=pre_cc,
|
|
2093
|
+
cache_read_tokens=pre_cr,
|
|
2094
|
+
used_pct=pre_used_pct,
|
|
2095
|
+
dollar_per_pct=pre_dpp,
|
|
2096
|
+
delta_cost_pct=None,
|
|
2097
|
+
# Pre-credit segment is historical even though it
|
|
2098
|
+
# shares the bucket date with the live week.
|
|
2099
|
+
is_current=False,
|
|
2100
|
+
models=_model_breakdowns_to_models(
|
|
2101
|
+
pre_model_breakdowns, pre_cost
|
|
2102
|
+
),
|
|
2103
|
+
week_start_at=original_start_iso,
|
|
2104
|
+
week_end_at=ev["effective_reset_at_utc"],
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
# Find post-credit row's index and insert pre-credit BEFORE
|
|
2108
|
+
# it (chronological order: pre then post in oldest-first).
|
|
2109
|
+
post_idx = None
|
|
2110
|
+
for i, r in enumerate(rows_oldest_first):
|
|
2111
|
+
if r.week_start_at == post_sw.start_ts and r.week_end_at == post_sw.end_ts:
|
|
2112
|
+
post_idx = i
|
|
2113
|
+
break
|
|
2114
|
+
if post_idx is None:
|
|
2115
|
+
# The post-credit row may have been dropped by
|
|
2116
|
+
# _aggregate_weekly (no entries in the post-credit
|
|
2117
|
+
# interval) — append at the most-recent slot so the
|
|
2118
|
+
# pre-credit segment still surfaces.
|
|
2119
|
+
insertions.append((len(rows_oldest_first), pre_row))
|
|
2120
|
+
else:
|
|
2121
|
+
insertions.append((post_idx, pre_row))
|
|
2122
|
+
|
|
2123
|
+
# Apply insertions in REVERSE index order so prior insertions
|
|
2124
|
+
# don't shift the indices of later ones.
|
|
2125
|
+
for idx, pre_row in sorted(insertions, key=lambda t: -t[0]):
|
|
2126
|
+
rows_oldest_first.insert(idx, pre_row)
|
|
2127
|
+
|
|
1958
2128
|
# Reverse so caller gets newest-first; compute delta_cost_pct vs the
|
|
1959
2129
|
# immediately older row in that orientation.
|
|
1960
2130
|
rows = list(reversed(rows_oldest_first))
|
|
@@ -2091,10 +2261,14 @@ def _dashboard_build_blocks_panel(conn: "sqlite3.Connection",
|
|
|
2091
2261
|
entries = get_entries(fetch_start, fetch_end, skip_sync=skip_sync)
|
|
2092
2262
|
entries = [e for e in entries if week_start_at <= e.timestamp < week_end_at]
|
|
2093
2263
|
|
|
2094
|
-
recorded_windows = _load_recorded_five_hour_windows(
|
|
2264
|
+
recorded_windows, block_start_overrides = _load_recorded_five_hour_windows(
|
|
2265
|
+
fetch_start, fetch_end,
|
|
2266
|
+
)
|
|
2095
2267
|
blocks = _group_entries_into_blocks(
|
|
2096
2268
|
entries, mode="auto",
|
|
2097
|
-
recorded_windows=recorded_windows,
|
|
2269
|
+
recorded_windows=recorded_windows,
|
|
2270
|
+
block_start_overrides=block_start_overrides,
|
|
2271
|
+
now=now_utc,
|
|
2098
2272
|
)
|
|
2099
2273
|
blocks = [b for b in blocks if not b.is_gap]
|
|
2100
2274
|
if not blocks:
|
|
@@ -2316,7 +2490,7 @@ def _select_current_block_for_envelope(
|
|
|
2316
2490
|
|
|
2317
2491
|
block = conn.execute(
|
|
2318
2492
|
"""
|
|
2319
|
-
SELECT block_start_at, last_observed_at_utc,
|
|
2493
|
+
SELECT five_hour_window_key, block_start_at, last_observed_at_utc,
|
|
2320
2494
|
seven_day_pct_at_block_start,
|
|
2321
2495
|
crossed_seven_day_reset
|
|
2322
2496
|
FROM five_hour_blocks
|
|
@@ -2365,11 +2539,41 @@ def _select_current_block_for_envelope(
|
|
|
2365
2539
|
None if (p_anchor is None or current_used_pct is None)
|
|
2366
2540
|
else round(current_used_pct - p_anchor, 9)
|
|
2367
2541
|
)
|
|
2542
|
+
|
|
2543
|
+
# Spec §5.3 — in-place credit events for this 5h block's window,
|
|
2544
|
+
# ascending by ``effective_reset_at_utc``. Drives the
|
|
2545
|
+
# ``CurrentWeekPanel.tsx`` ``⚡ credited -Xpp`` chip and the
|
|
2546
|
+
# ``CurrentWeekModal.tsx`` merged-stream 5h milestones section.
|
|
2547
|
+
# Snake_case keys to match the envelope convention (see CLAUDE.md;
|
|
2548
|
+
# CLI ``--json`` uses camelCase, dashboard envelope is snake_case).
|
|
2549
|
+
cred_rows = conn.execute(
|
|
2550
|
+
"""
|
|
2551
|
+
SELECT effective_reset_at_utc, prior_percent, post_percent
|
|
2552
|
+
FROM five_hour_reset_events
|
|
2553
|
+
WHERE five_hour_window_key = ?
|
|
2554
|
+
ORDER BY effective_reset_at_utc ASC
|
|
2555
|
+
""",
|
|
2556
|
+
(int(block["five_hour_window_key"]),),
|
|
2557
|
+
).fetchall()
|
|
2558
|
+
credits = [
|
|
2559
|
+
{
|
|
2560
|
+
"effective_reset_at_utc": c["effective_reset_at_utc"],
|
|
2561
|
+
"prior_percent": float(c["prior_percent"]),
|
|
2562
|
+
"post_percent": float(c["post_percent"]),
|
|
2563
|
+
"delta_pp": round(
|
|
2564
|
+
float(c["post_percent"]) - float(c["prior_percent"]), 1
|
|
2565
|
+
),
|
|
2566
|
+
}
|
|
2567
|
+
for c in cred_rows
|
|
2568
|
+
]
|
|
2569
|
+
|
|
2368
2570
|
return {
|
|
2369
2571
|
"block_start_at": block["block_start_at"],
|
|
2572
|
+
"five_hour_window_key": int(block["five_hour_window_key"]),
|
|
2370
2573
|
"seven_day_pct_at_block_start": p_start,
|
|
2371
2574
|
"seven_day_pct_delta_pp": delta,
|
|
2372
2575
|
"crossed_seven_day_reset": crossed,
|
|
2576
|
+
"credits": credits,
|
|
2373
2577
|
}
|
|
2374
2578
|
|
|
2375
2579
|
|
|
@@ -2397,10 +2601,17 @@ def _build_alerts_envelope_array(
|
|
|
2397
2601
|
final sort.
|
|
2398
2602
|
"""
|
|
2399
2603
|
out: list[dict] = []
|
|
2604
|
+
# ``reset_event_id`` (v1.7.2) segments the same (week, threshold)
|
|
2605
|
+
# across pre-credit (0) and post-credit (event.id) cohorts, both
|
|
2606
|
+
# of which can be alerted. The envelope id must include the
|
|
2607
|
+
# segment so React's <li key={a.id}> / <tr key={a.id}> doesn't
|
|
2608
|
+
# collide on the duplicate (week, threshold) pair. Older clients
|
|
2609
|
+
# tolerate longer ids — the id is opaque to them; only the React
|
|
2610
|
+
# key uniqueness invariant matters.
|
|
2400
2611
|
weekly_rows = conn.execute(
|
|
2401
2612
|
"""
|
|
2402
2613
|
SELECT week_start_date, percent_threshold, captured_at_utc,
|
|
2403
|
-
alerted_at, cumulative_cost_usd
|
|
2614
|
+
alerted_at, cumulative_cost_usd, reset_event_id
|
|
2404
2615
|
FROM percent_milestones
|
|
2405
2616
|
WHERE alerted_at IS NOT NULL
|
|
2406
2617
|
ORDER BY alerted_at DESC
|
|
@@ -2413,7 +2624,7 @@ def _build_alerts_envelope_array(
|
|
|
2413
2624
|
cumulative = float(r["cumulative_cost_usd"])
|
|
2414
2625
|
dpp = (cumulative / threshold) if threshold else None
|
|
2415
2626
|
out.append({
|
|
2416
|
-
"id": f"weekly:{r['week_start_date']}:{threshold}",
|
|
2627
|
+
"id": f"weekly:{r['week_start_date']}:{threshold}:{r['reset_event_id']}",
|
|
2417
2628
|
"axis": "weekly",
|
|
2418
2629
|
"threshold": threshold,
|
|
2419
2630
|
"crossed_at": r["captured_at_utc"],
|
|
@@ -2422,13 +2633,29 @@ def _build_alerts_envelope_array(
|
|
|
2422
2633
|
"week_start_date": r["week_start_date"],
|
|
2423
2634
|
"cumulative_cost_usd": cumulative,
|
|
2424
2635
|
"dollars_per_percent": dpp,
|
|
2636
|
+
# Round-3: parallel to the 5h context block below — both
|
|
2637
|
+
# axes now expose ``reset_event_id`` so downstream
|
|
2638
|
+
# clients (panel, modal, third-party consumers) can
|
|
2639
|
+
# discriminate pre- vs post-credit crossings of the
|
|
2640
|
+
# same (week, threshold) without scraping the
|
|
2641
|
+
# envelope ``id`` string. 0 = pre-credit / no-event;
|
|
2642
|
+
# event.id = post-credit segment.
|
|
2643
|
+
"reset_event_id": int(r["reset_event_id"]),
|
|
2425
2644
|
},
|
|
2426
2645
|
})
|
|
2427
2646
|
|
|
2647
|
+
# Site F (spec §3.2 bucket C / §3.3): widen the row identity to
|
|
2648
|
+
# include ``reset_event_id`` so post-credit (seg=event.id) crossings
|
|
2649
|
+
# of the same (window_key, threshold) don't collide with pre-credit
|
|
2650
|
+
# (seg=0) crossings on the React row key. Older clients tolerate
|
|
2651
|
+
# longer ids — the id is opaque to them; only the React key
|
|
2652
|
+
# uniqueness invariant matters. Mirrors the weekly precedent at
|
|
2653
|
+
# line ~2597.
|
|
2428
2654
|
fh_rows = conn.execute(
|
|
2429
2655
|
"""
|
|
2430
2656
|
SELECT m.five_hour_window_key, m.percent_threshold, m.captured_at_utc,
|
|
2431
|
-
m.alerted_at, m.block_cost_usd,
|
|
2657
|
+
m.alerted_at, m.block_cost_usd, m.reset_event_id,
|
|
2658
|
+
b.block_start_at
|
|
2432
2659
|
FROM five_hour_milestones m
|
|
2433
2660
|
LEFT JOIN five_hour_blocks b ON b.five_hour_window_key = m.five_hour_window_key
|
|
2434
2661
|
WHERE m.alerted_at IS NOT NULL
|
|
@@ -2440,7 +2667,10 @@ def _build_alerts_envelope_array(
|
|
|
2440
2667
|
for r in fh_rows:
|
|
2441
2668
|
threshold = int(r["percent_threshold"])
|
|
2442
2669
|
out.append({
|
|
2443
|
-
"id":
|
|
2670
|
+
"id": (
|
|
2671
|
+
f"five_hour:{int(r['five_hour_window_key'])}:"
|
|
2672
|
+
f"{threshold}:{int(r['reset_event_id'])}"
|
|
2673
|
+
),
|
|
2444
2674
|
"axis": "five_hour",
|
|
2445
2675
|
"threshold": threshold,
|
|
2446
2676
|
"crossed_at": r["captured_at_utc"],
|
|
@@ -2449,6 +2679,7 @@ def _build_alerts_envelope_array(
|
|
|
2449
2679
|
"five_hour_window_key": int(r["five_hour_window_key"]),
|
|
2450
2680
|
"block_start_at": r["block_start_at"] or "",
|
|
2451
2681
|
"block_cost_usd": float(r["block_cost_usd"] or 0.0),
|
|
2682
|
+
"reset_event_id": int(r["reset_event_id"]),
|
|
2452
2683
|
},
|
|
2453
2684
|
})
|
|
2454
2685
|
|
|
@@ -2884,6 +3115,17 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2884
3115
|
}
|
|
2885
3116
|
for m in (snap.percent_milestones or [])
|
|
2886
3117
|
],
|
|
3118
|
+
# Spec §5.3 (Codex r1 finding 3) — NEW envelope key
|
|
3119
|
+
# parallel to ``milestones`` (which carries the WEEKLY
|
|
3120
|
+
# timeline). 5h-block milestones for the active block,
|
|
3121
|
+
# in capture-time order, both pre- and post-credit
|
|
3122
|
+
# segments included (bucket B per §3.2 — no
|
|
3123
|
+
# ``reset_event_id`` filter; the React layer renders
|
|
3124
|
+
# repeated thresholds as distinct rows keyed on
|
|
3125
|
+
# ``reset_event_id``). Empty list when no 5h block is
|
|
3126
|
+
# bound or the data source crashed during sync
|
|
3127
|
+
# (recorded on ``last_sync_error``).
|
|
3128
|
+
"five_hour_milestones": getattr(snap, "five_hour_milestones", []) or [],
|
|
2887
3129
|
},
|
|
2888
3130
|
|
|
2889
3131
|
"forecast":
|
|
@@ -4658,8 +4900,10 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
4658
4900
|
now_utc = _command_as_of()
|
|
4659
4901
|
# Recorded-windows lookup widens by one block on each side so
|
|
4660
4902
|
# a recorded reset just outside the bounds can still anchor.
|
|
4661
|
-
recorded_windows =
|
|
4662
|
-
|
|
4903
|
+
recorded_windows, block_start_overrides = (
|
|
4904
|
+
_load_recorded_five_hour_windows(
|
|
4905
|
+
start_at - BLOCK_DURATION, end_at + BLOCK_DURATION,
|
|
4906
|
+
)
|
|
4663
4907
|
)
|
|
4664
4908
|
# Entries: only the window we care about. Mirrors the panel's
|
|
4665
4909
|
# discipline of pre-filtering before grouping (cf.
|
|
@@ -4677,7 +4921,9 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
4677
4921
|
))
|
|
4678
4922
|
blocks = _group_entries_into_blocks(
|
|
4679
4923
|
entries_in_window, mode="auto",
|
|
4680
|
-
recorded_windows=recorded_windows,
|
|
4924
|
+
recorded_windows=recorded_windows,
|
|
4925
|
+
block_start_overrides=block_start_overrides,
|
|
4926
|
+
now=now_utc,
|
|
4681
4927
|
)
|
|
4682
4928
|
target = next(
|
|
4683
4929
|
(b for b in blocks
|