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/bin/_cctally_tui.py
CHANGED
|
@@ -317,6 +317,10 @@ def get_milestones_for_week(*args, **kwargs):
|
|
|
317
317
|
return sys.modules["cctally"].get_milestones_for_week(*args, **kwargs)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def _canonicalize_optional_iso(*args, **kwargs):
|
|
321
|
+
return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
|
|
322
|
+
|
|
323
|
+
|
|
320
324
|
def get_recent_weeks(*args, **kwargs):
|
|
321
325
|
return sys.modules["cctally"].get_recent_weeks(*args, **kwargs)
|
|
322
326
|
|
|
@@ -949,7 +953,8 @@ class TuiPercentMilestone:
|
|
|
949
953
|
def _tui_build_percent_milestones(
|
|
950
954
|
conn: sqlite3.Connection,
|
|
951
955
|
) -> list[TuiPercentMilestone]:
|
|
952
|
-
"""Return per-percent crossings for the current week
|
|
956
|
+
"""Return per-percent crossings for the current week's ACTIVE
|
|
957
|
+
segment, ascending by percent.
|
|
953
958
|
|
|
954
959
|
Resolves `week_start_date` from the latest `weekly_usage_snapshots` row
|
|
955
960
|
— the same path `cmd_percent_breakdown` takes. The post-override
|
|
@@ -957,15 +962,55 @@ def _tui_build_percent_milestones(
|
|
|
957
962
|
reset, `_apply_midweek_reset_override` shifts that datetime forward to
|
|
958
963
|
the reset instant, whose `.date()` no longer matches the `week_start_date`
|
|
959
964
|
under which milestones were recorded.
|
|
960
|
-
|
|
965
|
+
|
|
966
|
+
v1.7.2: when a `week_reset_events` row exists for the snapshot's
|
|
967
|
+
`week_end_at`, narrow to the active segment so the dashboard /
|
|
968
|
+
TUI milestone panel stays coherent with the already-credit-aware
|
|
969
|
+
header. ``active_segment = 0`` (sentinel) preserves legacy
|
|
970
|
+
behavior on un-credited weeks.
|
|
971
|
+
|
|
972
|
+
Returns [] if no usage snapshot exists, OR if the active segment
|
|
973
|
+
has no milestone rows yet (post-credit "fresh" state).
|
|
961
974
|
"""
|
|
962
975
|
latest = conn.execute(
|
|
963
|
-
"SELECT week_start_date FROM weekly_usage_snapshots "
|
|
976
|
+
"SELECT week_start_date, week_end_at FROM weekly_usage_snapshots "
|
|
977
|
+
"WHERE week_end_at IS NOT NULL "
|
|
964
978
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
965
979
|
).fetchone()
|
|
966
980
|
if latest is None:
|
|
967
|
-
|
|
968
|
-
|
|
981
|
+
# Legacy fallback: a snapshot without week_end_at can still have
|
|
982
|
+
# milestones — keep the prior behavior in that path.
|
|
983
|
+
latest = conn.execute(
|
|
984
|
+
"SELECT week_start_date, NULL AS week_end_at "
|
|
985
|
+
"FROM weekly_usage_snapshots "
|
|
986
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
987
|
+
).fetchone()
|
|
988
|
+
if latest is None:
|
|
989
|
+
return []
|
|
990
|
+
|
|
991
|
+
# Resolve active segment via the canonical end_at.
|
|
992
|
+
active_segment = 0
|
|
993
|
+
if latest["week_end_at"]:
|
|
994
|
+
try:
|
|
995
|
+
canon_end = _canonicalize_optional_iso(
|
|
996
|
+
latest["week_end_at"], "tui.pm.cur"
|
|
997
|
+
)
|
|
998
|
+
except (AttributeError, ValueError):
|
|
999
|
+
canon_end = None
|
|
1000
|
+
if canon_end:
|
|
1001
|
+
seg_row = conn.execute(
|
|
1002
|
+
"SELECT id FROM week_reset_events "
|
|
1003
|
+
"WHERE new_week_end_at = ? "
|
|
1004
|
+
"ORDER BY id DESC LIMIT 1",
|
|
1005
|
+
(canon_end,),
|
|
1006
|
+
).fetchone()
|
|
1007
|
+
if seg_row is not None:
|
|
1008
|
+
active_segment = int(seg_row["id"])
|
|
1009
|
+
|
|
1010
|
+
rows = [
|
|
1011
|
+
r for r in get_milestones_for_week(conn, latest["week_start_date"])
|
|
1012
|
+
if int(r["reset_event_id"] or 0) == active_segment
|
|
1013
|
+
]
|
|
969
1014
|
out: list[TuiPercentMilestone] = []
|
|
970
1015
|
for r in rows:
|
|
971
1016
|
try:
|
|
@@ -985,6 +1030,56 @@ def _tui_build_percent_milestones(
|
|
|
985
1030
|
return out
|
|
986
1031
|
|
|
987
1032
|
|
|
1033
|
+
def _tui_build_five_hour_milestones(
|
|
1034
|
+
conn: sqlite3.Connection,
|
|
1035
|
+
five_hour_window_key: int | None,
|
|
1036
|
+
) -> list[dict]:
|
|
1037
|
+
"""Return per-percent 5h-block milestones for the given window, in
|
|
1038
|
+
capture-time order. Spec §5.3 — drives the CurrentWeekModal's new
|
|
1039
|
+
5h-milestone timeline section.
|
|
1040
|
+
|
|
1041
|
+
Bucket B per §3.2: NO ``reset_event_id`` filter — both pre- and
|
|
1042
|
+
post-credit segments render in the merged chronological stream so
|
|
1043
|
+
the user sees the full history of the active block including
|
|
1044
|
+
repeated threshold values after an in-place credit. The React layer
|
|
1045
|
+
differentiates rows by ``reset_event_id`` for key uniqueness.
|
|
1046
|
+
|
|
1047
|
+
Returns [] when the current week has no API-anchored 5h block. The
|
|
1048
|
+
envelope-shaped dict mirrors the CLI ``five-hour-breakdown --json``
|
|
1049
|
+
milestone objects but with snake_case keys (envelope convention).
|
|
1050
|
+
"""
|
|
1051
|
+
if five_hour_window_key is None:
|
|
1052
|
+
return []
|
|
1053
|
+
rows = conn.execute(
|
|
1054
|
+
"""
|
|
1055
|
+
SELECT percent_threshold, captured_at_utc, block_cost_usd,
|
|
1056
|
+
marginal_cost_usd, seven_day_pct_at_crossing,
|
|
1057
|
+
reset_event_id
|
|
1058
|
+
FROM five_hour_milestones
|
|
1059
|
+
WHERE five_hour_window_key = ?
|
|
1060
|
+
ORDER BY captured_at_utc ASC, id ASC
|
|
1061
|
+
""",
|
|
1062
|
+
(int(five_hour_window_key),),
|
|
1063
|
+
).fetchall()
|
|
1064
|
+
out: list[dict] = []
|
|
1065
|
+
for r in rows:
|
|
1066
|
+
out.append({
|
|
1067
|
+
"percent_threshold": int(r["percent_threshold"]),
|
|
1068
|
+
"captured_at_utc": r["captured_at_utc"],
|
|
1069
|
+
"block_cost_usd": float(r["block_cost_usd"]),
|
|
1070
|
+
"marginal_cost_usd": (
|
|
1071
|
+
None if r["marginal_cost_usd"] is None
|
|
1072
|
+
else float(r["marginal_cost_usd"])
|
|
1073
|
+
),
|
|
1074
|
+
"seven_day_pct_at_crossing": (
|
|
1075
|
+
None if r["seven_day_pct_at_crossing"] is None
|
|
1076
|
+
else float(r["seven_day_pct_at_crossing"])
|
|
1077
|
+
),
|
|
1078
|
+
"reset_event_id": int(r["reset_event_id"] or 0),
|
|
1079
|
+
})
|
|
1080
|
+
return out
|
|
1081
|
+
|
|
1082
|
+
|
|
988
1083
|
@dataclass
|
|
989
1084
|
class DataSnapshot:
|
|
990
1085
|
"""All data needed to render one TUI frame. Produced by sync thread,
|
|
@@ -1017,6 +1112,13 @@ class DataSnapshot:
|
|
|
1017
1112
|
# `current_week.five_hour_block` is precomputed via
|
|
1018
1113
|
# `_select_current_block_for_envelope`).
|
|
1019
1114
|
alerts: list[dict] = field(default_factory=list)
|
|
1115
|
+
# ---- 5h in-place credit (v1.7.x) ----
|
|
1116
|
+
# Already-envelope-shaped dicts for the CurrentWeekModal's new 5h
|
|
1117
|
+
# milestone timeline (spec §5.3, Codex r1 finding 3). Parallel to
|
|
1118
|
+
# ``percent_milestones`` (which carries the WEEKLY timeline). Loaded
|
|
1119
|
+
# at sync-thread time so ``snapshot_to_envelope`` stays a pure
|
|
1120
|
+
# renderer; empty list when no current 5h block is bound.
|
|
1121
|
+
five_hour_milestones: list[dict] = field(default_factory=list)
|
|
1020
1122
|
|
|
1021
1123
|
@classmethod
|
|
1022
1124
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -1423,27 +1525,71 @@ def _tui_build_trend(
|
|
|
1423
1525
|
columns (`week_start_at`, `used_pct`, `dollars_per_percent`) matches
|
|
1424
1526
|
`cmd_report` byte-for-byte — verified in the bundle regression diff.
|
|
1425
1527
|
"""
|
|
1426
|
-
# `get_recent_weeks` returns WeekRef rows DESC by week_start_date
|
|
1528
|
+
# `get_recent_weeks` returns WeekRef rows DESC by week_start_date,
|
|
1529
|
+
# already routed through `_apply_reset_events_to_weekrefs` so credited
|
|
1530
|
+
# weeks come back as TWO refs (pre-credit + post-credit) sharing the
|
|
1531
|
+
# same `WeekRef.key`.
|
|
1427
1532
|
week_refs = get_recent_weeks(conn, max(1, count))
|
|
1428
1533
|
|
|
1429
1534
|
# Figure out which week_ref corresponds to the current subscription week.
|
|
1430
|
-
#
|
|
1431
|
-
#
|
|
1535
|
+
# Mirrors `cmd_report`'s Bug D pattern: build a current_ref from the
|
|
1536
|
+
# latest usage snapshot, route it through `_apply_reset_events_to_weekrefs`
|
|
1537
|
+
# so its `week_start_at` reflects the post-credit segment (or the
|
|
1538
|
+
# original start for non-credit weeks), then disambiguate the
|
|
1539
|
+
# synthesized pre-credit ref from the live post-credit ref via BOTH
|
|
1540
|
+
# `key` AND `week_start_at`. Key-only equality marks both segments
|
|
1541
|
+
# as current, which is why the dashboard's trend panel previously
|
|
1542
|
+
# showed two adjacent rows both highlighted as "current" with
|
|
1543
|
+
# identical 4.0% values on the user's live in-place credit data.
|
|
1432
1544
|
latest_usage = conn.execute(
|
|
1433
1545
|
"SELECT week_start_date, week_end_date "
|
|
1434
1546
|
"FROM weekly_usage_snapshots "
|
|
1435
1547
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
1436
1548
|
).fetchone()
|
|
1437
1549
|
current_key: str | None = None
|
|
1438
|
-
|
|
1550
|
+
current_week_start_at: str | None = None
|
|
1551
|
+
if latest_usage is not None and latest_usage["week_start_date"] is not None:
|
|
1439
1552
|
current_key = latest_usage["week_start_date"]
|
|
1553
|
+
try:
|
|
1554
|
+
c = _cctally()
|
|
1555
|
+
canon_start, canon_end = c._get_canonical_boundary_for_date(
|
|
1556
|
+
conn, latest_usage["week_start_date"]
|
|
1557
|
+
)
|
|
1558
|
+
current_ref = c.make_week_ref(
|
|
1559
|
+
week_start_date=latest_usage["week_start_date"],
|
|
1560
|
+
week_end_date=latest_usage["week_end_date"],
|
|
1561
|
+
week_start_at=canon_start,
|
|
1562
|
+
week_end_at=canon_end,
|
|
1563
|
+
)
|
|
1564
|
+
_adjusted = c._apply_reset_events_to_weekrefs(conn, [current_ref])
|
|
1565
|
+
if _adjusted:
|
|
1566
|
+
current_week_start_at = _adjusted[0].week_start_at
|
|
1567
|
+
except (ValueError, sqlite3.DatabaseError, AttributeError):
|
|
1568
|
+
current_week_start_at = None
|
|
1440
1569
|
|
|
1441
1570
|
# Build an intermediate list of (week_ref, used_pct, dpp) in oldest-first
|
|
1442
1571
|
# chronological order.
|
|
1443
1572
|
chrono = list(reversed(week_refs))
|
|
1573
|
+
# Split-key set (Bug D): credited weeks appear twice in `week_refs`
|
|
1574
|
+
# with identical `WeekRef.key`. For those keys ONLY, pin
|
|
1575
|
+
# `as_of_utc=week_ref.week_end_at` so each segment finds its own
|
|
1576
|
+
# latest snapshot — without this both segments collapse to the
|
|
1577
|
+
# post-credit snapshot's weekly_percent. Non-credit weeks (single
|
|
1578
|
+
# ref per key) keep the legacy unfiltered lookup.
|
|
1579
|
+
_split_keys = {
|
|
1580
|
+
r.key
|
|
1581
|
+
for r in week_refs
|
|
1582
|
+
if sum(1 for x in week_refs if x.key == r.key) > 1
|
|
1583
|
+
}
|
|
1444
1584
|
intermediate: list[tuple[Any, float | None, float | None]] = []
|
|
1445
1585
|
for week_ref in chrono:
|
|
1446
|
-
usage = get_latest_usage_for_week(
|
|
1586
|
+
usage = get_latest_usage_for_week(
|
|
1587
|
+
conn,
|
|
1588
|
+
week_ref,
|
|
1589
|
+
as_of_utc=(
|
|
1590
|
+
week_ref.week_end_at if week_ref.key in _split_keys else None
|
|
1591
|
+
),
|
|
1592
|
+
)
|
|
1447
1593
|
# See cmd_report for why reset-affected weeks skip the cost cache
|
|
1448
1594
|
# and live-compute from session_entries over the effective range.
|
|
1449
1595
|
if _week_ref_has_reset_event(conn, week_ref):
|
|
@@ -1491,6 +1637,23 @@ def _tui_build_trend(
|
|
|
1491
1637
|
# localizing midnight-UTC doesn't shift it to the prior day in
|
|
1492
1638
|
# zones west of UTC (e.g. 2026-04-14 → "Apr 13" in America/New_York).
|
|
1493
1639
|
week_label = week_ref.week_start.strftime("%b %d")
|
|
1640
|
+
# Bug G (v1.7.2 round-5): match on BOTH `key` AND `week_start_at`
|
|
1641
|
+
# for credited weeks so the pre-credit synthesized ref doesn't
|
|
1642
|
+
# also light up as "current" — both refs share `key`, only their
|
|
1643
|
+
# `week_start_at` differs (post-credit = effective reset moment,
|
|
1644
|
+
# pre-credit = original API-derived start). Non-credit weeks
|
|
1645
|
+
# have only one ref per key so `week_start_at` matching is
|
|
1646
|
+
# automatic. When `current_week_start_at` is None (no reset
|
|
1647
|
+
# event for the current week, or the resolution above failed),
|
|
1648
|
+
# falls back to legacy key-only matching.
|
|
1649
|
+
is_cur = (
|
|
1650
|
+
current_key is not None
|
|
1651
|
+
and week_ref.key == current_key
|
|
1652
|
+
and (
|
|
1653
|
+
current_week_start_at is None
|
|
1654
|
+
or week_ref.week_start_at == current_week_start_at
|
|
1655
|
+
)
|
|
1656
|
+
)
|
|
1494
1657
|
out.append(TuiTrendRow(
|
|
1495
1658
|
week_label=week_label,
|
|
1496
1659
|
week_start_at=week_start_dt,
|
|
@@ -1502,7 +1665,7 @@ def _tui_build_trend(
|
|
|
1502
1665
|
dollars_per_percent=dpp,
|
|
1503
1666
|
delta_dpp=delta,
|
|
1504
1667
|
spark_height=spark,
|
|
1505
|
-
is_current=
|
|
1668
|
+
is_current=is_cur,
|
|
1506
1669
|
))
|
|
1507
1670
|
if dpp is not None:
|
|
1508
1671
|
prev_dpp = dpp
|
|
@@ -1782,6 +1945,19 @@ def _tui_build_snapshot(
|
|
|
1782
1945
|
alerts = _build_alerts_envelope_array(conn)
|
|
1783
1946
|
except Exception as exc:
|
|
1784
1947
|
errors.append(f"alerts: {exc}")
|
|
1948
|
+
# ---- 5h in-place credit (v1.7.x) ----
|
|
1949
|
+
# Load 5h milestones (pre + post credit) for the current
|
|
1950
|
+
# block's window so CurrentWeekModal can render a merged
|
|
1951
|
+
# chronological timeline alongside its weekly milestones.
|
|
1952
|
+
# Spec §5.3 (Codex r1 finding 3).
|
|
1953
|
+
fh_milestones: list[dict] = []
|
|
1954
|
+
try:
|
|
1955
|
+
win_key = None
|
|
1956
|
+
if cw is not None and isinstance(cw.five_hour_block, dict):
|
|
1957
|
+
win_key = cw.five_hour_block.get("five_hour_window_key")
|
|
1958
|
+
fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
|
|
1959
|
+
except Exception as exc:
|
|
1960
|
+
errors.append(f"five-hour-milestones: {exc}")
|
|
1785
1961
|
return DataSnapshot(
|
|
1786
1962
|
current_week=cw,
|
|
1787
1963
|
forecast=fc,
|
|
@@ -1797,6 +1973,7 @@ def _tui_build_snapshot(
|
|
|
1797
1973
|
blocks_panel=blocks_panel,
|
|
1798
1974
|
daily_panel=daily_panel,
|
|
1799
1975
|
alerts=alerts,
|
|
1976
|
+
five_hour_milestones=fh_milestones,
|
|
1800
1977
|
)
|
|
1801
1978
|
finally:
|
|
1802
1979
|
conn.close()
|
|
@@ -2269,11 +2446,26 @@ def _tui_panel_current_week(
|
|
|
2269
2446
|
f"${cw.dollars_per_percent:.2f}"
|
|
2270
2447
|
if cw.dollars_per_percent is not None else "—"
|
|
2271
2448
|
)
|
|
2449
|
+
# Spec §5.4 — credit badge next to the 5h percent. Source: same
|
|
2450
|
+
# ``cw.five_hour_block.credits`` channel that drives the dashboard
|
|
2451
|
+
# chip; only show when at least one credit is present for the
|
|
2452
|
+
# current block. Format: ``⚡ -Xpp`` (single) / ``⚡ -Xpp, -Ypp``
|
|
2453
|
+
# (stacked across distinct 10-min slots).
|
|
2454
|
+
fh_credit_badge = ""
|
|
2455
|
+
fhb = getattr(cw, "five_hour_block", None)
|
|
2456
|
+
if isinstance(fhb, dict):
|
|
2457
|
+
fh_credits = fhb.get("credits") or []
|
|
2458
|
+
if fh_credits:
|
|
2459
|
+
deltas = ", ".join(
|
|
2460
|
+
f"{float(c.get('delta_pp', 0.0)):+.0f}pp"
|
|
2461
|
+
for c in fh_credits
|
|
2462
|
+
)
|
|
2463
|
+
fh_credit_badge = f" {{bright}}⚡ {deltas}{{/}}"
|
|
2272
2464
|
lines = [
|
|
2273
2465
|
"",
|
|
2274
2466
|
f" Used {{{used_cls}}}{bar_fill}{{/}} {{{used_cls}.b}}{cw.used_pct:>5.1f}%{{/}}",
|
|
2275
2467
|
"",
|
|
2276
|
-
f" 5-hour {{bar.accent}}{five_bar}{{/}} {{bright}}{int(five):>3d}%{{/}}",
|
|
2468
|
+
f" 5-hour {{bar.accent}}{five_bar}{{/}} {{bright}}{int(five):>3d}%{{/}}{fh_credit_badge}",
|
|
2277
2469
|
f" {{dim}}{fr_str}{{/}}" if fr_str else "",
|
|
2278
2470
|
"",
|
|
2279
2471
|
f" {{dim}}Spent{{/}} {{bright}}${cw.spent_usd:.2f}{{/}} "
|
|
@@ -2322,6 +2514,19 @@ def _tui_panel_current_week_hero(
|
|
|
2322
2514
|
else:
|
|
2323
2515
|
reset_suffix = ""
|
|
2324
2516
|
|
|
2517
|
+
# Spec §5.4 — credit badge in the hero variant. Same source as the
|
|
2518
|
+
# grid variant; append after the reset suffix so the badge follows
|
|
2519
|
+
# the "resets in" timer.
|
|
2520
|
+
fhb_hero = getattr(cw, "five_hour_block", None)
|
|
2521
|
+
if isinstance(fhb_hero, dict):
|
|
2522
|
+
fh_credits_hero = fhb_hero.get("credits") or []
|
|
2523
|
+
if fh_credits_hero:
|
|
2524
|
+
deltas_hero = ", ".join(
|
|
2525
|
+
f"{float(c.get('delta_pp', 0.0)):+.0f}pp"
|
|
2526
|
+
for c in fh_credits_hero
|
|
2527
|
+
)
|
|
2528
|
+
reset_suffix = f"{reset_suffix} {{bright}}⚡ {deltas_hero}{{/}}"
|
|
2529
|
+
|
|
2325
2530
|
if snap.last_sync_error:
|
|
2326
2531
|
health = "{warn}daemon error{/}"
|
|
2327
2532
|
elif snap.last_sync_at is None:
|
package/bin/_lib_blocks.py
CHANGED
|
@@ -94,6 +94,7 @@ def _group_entries_into_blocks(
|
|
|
94
94
|
mode: str = "auto",
|
|
95
95
|
*,
|
|
96
96
|
recorded_windows: list[dt.datetime] | None = None,
|
|
97
|
+
block_start_overrides: dict[dt.datetime, dt.datetime] | None = None,
|
|
97
98
|
now: dt.datetime | None = None,
|
|
98
99
|
) -> list[Block]:
|
|
99
100
|
"""Group sorted UsageEntry objects into 5-hour blocks with gap detection.
|
|
@@ -106,6 +107,18 @@ def _group_entries_into_blocks(
|
|
|
106
107
|
into per-R buckets and built as 'recorded' blocks. Leftover entries
|
|
107
108
|
run through the existing gap-detection heuristic (anchor='heuristic').
|
|
108
109
|
|
|
110
|
+
`block_start_overrides` (v1.7.2 round-5 / Bug J): an optional
|
|
111
|
+
`{R → block_start_at}` map. When present for a given R, the
|
|
112
|
+
recorded block's displayed ``start_time`` becomes the override
|
|
113
|
+
instead of the default ``R - BLOCK_DURATION``. Used by
|
|
114
|
+
``_load_recorded_five_hour_windows`` to preserve the real
|
|
115
|
+
``five_hour_blocks.block_start_at`` for credit-truncated windows
|
|
116
|
+
(an in-place credit shortens the prior 5h block's effective end
|
|
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).
|
|
121
|
+
|
|
109
122
|
`now` pins the current instant (typically via `_command_as_of()`). When
|
|
110
123
|
omitted, falls back to wall clock so existing callers are unaffected.
|
|
111
124
|
"""
|
|
@@ -117,13 +130,22 @@ def _group_entries_into_blocks(
|
|
|
117
130
|
now = dt.datetime.now(dt.timezone.utc)
|
|
118
131
|
|
|
119
132
|
recorded_windows = sorted(recorded_windows or [])
|
|
133
|
+
block_start_overrides = block_start_overrides or {}
|
|
120
134
|
|
|
121
135
|
# ── Partition entries by recorded windows ──────────────────────────
|
|
122
136
|
# For each R in recorded_windows, entries whose timestamp falls in
|
|
123
|
-
# [
|
|
124
|
-
# (gaps between recorded windows, or fully outside any window)
|
|
125
|
-
# into `leftover` and runs through the existing heuristic
|
|
126
|
-
#
|
|
137
|
+
# [override_start_or_R-5h, R) go into recorded_buckets[R]. Everything
|
|
138
|
+
# else (gaps between recorded windows, or fully outside any window)
|
|
139
|
+
# drops into `leftover` and runs through the existing heuristic
|
|
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.
|
|
127
149
|
recorded_buckets: dict[dt.datetime, list[UsageEntry]] = {
|
|
128
150
|
R: [] for R in recorded_windows
|
|
129
151
|
}
|
|
@@ -132,7 +154,8 @@ def _group_entries_into_blocks(
|
|
|
132
154
|
idx = bisect.bisect_right(recorded_windows, entry.timestamp)
|
|
133
155
|
if idx < len(recorded_windows):
|
|
134
156
|
R = recorded_windows[idx]
|
|
135
|
-
|
|
157
|
+
bucket_start = block_start_overrides.get(R, R - BLOCK_DURATION)
|
|
158
|
+
if bucket_start <= entry.timestamp:
|
|
136
159
|
recorded_buckets[R].append(entry)
|
|
137
160
|
continue
|
|
138
161
|
leftover.append(entry)
|
|
@@ -193,7 +216,11 @@ def _group_entries_into_blocks(
|
|
|
193
216
|
bucket = recorded_buckets[R]
|
|
194
217
|
if not bucket:
|
|
195
218
|
continue
|
|
196
|
-
|
|
219
|
+
# Display start: override when present (credit-truncated
|
|
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)
|
|
197
224
|
end_time = R
|
|
198
225
|
bucket_sorted = sorted(bucket, key=lambda e: e.timestamp)
|
|
199
226
|
blk = _build_activity_block(
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -60,6 +60,19 @@ class DoctorState:
|
|
|
60
60
|
# skipped/failed/pending or (b) a buggy writer slipped through
|
|
61
61
|
# after the migration ran.
|
|
62
62
|
forked_bucket_counts: Optional[dict]
|
|
63
|
+
# v1.7.2 credited-week tracking. Each entry is a dict with:
|
|
64
|
+
# * ``week_start_date`` — the credited week's bucket key
|
|
65
|
+
# * ``latest_weekly_percent`` — most recent weekly_percent for that
|
|
66
|
+
# week (used to gate the WARN — a credit + 0% means the user
|
|
67
|
+
# hasn't started the new segment yet, which is the EXPECTED
|
|
68
|
+
# state and shouldn't warn)
|
|
69
|
+
# * ``post_credit_milestone_count`` — count of percent_milestones
|
|
70
|
+
# rows with ``reset_event_id`` matching the credit event for
|
|
71
|
+
# this week
|
|
72
|
+
# None means the stats.db couldn't be opened to gather; check
|
|
73
|
+
# degrades to OK rather than FAIL (consistent with the rest of
|
|
74
|
+
# the doctor kernel's degradation posture).
|
|
75
|
+
credited_weeks: Optional[list[dict]]
|
|
63
76
|
codex_entries_count: Optional[int]
|
|
64
77
|
codex_last_entry_at: Optional[dt.datetime]
|
|
65
78
|
codex_jsonl_present: bool
|
|
@@ -559,6 +572,71 @@ def _check_data_forked_buckets(s: DoctorState) -> CheckResult:
|
|
|
559
572
|
|
|
560
573
|
|
|
561
574
|
|
|
575
|
+
def _check_data_post_credit_milestones(s: DoctorState) -> CheckResult:
|
|
576
|
+
"""Invariant: for every week with a ``week_reset_events`` row whose
|
|
577
|
+
``effective_reset_at_utc`` is in the past AND latest_weekly_percent
|
|
578
|
+
>= 1.0, the percent_milestones ledger should have at least one row
|
|
579
|
+
in the credit's segment.
|
|
580
|
+
|
|
581
|
+
Pre-v1.7.2 the milestone writer didn't know about segments, so a
|
|
582
|
+
credited week could have a non-empty pre-credit ledger but zero
|
|
583
|
+
post-credit rows even after the user's usage climbed past 1%. This
|
|
584
|
+
check surfaces that drift as a WARN (informational; no remediation
|
|
585
|
+
— the next ``record-usage`` tick at >=1% will self-heal via the
|
|
586
|
+
segment-aware probe).
|
|
587
|
+
|
|
588
|
+
OK (silent) when:
|
|
589
|
+
* No credited weeks exist (state.credited_weeks is empty/None).
|
|
590
|
+
* Every credited week has at least one post-credit milestone row
|
|
591
|
+
OR latest_weekly_percent < 1.0 (= "new segment not started yet,
|
|
592
|
+
which is expected on a fresh credit").
|
|
593
|
+
"""
|
|
594
|
+
weeks = s.credited_weeks
|
|
595
|
+
if weeks is None:
|
|
596
|
+
# Gather failed (stats.db open error). Don't double-warn; the
|
|
597
|
+
# db.stats.file check already covers DB-open issues.
|
|
598
|
+
return CheckResult(
|
|
599
|
+
id="data.post_credit_milestones",
|
|
600
|
+
title="Post-credit milestones",
|
|
601
|
+
severity="ok",
|
|
602
|
+
summary="no data",
|
|
603
|
+
remediation=None,
|
|
604
|
+
details={"reason": "credited_weeks gather returned None"},
|
|
605
|
+
)
|
|
606
|
+
stuck = [
|
|
607
|
+
w for w in weeks
|
|
608
|
+
if float(w.get("latest_weekly_percent") or 0.0) >= 1.0
|
|
609
|
+
and int(w.get("post_credit_milestone_count") or 0) == 0
|
|
610
|
+
]
|
|
611
|
+
if not stuck:
|
|
612
|
+
return CheckResult(
|
|
613
|
+
id="data.post_credit_milestones",
|
|
614
|
+
title="Post-credit milestones",
|
|
615
|
+
severity="ok",
|
|
616
|
+
summary=(
|
|
617
|
+
f"{len(weeks)} credited week(s); all tracked"
|
|
618
|
+
if weeks else "no credited weeks"
|
|
619
|
+
),
|
|
620
|
+
remediation=None,
|
|
621
|
+
details={"credited_weeks": len(weeks)},
|
|
622
|
+
)
|
|
623
|
+
starts = ", ".join(sorted(w["week_start_date"] for w in stuck))
|
|
624
|
+
return CheckResult(
|
|
625
|
+
id="data.post_credit_milestones",
|
|
626
|
+
title="Post-credit milestones",
|
|
627
|
+
severity="warn",
|
|
628
|
+
summary=(
|
|
629
|
+
f"{len(stuck)} credited week(s) with no post-credit milestone "
|
|
630
|
+
f"crossings yet: {starts}"
|
|
631
|
+
),
|
|
632
|
+
remediation=None,
|
|
633
|
+
details={
|
|
634
|
+
"stuck_week_count": len(stuck),
|
|
635
|
+
"stuck_week_starts": [w["week_start_date"] for w in stuck],
|
|
636
|
+
},
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
562
640
|
_LOOPBACK_HOSTS = frozenset({"loopback", "127.0.0.1", "::1", "localhost"})
|
|
563
641
|
|
|
564
642
|
|
|
@@ -786,6 +864,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
786
864
|
("data.cache_sync_state", "_check_data_cache_sync_state"),
|
|
787
865
|
("data.codex_cache", "_check_data_codex_cache"),
|
|
788
866
|
("data.forked_buckets", "_check_data_forked_buckets"),
|
|
867
|
+
("data.post_credit_milestones", "_check_data_post_credit_milestones"),
|
|
789
868
|
)),
|
|
790
869
|
("safety", "Safety", (
|
|
791
870
|
("safety.dashboard_bind", "_check_safety_dashboard_bind"),
|
package/bin/_lib_render.py
CHANGED
|
@@ -2671,6 +2671,12 @@ def _five_hour_blocks_to_json(
|
|
|
2671
2671
|
"sevenDayPctAtBlockEnd": p_end,
|
|
2672
2672
|
"sevenDayPctDeltaPp": delta,
|
|
2673
2673
|
"crossedSevenDayReset": crossed,
|
|
2674
|
+
# Spec §5.1 — in-place credit events for this 5h block, in
|
|
2675
|
+
# ascending effective_reset_at order. Empty list when the
|
|
2676
|
+
# block carries no credit detections. Source: ``__credits``
|
|
2677
|
+
# side-channel attached by ``cmd_five_hour_blocks`` via
|
|
2678
|
+
# ``credits_by_window``.
|
|
2679
|
+
"credits": d.get("__credits") or [],
|
|
2674
2680
|
}
|
|
2675
2681
|
if breakdown_axis == "model":
|
|
2676
2682
|
out["modelBreakdowns"] = [
|
|
@@ -2752,6 +2758,20 @@ def _render_five_hour_blocks_table(
|
|
|
2752
2758
|
formatted_start = _format_block_start(d["block_start_at"], args._resolved_tz)
|
|
2753
2759
|
if crossed:
|
|
2754
2760
|
formatted_start = f"⚡ {formatted_start}"
|
|
2761
|
+
# Spec §5.1 — credit chip suffix. When the block carries one or
|
|
2762
|
+
# more in-place credit events (5h credit detection v1.7.x;
|
|
2763
|
+
# __credits side-channel set by ``cmd_five_hour_blocks``), append
|
|
2764
|
+
# a ⚡-prefixed chip listing the per-credit delta-pp. Multiple
|
|
2765
|
+
# credits in the same block concatenate (e.g.
|
|
2766
|
+
# `⚡ credited -20pp, -8pp`). The chip text sits AFTER the
|
|
2767
|
+
# block-start cell's existing crossed-reset glyph so the two
|
|
2768
|
+
# signals visually disambiguate by position (crossed-reset
|
|
2769
|
+
# prefix vs. credit suffix). Both use ⚡ — different semantics,
|
|
2770
|
+
# different positions.
|
|
2771
|
+
credits = d.get("__credits") or []
|
|
2772
|
+
if credits:
|
|
2773
|
+
deltas = ", ".join(f"{c['deltaPp']:+.0f}pp" for c in credits)
|
|
2774
|
+
formatted_start = f"{formatted_start} ⚡ credited {deltas}"
|
|
2755
2775
|
rows.append([
|
|
2756
2776
|
formatted_start,
|
|
2757
2777
|
"ACTIVE" if d["__is_active"] else "closed",
|