cctally 1.10.1 → 1.10.2
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 +5 -0
- package/bin/_cctally_dashboard.py +5 -3
- package/bin/_lib_blocks.py +74 -44
- package/bin/_lib_view_models.py +2 -0
- package/bin/cctally +79 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.10.2] - 2026-05-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `blocks` panel + CLI no longer renders a phantom ACTIVE heuristic block when a canonical 5h reset doesn't fall on a 10-minute boundary. The legacy partition predicate used `[R_floored - 5h, R_floored)` as the bucket interval, so floor-band entries — session timestamps inside `[floor(R), R)` (e.g. the 09:30–09:39:59 UTC band for a 09:39:59 reset) — fell into neither the preceding nor the following recorded bucket, dropped to `leftover`, and became a phantom heuristic block running `+5h` from `floor_to_hour(first_leftover_entry)`. With current-time activity in the band, that phantom block was `is_active=True` and overlapped the real canonical ACTIVE block, surfacing as TWO ACTIVE rows in `cctally blocks` and a `~HH:00` heuristic plus a `HH:40` recorded entry in the dashboard `/api/data` payload. The fix routes both the entry partition AND the recorded `Block` construction (start_time / end_time / is_active / burn / projection) through a single `_exact_interval(R)` helper that returns the canonical `(block_start_at, five_hour_resets_at)` from `five_hour_blocks` (jitter intact); `_load_recorded_five_hour_windows` grows a third return — `canonical_intervals: {R_floored → (bs_utc, rs_utc)}` — plumbed through `build_blocks_view`, `cmd_blocks`, `_dashboard_build_blocks_view`, and the `/api/block/:start_at` handler. Raw-only anchors (no canonical `five_hour_blocks` row) fall back to the legacy `(R - 5h, R)` shape. Credit-truncated anchors compose with the new path: the truncation rewrites the upper bound in-place inside `canonical_intervals` while preserving the override-supplied `bs`. Regression: new `tests/fixtures/blocks/floor-band-trap/` scenario (canonical reset at `T:39:59 UTC` with 8 dense entries in the trap band) locks the kernel behavior; new `test_block_detail_floor_band_trap_returns_exact_window` in `tests/test_dashboard_api_block.py` covers the panel ↔ detail consistency surface (clicking a block whose canonical reset is off the 10-min boundary returns 200 with the exact `bs` window instead of 404'ing against the floored bs). ([#76](https://github.com/omrikais/cctally-dev/issues/76))
|
|
12
|
+
|
|
8
13
|
## [1.10.1] - 2026-05-20
|
|
9
14
|
|
|
10
15
|
### Fixed
|
|
@@ -2330,8 +2330,8 @@ def _dashboard_build_blocks_view(conn: "sqlite3.Connection",
|
|
|
2330
2330
|
entries = get_entries(fetch_start, fetch_end, skip_sync=skip_sync)
|
|
2331
2331
|
entries = [e for e in entries if week_start_at <= e.timestamp < week_end_at]
|
|
2332
2332
|
|
|
2333
|
-
recorded_windows, block_start_overrides =
|
|
2334
|
-
fetch_start, fetch_end
|
|
2333
|
+
recorded_windows, block_start_overrides, canonical_intervals = (
|
|
2334
|
+
_load_recorded_five_hour_windows(fetch_start, fetch_end)
|
|
2335
2335
|
)
|
|
2336
2336
|
c = _cctally()
|
|
2337
2337
|
return c.build_blocks_view(
|
|
@@ -2339,6 +2339,7 @@ def _dashboard_build_blocks_view(conn: "sqlite3.Connection",
|
|
|
2339
2339
|
now_utc=now_utc,
|
|
2340
2340
|
recorded_windows=recorded_windows,
|
|
2341
2341
|
block_start_overrides=block_start_overrides,
|
|
2342
|
+
canonical_intervals=canonical_intervals,
|
|
2342
2343
|
range_start=week_start_at,
|
|
2343
2344
|
range_end=week_end_at,
|
|
2344
2345
|
display_tz=display_tz,
|
|
@@ -5997,7 +5998,7 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
5997
5998
|
now_utc = _command_as_of()
|
|
5998
5999
|
# Recorded-windows lookup widens by one block on each side so
|
|
5999
6000
|
# a recorded reset just outside the bounds can still anchor.
|
|
6000
|
-
recorded_windows, block_start_overrides = (
|
|
6001
|
+
recorded_windows, block_start_overrides, canonical_intervals = (
|
|
6001
6002
|
_load_recorded_five_hour_windows(
|
|
6002
6003
|
start_at - BLOCK_DURATION, end_at + BLOCK_DURATION,
|
|
6003
6004
|
)
|
|
@@ -6020,6 +6021,7 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
6020
6021
|
entries_in_window, mode="auto",
|
|
6021
6022
|
recorded_windows=recorded_windows,
|
|
6022
6023
|
block_start_overrides=block_start_overrides,
|
|
6024
|
+
canonical_intervals=canonical_intervals,
|
|
6023
6025
|
now=now_utc,
|
|
6024
6026
|
)
|
|
6025
6027
|
target = next(
|
package/bin/_lib_blocks.py
CHANGED
|
@@ -95,6 +95,9 @@ def _group_entries_into_blocks(
|
|
|
95
95
|
*,
|
|
96
96
|
recorded_windows: list[dt.datetime] | None = None,
|
|
97
97
|
block_start_overrides: dict[dt.datetime, dt.datetime] | None = None,
|
|
98
|
+
canonical_intervals: dict[
|
|
99
|
+
dt.datetime, tuple[dt.datetime, dt.datetime]
|
|
100
|
+
] | None = None,
|
|
98
101
|
now: dt.datetime | None = None,
|
|
99
102
|
) -> list[Block]:
|
|
100
103
|
"""Group sorted UsageEntry objects into 5-hour blocks with gap detection.
|
|
@@ -103,21 +106,29 @@ def _group_entries_into_blocks(
|
|
|
103
106
|
The last block is marked active if now < block_start + 5h.
|
|
104
107
|
|
|
105
108
|
When `recorded_windows` is non-empty, entries whose timestamp falls in
|
|
106
|
-
|
|
107
|
-
into per-R buckets and built as 'recorded' blocks.
|
|
108
|
-
run through the existing gap-detection heuristic
|
|
109
|
+
the exact ``[bs, rs)`` interval of each accepted anchor are
|
|
110
|
+
partitioned into per-R buckets and built as 'recorded' blocks.
|
|
111
|
+
Leftover entries run through the existing gap-detection heuristic
|
|
112
|
+
(anchor='heuristic').
|
|
113
|
+
|
|
114
|
+
`canonical_intervals` (issue #76): an optional
|
|
115
|
+
``{R_floored → (bs_utc, rs_utc)}`` map carrying the EXACT
|
|
116
|
+
``(block_start_at, five_hour_resets_at)`` per accepted anchor. Drives
|
|
117
|
+
both the partition predicate AND Phase 1.5 ``Block`` construction
|
|
118
|
+
so floor-band entries (timestamps inside ``[floor(R), R)`` for a
|
|
119
|
+
canonical reset that doesn't fall on a 10-minute boundary) land in
|
|
120
|
+
the right bucket and the rendered window matches Anthropic's actual
|
|
121
|
+
interval. Anchors missing from the map fall back to ``(R - 5h, R)``
|
|
122
|
+
(the legacy floored shape) — preserves raw-only / pre-rollup
|
|
123
|
+
history behavior.
|
|
109
124
|
|
|
110
125
|
`block_start_overrides` (v1.7.2 round-5 / Bug J): an optional
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
``_load_recorded_five_hour_windows``
|
|
115
|
-
``
|
|
116
|
-
|
|
117
|
-
to the credit moment, but the block's API-derived START is
|
|
118
|
-
unchanged — without an override the renderer would compute
|
|
119
|
-
``start = truncated_R - 5h`` which is hours before the real start
|
|
120
|
-
and confuses the user with an off-by-hours window header).
|
|
126
|
+
``{R → block_start_at}`` map. Provides the same override semantics
|
|
127
|
+
for callers that don't pass ``canonical_intervals``. When BOTH are
|
|
128
|
+
present, ``canonical_intervals`` wins (the credit-truncation path
|
|
129
|
+
in ``_load_recorded_five_hour_windows`` writes a unified entry
|
|
130
|
+
into ``canonical_intervals`` already carrying the override-supplied
|
|
131
|
+
``bs`` and the truncated ``rs``).
|
|
121
132
|
|
|
122
133
|
`now` pins the current instant (typically via `_command_as_of()`). When
|
|
123
134
|
omitted, falls back to wall clock so existing callers are unaffected.
|
|
@@ -131,31 +142,44 @@ def _group_entries_into_blocks(
|
|
|
131
142
|
|
|
132
143
|
recorded_windows = sorted(recorded_windows or [])
|
|
133
144
|
block_start_overrides = block_start_overrides or {}
|
|
145
|
+
canonical_intervals = canonical_intervals or {}
|
|
146
|
+
|
|
147
|
+
# Single source of truth for the (bs, rs) interval of each accepted
|
|
148
|
+
# anchor — shared by the partition predicate below AND Phase 1.5
|
|
149
|
+
# block construction. issue #76: a 10-min-floored R as a partition
|
|
150
|
+
# boundary trapped floor-band entries into a phantom heuristic
|
|
151
|
+
# block whenever R was off the 10-min boundary; routing both
|
|
152
|
+
# surfaces through this helper keeps them consistent.
|
|
153
|
+
def _exact_interval(R: dt.datetime) -> tuple[dt.datetime, dt.datetime]:
|
|
154
|
+
if R in canonical_intervals:
|
|
155
|
+
return canonical_intervals[R]
|
|
156
|
+
bs = block_start_overrides.get(R, R - BLOCK_DURATION)
|
|
157
|
+
return (bs, R)
|
|
134
158
|
|
|
135
159
|
# ── Partition entries by recorded windows ──────────────────────────
|
|
136
160
|
# For each R in recorded_windows, entries whose timestamp falls in
|
|
137
|
-
# [
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
# grouper.
|
|
141
|
-
#
|
|
142
|
-
# Why override_start_or_R-5h, not always R-5h: a credit-truncated
|
|
143
|
-
# canonical block has R = effective_reset_at_utc (e.g. 17:58Z) but
|
|
144
|
-
# its real ``block_start_at`` is unchanged (e.g. 15:50Z). Using
|
|
145
|
-
# `R - 5h` as the partition floor would pull entries from earlier
|
|
146
|
-
# blocks (e.g. 12:58-15:50Z range) into the truncated bucket. The
|
|
147
|
-
# override keeps the real start so each entry lands in the bucket
|
|
148
|
-
# whose API-defined interval actually contains it.
|
|
161
|
+
# [bs, rs) (the EXACT canonical interval, NOT the 10-min-floored
|
|
162
|
+
# shape) go into recorded_buckets[R]. Everything else (gaps between
|
|
163
|
+
# recorded windows, or fully outside any window) drops into
|
|
164
|
+
# `leftover` and runs through the existing heuristic grouper.
|
|
149
165
|
recorded_buckets: dict[dt.datetime, list[UsageEntry]] = {
|
|
150
166
|
R: [] for R in recorded_windows
|
|
151
167
|
}
|
|
152
168
|
leftover: list[UsageEntry] = []
|
|
169
|
+
# Sort the (interval, R) pairs by rs so bisect_right(rs_keys, ts)
|
|
170
|
+
# locates the candidate window in O(log n) per entry.
|
|
171
|
+
sorted_intervals: list[
|
|
172
|
+
tuple[tuple[dt.datetime, dt.datetime], dt.datetime]
|
|
173
|
+
] = sorted(
|
|
174
|
+
[(_exact_interval(R), R) for R in recorded_windows],
|
|
175
|
+
key=lambda item: item[0][1],
|
|
176
|
+
)
|
|
177
|
+
rs_keys = [iv[0][1] for iv in sorted_intervals]
|
|
153
178
|
for entry in entries_sorted:
|
|
154
|
-
idx = bisect.bisect_right(
|
|
155
|
-
if idx < len(
|
|
156
|
-
R =
|
|
157
|
-
|
|
158
|
-
if bucket_start <= entry.timestamp:
|
|
179
|
+
idx = bisect.bisect_right(rs_keys, entry.timestamp)
|
|
180
|
+
if idx < len(sorted_intervals):
|
|
181
|
+
(bs, rs), R = sorted_intervals[idx]
|
|
182
|
+
if bs <= entry.timestamp < rs:
|
|
159
183
|
recorded_buckets[R].append(entry)
|
|
160
184
|
continue
|
|
161
185
|
leftover.append(entry)
|
|
@@ -192,17 +216,22 @@ def _group_entries_into_blocks(
|
|
|
192
216
|
|
|
193
217
|
# Clamp each raw_block's end so it cannot overlap a later recorded
|
|
194
218
|
# window. Entries in `leftover` are by construction earlier than the
|
|
195
|
-
# next recorded
|
|
196
|
-
# PREVIOUS 5h window that ended no later than that boundary.
|
|
197
|
-
# this clamp, the +5h heuristic span can cross into the
|
|
198
|
-
# window and produce two simultaneously-active rows.
|
|
219
|
+
# next recorded window's real ``bs``, so the heuristic block belongs
|
|
220
|
+
# to a PREVIOUS 5h window that ended no later than that boundary.
|
|
221
|
+
# Without this clamp, the +5h heuristic span can cross into the
|
|
222
|
+
# recorded window and produce two simultaneously-active rows.
|
|
223
|
+
# issue #76: use the EXACT ``bs`` from ``_exact_interval`` so the
|
|
224
|
+
# clamp matches the partition predicate above (using a floored
|
|
225
|
+
# ``R - 5h`` would leave a sliver of overlap whenever bs sits in
|
|
226
|
+
# the 10-min floor band).
|
|
199
227
|
if recorded_windows:
|
|
200
228
|
for rb in raw_blocks:
|
|
201
229
|
idx = bisect.bisect_right(recorded_windows, rb["start"])
|
|
202
230
|
if idx < len(recorded_windows):
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
next_R = recorded_windows[idx]
|
|
232
|
+
next_bs, _next_rs = _exact_interval(next_R)
|
|
233
|
+
if rb["start"] < next_bs < rb["end"]:
|
|
234
|
+
rb["end"] = next_bs
|
|
206
235
|
|
|
207
236
|
# Track the "actual first entry timestamp" for each block so Phase 3
|
|
208
237
|
# can compute gap ends the same way the legacy interleaved code did
|
|
@@ -210,18 +239,19 @@ def _group_entries_into_blocks(
|
|
|
210
239
|
# floor-to-hour window start). Maps id(block) -> actual first ts.
|
|
211
240
|
first_entry_ts_by_block: dict[int, dt.datetime] = {}
|
|
212
241
|
|
|
213
|
-
# Phase 1.5: Build recorded Block objects from non-empty buckets
|
|
242
|
+
# Phase 1.5: Build recorded Block objects from non-empty buckets.
|
|
243
|
+
# issue #76: ``start_time`` / ``end_time`` come from the SAME
|
|
244
|
+
# ``_exact_interval(R)`` that drives the partition predicate above.
|
|
245
|
+
# Without this, a canonical reset off the 10-min boundary would
|
|
246
|
+
# render with the floored window (off by up to 9m59s) and
|
|
247
|
+
# ``is_active`` / burn / projection would compute against the
|
|
248
|
+
# wrong endpoints, even though the partition was correct.
|
|
214
249
|
recorded_block_objs: list[Block] = []
|
|
215
250
|
for R in recorded_windows:
|
|
216
251
|
bucket = recorded_buckets[R]
|
|
217
252
|
if not bucket:
|
|
218
253
|
continue
|
|
219
|
-
|
|
220
|
-
# canonical blocks need their real block_start_at so the
|
|
221
|
-
# rendered window header matches Anthropic's actual interval);
|
|
222
|
-
# default to R - BLOCK_DURATION for normal canonical anchors.
|
|
223
|
-
start_time = block_start_overrides.get(R, R - BLOCK_DURATION)
|
|
224
|
-
end_time = R
|
|
254
|
+
start_time, end_time = _exact_interval(R)
|
|
225
255
|
bucket_sorted = sorted(bucket, key=lambda e: e.timestamp)
|
|
226
256
|
blk = _build_activity_block(
|
|
227
257
|
bucket_sorted, start_time, end_time, now, mode,
|
package/bin/_lib_view_models.py
CHANGED
|
@@ -1146,6 +1146,7 @@ def build_blocks_view(
|
|
|
1146
1146
|
now_utc,
|
|
1147
1147
|
recorded_windows=None,
|
|
1148
1148
|
block_start_overrides=None,
|
|
1149
|
+
canonical_intervals=None,
|
|
1149
1150
|
range_start=None,
|
|
1150
1151
|
range_end=None,
|
|
1151
1152
|
display_tz=None,
|
|
@@ -1191,6 +1192,7 @@ def build_blocks_view(
|
|
|
1191
1192
|
mode=mode,
|
|
1192
1193
|
recorded_windows=recorded_windows,
|
|
1193
1194
|
block_start_overrides=block_start_overrides,
|
|
1195
|
+
canonical_intervals=canonical_intervals,
|
|
1194
1196
|
now=now_utc,
|
|
1195
1197
|
)
|
|
1196
1198
|
rows: list = []
|
package/bin/cctally
CHANGED
|
@@ -3401,10 +3401,42 @@ def _select_non_overlapping_recorded_windows(
|
|
|
3401
3401
|
def _load_recorded_five_hour_windows(
|
|
3402
3402
|
range_start: dt.datetime,
|
|
3403
3403
|
range_end: dt.datetime,
|
|
3404
|
-
) -> tuple[
|
|
3404
|
+
) -> tuple[
|
|
3405
|
+
list[dt.datetime],
|
|
3406
|
+
dict[dt.datetime, dt.datetime],
|
|
3407
|
+
dict[dt.datetime, tuple[dt.datetime, dt.datetime]],
|
|
3408
|
+
]:
|
|
3405
3409
|
"""Return sorted, UTC-aware recorded ``five_hour_resets_at`` values
|
|
3406
3410
|
that anchor real 5h windows in ``[range_start, range_end]``.
|
|
3407
3411
|
|
|
3412
|
+
Returns a 3-tuple ``(selected, block_start_overrides, canonical_intervals)``:
|
|
3413
|
+
|
|
3414
|
+
* ``selected``: list of 10-min-floored ``R`` anchors (sorted),
|
|
3415
|
+
each representing one accepted canonical 5h window. Same shape
|
|
3416
|
+
as before — drives `_group_entries_into_blocks`'s
|
|
3417
|
+
``recorded_windows=`` kwarg.
|
|
3418
|
+
|
|
3419
|
+
* ``block_start_overrides``: ``{R_floored → block_start_at_utc}``
|
|
3420
|
+
for credit-truncated anchors (Bug J). When a credit moment
|
|
3421
|
+
falls inside a canonical block's overlap with the next block,
|
|
3422
|
+
the earlier ``R`` is replaced by the credit moment (floored to
|
|
3423
|
+
10 min) and the original ``block_start_at`` is recorded here so
|
|
3424
|
+
the renderer keeps the real display start.
|
|
3425
|
+
|
|
3426
|
+
* ``canonical_intervals``: ``{R_floored → (bs_utc, rs_utc)}``
|
|
3427
|
+
carrying the **exact** ``(block_start_at, five_hour_resets_at)``
|
|
3428
|
+
for every selected anchor that has a canonical
|
|
3429
|
+
``five_hour_blocks`` row. ``rs_utc`` is the un-floored reset
|
|
3430
|
+
moment (jitter intact), ``bs_utc`` is the API-derived block
|
|
3431
|
+
start normalized to UTC. Drives `_group_entries_into_blocks`'s
|
|
3432
|
+
partition predicate AND Phase 1.5 block construction
|
|
3433
|
+
(issue #76 — 10-min-floor partition trap). Anchors with no
|
|
3434
|
+
canonical row (legacy weekly-snapshots-only) are absent from
|
|
3435
|
+
the map and the partitioner falls back to ``(R - 5h, R)``.
|
|
3436
|
+
Credit-truncated anchors land here with the truncated upper
|
|
3437
|
+
bound (``rs = effective_reset``) and the override-supplied
|
|
3438
|
+
``bs`` (the real pre-truncation block start).
|
|
3439
|
+
|
|
3408
3440
|
Two sources contribute to the merged anchor set:
|
|
3409
3441
|
|
|
3410
3442
|
1. ``weekly_usage_snapshots.five_hour_resets_at`` — every
|
|
@@ -3515,7 +3547,7 @@ def _load_recorded_five_hour_windows(
|
|
|
3515
3547
|
# OSError covers ensure_dirs() failures (read-only FS, permission
|
|
3516
3548
|
# denied on parent dir) that propagate from open_db() before any
|
|
3517
3549
|
# SQL runs. Either way, fall back to the heuristic anchor path.
|
|
3518
|
-
return [], {}
|
|
3550
|
+
return [], {}, {}
|
|
3519
3551
|
counts: dict[dt.datetime, int] = {}
|
|
3520
3552
|
for row in rows:
|
|
3521
3553
|
raw = row["five_hour_resets_at"] if hasattr(row, "keys") else row[0]
|
|
@@ -3571,6 +3603,21 @@ def _load_recorded_five_hour_windows(
|
|
|
3571
3603
|
canonical_pairs.append((bs, rs))
|
|
3572
3604
|
canonical_pairs.sort(key=lambda p: p[0])
|
|
3573
3605
|
|
|
3606
|
+
# issue #76: canonical_intervals maps every floored R -> its EXACT
|
|
3607
|
+
# (block_start_at, five_hour_resets_at) — both UTC, rs un-floored
|
|
3608
|
+
# (jitter intact). Drives the partition predicate AND Phase 1.5
|
|
3609
|
+
# block construction in `_group_entries_into_blocks` so floor-band
|
|
3610
|
+
# entries (timestamps in [floor(R), R)) land in the right bucket
|
|
3611
|
+
# and the displayed window matches Anthropic's actual interval.
|
|
3612
|
+
# Built before the credit-truncation loop below so that loop can
|
|
3613
|
+
# rewrite the upper bound in-place (truncated R replaces rs).
|
|
3614
|
+
canonical_intervals: dict[
|
|
3615
|
+
dt.datetime, tuple[dt.datetime, dt.datetime]
|
|
3616
|
+
] = {}
|
|
3617
|
+
for bs, rs in canonical_pairs:
|
|
3618
|
+
snapped = _floor_to_ten_minutes(rs)
|
|
3619
|
+
canonical_intervals[snapped] = (bs, rs)
|
|
3620
|
+
|
|
3574
3621
|
# Detect overlap-with-credit and replace the earlier R with a
|
|
3575
3622
|
# credit-truncated anchor. The (anchor → real_block_start) map is
|
|
3576
3623
|
# returned alongside the anchor list so the renderer can show the
|
|
@@ -3595,6 +3642,21 @@ def _load_recorded_five_hour_windows(
|
|
|
3595
3642
|
if bs < cm_floored < rs:
|
|
3596
3643
|
truncated_R = cm_floored
|
|
3597
3644
|
block_start_overrides[cm_floored] = bs
|
|
3645
|
+
# Rewrite canonical_intervals[snapped_orig]
|
|
3646
|
+
# to the truncated interval under the
|
|
3647
|
+
# truncated key. issue #76: the
|
|
3648
|
+
# partitioner reads canonical_intervals
|
|
3649
|
+
# for the exact bs/rs; the truncated entry
|
|
3650
|
+
# must reflect the credit-shifted upper
|
|
3651
|
+
# bound (cm_floored) AND the real bs (the
|
|
3652
|
+
# override) so partition + Phase 1.5
|
|
3653
|
+
# render the credit-shortened block
|
|
3654
|
+
# consistently.
|
|
3655
|
+
snapped_orig = _floor_to_ten_minutes(rs)
|
|
3656
|
+
canonical_intervals.pop(snapped_orig, None)
|
|
3657
|
+
canonical_intervals[cm_floored] = (
|
|
3658
|
+
bs, cm_floored,
|
|
3659
|
+
)
|
|
3598
3660
|
break
|
|
3599
3661
|
truncated_pairs.append((bs, truncated_R))
|
|
3600
3662
|
|
|
@@ -3634,7 +3696,16 @@ def _load_recorded_five_hour_windows(
|
|
|
3634
3696
|
selected = sorted(
|
|
3635
3697
|
list(selected_non_truncated) + list(truncated_anchors)
|
|
3636
3698
|
)
|
|
3637
|
-
|
|
3699
|
+
# Filter canonical_intervals down to selected anchors. Raw-only
|
|
3700
|
+
# anchors (selected via weekly_usage_snapshots but absent from
|
|
3701
|
+
# five_hour_blocks) stay out of the map; the partitioner falls
|
|
3702
|
+
# back to (R - 5h, R) for them. issue #76 / spec §1.1 D1.
|
|
3703
|
+
canonical_intervals = {
|
|
3704
|
+
R: canonical_intervals[R]
|
|
3705
|
+
for R in selected
|
|
3706
|
+
if R in canonical_intervals
|
|
3707
|
+
}
|
|
3708
|
+
return selected, block_start_overrides, canonical_intervals
|
|
3638
3709
|
|
|
3639
3710
|
|
|
3640
3711
|
def cmd_blocks(args: argparse.Namespace) -> int:
|
|
@@ -3679,8 +3750,10 @@ def cmd_blocks(args: argparse.Namespace) -> int:
|
|
|
3679
3750
|
# reset R just after ``range_end`` (e.g. the active window when
|
|
3680
3751
|
# range_end is wall-clock "now") can still anchor entries that fall
|
|
3681
3752
|
# inside [range_start, range_end].
|
|
3682
|
-
recorded_windows, block_start_overrides =
|
|
3683
|
-
|
|
3753
|
+
recorded_windows, block_start_overrides, canonical_intervals = (
|
|
3754
|
+
_load_recorded_five_hour_windows(
|
|
3755
|
+
range_start - BLOCK_DURATION, range_end + BLOCK_DURATION,
|
|
3756
|
+
)
|
|
3684
3757
|
)
|
|
3685
3758
|
|
|
3686
3759
|
# Group into blocks via the view-model kernel (issue #56). The
|
|
@@ -3700,6 +3773,7 @@ def cmd_blocks(args: argparse.Namespace) -> int:
|
|
|
3700
3773
|
now_utc=now_utc,
|
|
3701
3774
|
recorded_windows=recorded_windows,
|
|
3702
3775
|
block_start_overrides=block_start_overrides,
|
|
3776
|
+
canonical_intervals=canonical_intervals,
|
|
3703
3777
|
range_start=range_start,
|
|
3704
3778
|
range_end=range_end,
|
|
3705
3779
|
display_tz=tz,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|