cctally 1.117.0 → 1.117.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 +16 -0
- package/bin/_cctally_dashboard.py +39 -10
- package/bin/_cctally_dashboard_envelope.py +10 -0
- package/bin/_cctally_five_hour.py +57 -4
- package/bin/_cctally_forecast.py +272 -0
- package/bin/_cctally_milestone_history.py +379 -91
- package/bin/_cctally_tui.py +362 -33
- package/bin/_lib_dashboard_sources.py +94 -8
- package/bin/_lib_quota_model.py +29 -11
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +1 -1
- package/bin/cctally +4 -0
- package/dashboard/static/assets/{ConversationsView-Dcfn8T6f.js → ConversationsView-oqNsWzWV.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-BqmXbnhn.js → DoctorModal-B0DnU9d9.js} +1 -1
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B-HZkDnT.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-QhJkHB7i.js → SourceDetailModal-BleDpQqG.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CkXEIQcK.js → UpdateModal-CMqaAkSK.js} +1 -1
- package/dashboard/static/assets/{index-HKS1s56o.js → index-BjQV-0-D.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-BNGaNPCp.js → outlineNavigation-CO-VWBm7.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +1 -1
- package/dashboard/static/assets/ModalRoot-Ck3WJK2V.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.117.2] - 2026-11-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- With more than one Claude account, the `cctally dashboard` Current Week card and modal show one account's cycle, milestones, spend and `$/1%` beside that account's label, and the older/newer cycle buttons stay on that account.
|
|
12
|
+
- `cctally five-hour-breakdown` and the dashboard's five-hour milestones show `—` (`null` in `--json`) instead of a weekly percentage that a weekly credit later retired.
|
|
13
|
+
- With more than one Claude account, the `cctally dashboard` All view shows its combined spend headline, summing each account's current cycle, instead of usually leaving it out.
|
|
14
|
+
- Under Python 3.11, `cctally quota --json` no longer reports family shares and calibration figures that differ in their last digits from newer Python versions.
|
|
15
|
+
|
|
16
|
+
## [1.117.1] - 2026-11-03
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- With more than one Claude account, the `cctally dashboard` Current Week card and modal show one account's cycle, milestones, spend and `$/1%` beside that account's label, and the older/newer cycle buttons stay on that account.
|
|
20
|
+
- `cctally five-hour-breakdown` and the dashboard's five-hour milestones show `—` (`null` in `--json`) instead of a weekly percentage that a weekly credit later retired.
|
|
21
|
+
- With more than one Claude account, the `cctally dashboard` All view shows its combined spend headline, summing each account's current cycle, instead of usually leaving it out.
|
|
22
|
+
- Under Python 3.11, `cctally quota --json` no longer reports family shares and calibration figures that differ in their last digits from newer Python versions.
|
|
23
|
+
|
|
8
24
|
## [1.117.0] - 2026-11-02
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -11544,12 +11544,20 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
11544
11544
|
assembly runs inside one transaction per DB. 400 malformed key/source;
|
|
11545
11545
|
404 ``{code: "unknown_key", reason}`` for keys that don't resolve.
|
|
11546
11546
|
|
|
11547
|
-
``?account=<account_key|unattributed>`` (#416
|
|
11548
|
-
cycle on one account: its own crossings, its own spend.
|
|
11549
|
-
shipped merged response and is byte-identical — the
|
|
11550
|
-
qualifier only under account focus
|
|
11547
|
+
``?account=<account_key|unattributed>`` (#416 Codex; #836 Claude)
|
|
11548
|
+
focuses the cycle on one account: its own crossings, its own spend.
|
|
11549
|
+
ABSENT is the shipped merged response and is byte-identical — the
|
|
11550
|
+
client sends the qualifier only under account focus (Codex) or for the
|
|
11551
|
+
displayed account (Claude), exactly as `/api/source/` does. The
|
|
11551
11552
|
vendor-wide ``*`` sentinel is rejected here (unlike `/api/source/`,
|
|
11552
11553
|
where it labels an alert row): it names no cycle to render.
|
|
11554
|
+
|
|
11555
|
+
Claude (#836 spec 5.6) accepts the qualifier only while Claude is
|
|
11556
|
+
decorated (more than one REAL account), checked in the same read
|
|
11557
|
+
transaction as the assembly: 400 on an undecorated store, 404
|
|
11558
|
+
``unknown_key`` for an unknown or non-Claude account or a key outside
|
|
11559
|
+
that account's scoped index, and never a retry against the merged
|
|
11560
|
+
index.
|
|
11553
11561
|
"""
|
|
11554
11562
|
import re as _re
|
|
11555
11563
|
import urllib.parse as _urlparse
|
|
@@ -11576,11 +11584,6 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
11576
11584
|
):
|
|
11577
11585
|
self._send_milestones_json(400, {"error": "invalid account"})
|
|
11578
11586
|
return
|
|
11579
|
-
if source != "codex":
|
|
11580
|
-
# Claude weeks are not account-partitioned on this route yet;
|
|
11581
|
-
# accepting the qualifier silently would be a privacy lie.
|
|
11582
|
-
self._send_milestones_json(400, {"error": "invalid account"})
|
|
11583
|
-
return
|
|
11584
11587
|
account_key = raw_account[0]
|
|
11585
11588
|
c = sys.modules["cctally"]
|
|
11586
11589
|
try:
|
|
@@ -11592,12 +11595,38 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
11592
11595
|
self._send_milestones_json(400, {"error": "invalid cycle key"})
|
|
11593
11596
|
return
|
|
11594
11597
|
conn = open_db()
|
|
11598
|
+
rejected = False
|
|
11599
|
+
detail = None
|
|
11595
11600
|
try:
|
|
11596
11601
|
conn.execute("BEGIN")
|
|
11597
|
-
|
|
11602
|
+
if account_key is None:
|
|
11603
|
+
detail = c.build_claude_week_detail(conn, key)
|
|
11604
|
+
else:
|
|
11605
|
+
# #836 spec 5.6. Only a DECORATED store partitions
|
|
11606
|
+
# Claude history by account. On an undecorated one the
|
|
11607
|
+
# qualifier is refused, never ignored: answering it with
|
|
11608
|
+
# the merged week would claim an account scope the
|
|
11609
|
+
# payload does not have, which is the privacy lie the
|
|
11610
|
+
# earlier blanket rejection guarded against. Checked
|
|
11611
|
+
# inside this transaction so the gate and the assembly
|
|
11612
|
+
# read one state.
|
|
11613
|
+
import _cctally_account
|
|
11614
|
+
if not _cctally_account.provider_is_decorated(
|
|
11615
|
+
conn, "claude"):
|
|
11616
|
+
rejected = True
|
|
11617
|
+
elif account_key == "unattributed" or conn.execute(
|
|
11618
|
+
"SELECT 1 FROM accounts WHERE account_key = ? "
|
|
11619
|
+
"AND provider = 'claude'",
|
|
11620
|
+
(account_key,)).fetchone() is not None:
|
|
11621
|
+
detail = c.build_claude_week_detail(
|
|
11622
|
+
conn, key, account_key=account_key,
|
|
11623
|
+
now_utc=_command_as_of())
|
|
11598
11624
|
conn.commit()
|
|
11599
11625
|
finally:
|
|
11600
11626
|
conn.close()
|
|
11627
|
+
if rejected:
|
|
11628
|
+
self._send_milestones_json(400, {"error": "invalid account"})
|
|
11629
|
+
return
|
|
11601
11630
|
if detail is None:
|
|
11602
11631
|
self._send_milestones_json(404, {
|
|
11603
11632
|
"error": "unknown week", "code": "unknown_key",
|
|
@@ -2557,6 +2557,16 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2557
2557
|
# PREVIEW badge when set.
|
|
2558
2558
|
**sys.modules["_cctally_dashboard"]._channel_env_fragment(),
|
|
2559
2559
|
}
|
|
2560
|
+
# #836 (spec 5.1): the displayed account, decorated builds ONLY. Below the
|
|
2561
|
+
# R8 threshold both keys are omitted (never null), so an undecorated
|
|
2562
|
+
# `current_week` keeps its bytes. Appended after the literal so the key
|
|
2563
|
+
# order of every existing field is unchanged. This identity — never
|
|
2564
|
+
# `five_hour_block.account_key`, absent with no live block — is what the
|
|
2565
|
+
# client labels and scopes its history navigation by.
|
|
2566
|
+
if (cw is not None and getattr(cw, "account_decorated", False)
|
|
2567
|
+
and isinstance(envelope.get("current_week"), dict)):
|
|
2568
|
+
envelope["current_week"]["account_key"] = cw.account_key
|
|
2569
|
+
envelope["current_week"]["account_label"] = cw.account_label
|
|
2560
2570
|
envelope.update(_source_bundle_to_envelope(getattr(snap, "source_bundle", None)))
|
|
2561
2571
|
_overlay_claude_source_session_titles(envelope, snap, transcripts_visible)
|
|
2562
2572
|
_overlay_codex_source_session_labels(envelope, snap, transcripts_visible)
|
|
@@ -44,6 +44,7 @@ import sqlite3
|
|
|
44
44
|
import sys
|
|
45
45
|
from dataclasses import dataclass
|
|
46
46
|
|
|
47
|
+
import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
|
|
47
48
|
import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
|
|
48
49
|
import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
|
|
49
50
|
from _lib_five_hour import latest_five_hour_reading
|
|
@@ -1086,13 +1087,57 @@ def _credit_aware_block_weekly_axes(conn, block, *, now_utc, cache=None):
|
|
|
1086
1087
|
block.get("seven_day_pct_at_block_end"))
|
|
1087
1088
|
|
|
1088
1089
|
|
|
1090
|
+
def _weekly_retired_sql(conn, alias: str) -> "str | None":
|
|
1091
|
+
"""The suppression-retired predicate over ``alias``, or None on a store
|
|
1092
|
+
without the held column (#836, spec 5.4).
|
|
1093
|
+
|
|
1094
|
+
A held WRITE stores ``"weeklyObservationHeld": true`` in ``payload_json``
|
|
1095
|
+
beside its effective basis value (`_cctally_record`, the held branch of
|
|
1096
|
+
the snapshot write). A weekly credit's cleanup instead turns an UNHELD row,
|
|
1097
|
+
whose payload never carried that marker, into a held one and leaves its
|
|
1098
|
+
stale `weekly_percent` in place (`_delete_doomed_snapshot_rows`, replay
|
|
1099
|
+
`_cctally_journal._retire_weekly_effects`). The marker therefore separates
|
|
1100
|
+
the two exactly. A store without the column cannot hold a held row, the
|
|
1101
|
+
reason `_cctally_core.weekly_held_exclusion` omits its own predicate there.
|
|
1102
|
+
"""
|
|
1103
|
+
if not _cctally_core.weekly_held_exclusion(conn):
|
|
1104
|
+
return None
|
|
1105
|
+
return (f"({alias}.weekly_observation_held = 1 AND "
|
|
1106
|
+
f"json_extract({alias}.payload_json, '$.weeklyObservationHeld') "
|
|
1107
|
+
f"IS NOT 1)")
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def effective_weekly_value_sql(conn, alias: str = "s") -> str:
|
|
1111
|
+
"""The joined snapshot's EFFECTIVE weekly value (#836, spec 5.4): NULL for
|
|
1112
|
+
a suppression-retired row, its stored `weekly_percent` otherwise. A
|
|
1113
|
+
clamped held write and a genuine climb-back keep their values; no
|
|
1114
|
+
percentage band is involved."""
|
|
1115
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1116
|
+
if retired is None:
|
|
1117
|
+
return f"{alias}.weekly_percent"
|
|
1118
|
+
return f"CASE WHEN {retired} THEN NULL ELSE {alias}.weekly_percent END"
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def weekly_join_unavailable_sql(conn, alias: str = "s") -> str:
|
|
1122
|
+
"""A five-hour milestone's effective weekly join is UNAVAILABLE (#836,
|
|
1123
|
+
spec 5.4): the joined snapshot is absent — deleted, or the ``0`` sentinel
|
|
1124
|
+
`trg_usage_snapshot_suppression` writes — or suppression-retired. The
|
|
1125
|
+
shared predicate of the detail stamp and the dashboard's retired-join
|
|
1126
|
+
signature leg, over a ``LEFT JOIN`` aliased ``alias``."""
|
|
1127
|
+
retired = _weekly_retired_sql(conn, alias)
|
|
1128
|
+
if retired is None:
|
|
1129
|
+
return f"({alias}.id IS NULL)"
|
|
1130
|
+
return f"({alias}.id IS NULL OR {retired})"
|
|
1131
|
+
|
|
1132
|
+
|
|
1089
1133
|
#: The six `five_hour_milestones` columns every surface already published, plus
|
|
1090
|
-
#: the joined EFFECTIVE weekly value
|
|
1091
|
-
#:
|
|
1134
|
+
#: the joined EFFECTIVE weekly value (`effective_weekly_value_sql`, filled in
|
|
1135
|
+
#: as ``{effective}``). Ordering is `captured_at_utc, id` — spec §5.2's
|
|
1136
|
+
#: merged-stream order, which post-credit threshold repeats depend on.
|
|
1092
1137
|
_FIVE_HOUR_MILESTONE_SQL = """
|
|
1093
1138
|
SELECT m.percent_threshold, m.captured_at_utc, m.block_cost_usd,
|
|
1094
1139
|
m.marginal_cost_usd, m.seven_day_pct_at_crossing, m.reset_event_id,
|
|
1095
|
-
|
|
1140
|
+
{effective} AS effective_seven_day_pct_at_crossing
|
|
1096
1141
|
FROM five_hour_milestones m
|
|
1097
1142
|
LEFT JOIN weekly_usage_snapshots s ON s.id = m.usage_snapshot_id
|
|
1098
1143
|
WHERE {selector}
|
|
@@ -1139,6 +1184,12 @@ def _load_five_hour_milestones(
|
|
|
1139
1184
|
store is corrected by `db rederive`'s reviewed `retract`. A missing joined
|
|
1140
1185
|
snapshot still publishes None and never falls back to the raw value.
|
|
1141
1186
|
|
|
1187
|
+
**A suppression-retired snapshot publishes None** (#836, spec 5.4). The
|
|
1188
|
+
fold rejected its weekly value, and the cleanup kept the row only for its
|
|
1189
|
+
five-hour evidence; its provenance is exact (no held-write marker on a
|
|
1190
|
+
held row), so this is not the withdrawn band. See
|
|
1191
|
+
`effective_weekly_value_sql`.
|
|
1192
|
+
|
|
1142
1193
|
SELECTOR. Exactly one of `block_id` or `five_hour_window_key` must be given.
|
|
1143
1194
|
`block_id` is the precise form — `five_hour_milestones.block_id` names one
|
|
1144
1195
|
block outright. The pair form filters on the window key AND the account,
|
|
@@ -1166,7 +1217,9 @@ def _load_five_hour_milestones(
|
|
|
1166
1217
|
selector += " AND m.account_key = ?"
|
|
1167
1218
|
params += (account_key,)
|
|
1168
1219
|
rows = conn.execute(
|
|
1169
|
-
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1220
|
+
_FIVE_HOUR_MILESTONE_SQL.format(
|
|
1221
|
+
selector=selector,
|
|
1222
|
+
effective=effective_weekly_value_sql(conn, "s")), params,
|
|
1170
1223
|
).fetchall()
|
|
1171
1224
|
out: list[dict] = []
|
|
1172
1225
|
for r in rows:
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -52,6 +52,12 @@ from _cctally_core import (
|
|
|
52
52
|
_BudgetConfigError, _get_budget_config,
|
|
53
53
|
BUDGET_PERIODS as _CCTALLY_BUDGET_PERIODS,
|
|
54
54
|
)
|
|
55
|
+
# #836: the decorated Current Week's owner and cycle identity read the reset
|
|
56
|
+
# chokepoint and the held-column guard directly; neither is ns-patched.
|
|
57
|
+
from _cctally_core import (
|
|
58
|
+
_canonicalize_optional_iso, _latest_reset_event_for_end,
|
|
59
|
+
weekly_held_exclusion,
|
|
60
|
+
)
|
|
55
61
|
|
|
56
62
|
import importlib.util as _ilu
|
|
57
63
|
|
|
@@ -389,6 +395,272 @@ def _apply_midweek_reset_override(
|
|
|
389
395
|
return week_start_at, samples
|
|
390
396
|
|
|
391
397
|
|
|
398
|
+
# === #836: the decorated Current Week — one owner, one cycle identity =======
|
|
399
|
+
#
|
|
400
|
+
# On a store with more than one REAL Claude account (`provider_is_decorated`)
|
|
401
|
+
# the Current Week card, its weekly milestones and the history index's current
|
|
402
|
+
# entry describe ONE account: the owner of the newest non-held weekly sample at
|
|
403
|
+
# the build's as-of instant (#839's contract, spec 5.2). The merged reads above
|
|
404
|
+
# stay unchanged for every undecorated store and every other caller.
|
|
405
|
+
|
|
406
|
+
#: Every subscription window that can contain an instant starts no more than
|
|
407
|
+
#: seven days (plus host-offset slack on `week_start_date`) before it, so this
|
|
408
|
+
#: leading `week_start_date` bound keeps every candidate read an
|
|
409
|
+
#: `idx_usage_week_time` range rather than a scan of the whole history.
|
|
410
|
+
_CLAUDE_CYCLE_CANDIDATE_DAYS = 8
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@dataclass(frozen=True)
|
|
414
|
+
class ClaudeCycleScope:
|
|
415
|
+
"""The decorated Current Week's cycle identity (#836, spec 5.2 step 2).
|
|
416
|
+
|
|
417
|
+
Resolved once per build by `_claude_current_cycle_scope` and shared by the
|
|
418
|
+
card, the weekly milestones and the scoped history index, so the three
|
|
419
|
+
readers of one cycle cannot choose it differently.
|
|
420
|
+
|
|
421
|
+
``account_key``: the owner. ``as_of_utc``: the build instant (ISO-8601
|
|
422
|
+
``Z``); reset events are admitted only at or before it.
|
|
423
|
+
``week_start_date``: the storage key milestones are recorded under.
|
|
424
|
+
``week_start_at``: the owner's stored boundary text (``None`` on the
|
|
425
|
+
date-only leg). ``week_end_at``: the canonical end
|
|
426
|
+
(`_canonicalize_optional_iso`, the `week_reset_events.new_week_end_at`
|
|
427
|
+
form; ``None`` on the date-only leg). ``effective_start_at``: the week
|
|
428
|
+
start, or the active reset's effective instant when later (ISO-8601 ``Z``;
|
|
429
|
+
``None`` on the date-only leg). ``reset_event_id``: the active reset's id
|
|
430
|
+
(the milestone segment), ``0`` when none.
|
|
431
|
+
"""
|
|
432
|
+
account_key: str
|
|
433
|
+
as_of_utc: str
|
|
434
|
+
week_start_date: str
|
|
435
|
+
week_start_at: "str | None"
|
|
436
|
+
week_end_at: "str | None"
|
|
437
|
+
effective_start_at: "str | None"
|
|
438
|
+
reset_event_id: int
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _claude_newest_weekly_sample(conn, now_utc: dt.datetime, *,
|
|
442
|
+
account_key: "str | None" = None):
|
|
443
|
+
"""The newest non-held weekly sample captured at or before ``now_utc``
|
|
444
|
+
among windows containing it, or ``None`` (#836, spec 5.2 step 1).
|
|
445
|
+
|
|
446
|
+
Only non-held rows take part, so a newer held row of another account (a
|
|
447
|
+
copy of an older weekly value under a newer capture) never chooses the
|
|
448
|
+
window — the defect of the held-inclusive ``MAX(captured_at_utc)`` window
|
|
449
|
+
choice in `_fetch_current_week_snapshots` (spec D-C). Captures after
|
|
450
|
+
``now_utc`` never participate. Boundaryful windows
|
|
451
|
+
(``[week_start_at, week_end_at)`` containing ``now_utc``) are preferred;
|
|
452
|
+
when none qualifies, the date-only leg (``week_start_date`` ..
|
|
453
|
+
``week_end_date`` containing today's host-local date) applies, as in
|
|
454
|
+
`_fetch_current_week_snapshots`. Ties break by ``(captured_at_utc, id)``.
|
|
455
|
+
|
|
456
|
+
Returns a dict of the row's identity and boundary columns plus
|
|
457
|
+
``boundaryful``: whether the CHOSEN row carries both parseable bounds,
|
|
458
|
+
never which pool it came from. On the window's end date, after its reset
|
|
459
|
+
instant and before any next-window sample, a bounded row reaches the
|
|
460
|
+
date-only pool; its scope is still built from its own bounds, so the
|
|
461
|
+
account's own reset stays in effect (#836 review F1).
|
|
462
|
+
"""
|
|
463
|
+
floor_date = (now_utc.astimezone(dt.timezone.utc).date()
|
|
464
|
+
- dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
|
|
465
|
+
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
466
|
+
acct_p: tuple = () if account_key is None else (account_key,)
|
|
467
|
+
rows = conn.execute(
|
|
468
|
+
"SELECT id, account_key, week_start_date, week_end_date, "
|
|
469
|
+
" week_start_at, week_end_at, captured_at_utc "
|
|
470
|
+
"FROM weekly_usage_snapshots "
|
|
471
|
+
"WHERE week_start_date >= ?" + acct_pred
|
|
472
|
+
+ weekly_held_exclusion(conn),
|
|
473
|
+
(floor_date,) + acct_p,
|
|
474
|
+
).fetchall()
|
|
475
|
+
# internal fallback: host-local intentional (the date-only leg's day,
|
|
476
|
+
# matching `_fetch_current_week_snapshots`)
|
|
477
|
+
today_local = now_utc.astimezone().date().isoformat()
|
|
478
|
+
boundaryful = []
|
|
479
|
+
date_only = []
|
|
480
|
+
for r in rows:
|
|
481
|
+
try:
|
|
482
|
+
cap = parse_iso_datetime(r[6], "captured_at_utc")
|
|
483
|
+
except (TypeError, ValueError):
|
|
484
|
+
continue
|
|
485
|
+
if cap > now_utc:
|
|
486
|
+
continue
|
|
487
|
+
rank = (cap, int(r[0]))
|
|
488
|
+
try:
|
|
489
|
+
ws = parse_iso_datetime(r[4], "week_start_at") if r[4] else None
|
|
490
|
+
we = parse_iso_datetime(r[5], "week_end_at") if r[5] else None
|
|
491
|
+
except ValueError:
|
|
492
|
+
ws = we = None
|
|
493
|
+
bounded = ws is not None and we is not None
|
|
494
|
+
if bounded and ws <= now_utc < we:
|
|
495
|
+
boundaryful.append((rank, r, True))
|
|
496
|
+
elif (r[2] is not None and r[3] is not None
|
|
497
|
+
and r[2] <= today_local <= r[3]):
|
|
498
|
+
date_only.append((rank, r, bounded))
|
|
499
|
+
pool = boundaryful or date_only
|
|
500
|
+
if not pool:
|
|
501
|
+
return None
|
|
502
|
+
_rank, r, bounded = max(pool, key=lambda item: item[0])
|
|
503
|
+
return {
|
|
504
|
+
"id": int(r[0]),
|
|
505
|
+
"account_key": r[1],
|
|
506
|
+
"week_start_date": r[2],
|
|
507
|
+
"week_end_date": r[3],
|
|
508
|
+
"week_start_at": r[4],
|
|
509
|
+
"week_end_at": r[5],
|
|
510
|
+
"captured_at_utc": r[6],
|
|
511
|
+
"boundaryful": bounded,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _claude_select_current_owner(conn, now_utc: dt.datetime) -> "str | None":
|
|
516
|
+
"""The displayed account: the owner of the newest non-held weekly sample
|
|
517
|
+
at ``now_utc`` (#836, spec 5.2 step 1), or ``None`` when there is none."""
|
|
518
|
+
sample = _claude_newest_weekly_sample(conn, now_utc)
|
|
519
|
+
return None if sample is None else sample["account_key"]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _claude_current_cycle_scope(conn, now_utc: dt.datetime, *,
|
|
523
|
+
account_key: str) -> "ClaudeCycleScope | None":
|
|
524
|
+
"""``account_key``'s current cycle identity at ``now_utc`` (#836, spec 5.2
|
|
525
|
+
step 2), or ``None`` when the account has no eligible non-held sample.
|
|
526
|
+
|
|
527
|
+
The window is the one holding the account's newest eligible non-held
|
|
528
|
+
sample. The active reset is `_latest_reset_event_for_end` for the
|
|
529
|
+
canonical end, scoped to the account and admitted only at or before
|
|
530
|
+
``now_utc`` — the #750 S3 chokepoint's effective-instant order — so another
|
|
531
|
+
account's credit never re-anchors this cycle (spec D-A) and a reset that
|
|
532
|
+
takes effect later does not cut it yet. The effective start is that
|
|
533
|
+
reset's instant exactly when the history index cuts the account's storage
|
|
534
|
+
bucket there (`_claude_reset_cuts_cycle`, the one rule both use) — which
|
|
535
|
+
can be EARLIER than the nominal week start after a same-date early
|
|
536
|
+
re-anchor (#836 review F7). The date-only leg has no end text, so it
|
|
537
|
+
carries no reset.
|
|
538
|
+
"""
|
|
539
|
+
sample = _claude_newest_weekly_sample(conn, now_utc, account_key=account_key)
|
|
540
|
+
if sample is None:
|
|
541
|
+
return None
|
|
542
|
+
as_of = _iso_z(now_utc)
|
|
543
|
+
if not sample["boundaryful"]:
|
|
544
|
+
return ClaudeCycleScope(
|
|
545
|
+
account_key=account_key, as_of_utc=as_of,
|
|
546
|
+
week_start_date=sample["week_start_date"],
|
|
547
|
+
week_start_at=None, week_end_at=None, effective_start_at=None,
|
|
548
|
+
reset_event_id=0)
|
|
549
|
+
week_start = parse_iso_datetime(sample["week_start_at"], "week_start_at")
|
|
550
|
+
canon_end = _canonicalize_optional_iso(sample["week_end_at"],
|
|
551
|
+
"claude.cycle.end")
|
|
552
|
+
effective_start = week_start
|
|
553
|
+
reset_event_id = 0
|
|
554
|
+
if canon_end:
|
|
555
|
+
event = _latest_reset_event_for_end(
|
|
556
|
+
conn, canon_end, account_key=account_key, as_of_utc=as_of)
|
|
557
|
+
if event is not None:
|
|
558
|
+
reset_event_id = int(event["id"])
|
|
559
|
+
if event["effective_reset_at_utc"]:
|
|
560
|
+
try:
|
|
561
|
+
reset_dt = parse_iso_datetime(
|
|
562
|
+
event["effective_reset_at_utc"], "reset_event.effective")
|
|
563
|
+
except ValueError:
|
|
564
|
+
reset_dt = None
|
|
565
|
+
if reset_dt is not None:
|
|
566
|
+
import _cctally_milestone_history as _mhist
|
|
567
|
+
outer = _mhist._claude_storage_boundaries(
|
|
568
|
+
conn, sample["week_start_date"],
|
|
569
|
+
account_key=account_key)
|
|
570
|
+
if _mhist._claude_reset_cuts_cycle(outer, reset_dt):
|
|
571
|
+
effective_start = reset_dt
|
|
572
|
+
return ClaudeCycleScope(
|
|
573
|
+
account_key=account_key, as_of_utc=as_of,
|
|
574
|
+
week_start_date=sample["week_start_date"],
|
|
575
|
+
week_start_at=sample["week_start_at"],
|
|
576
|
+
week_end_at=canon_end,
|
|
577
|
+
effective_start_at=_iso_z(effective_start),
|
|
578
|
+
reset_event_id=reset_event_id)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
|
|
582
|
+
now_utc: dt.datetime):
|
|
583
|
+
"""The decorated card's held-inclusive samples for ``scope`` (#836).
|
|
584
|
+
|
|
585
|
+
The shape of ``_fetch_current_week_snapshots(include_held=True,
|
|
586
|
+
include_account=True, include_scope=True)`` — ``(week_start_at,
|
|
587
|
+
week_end_at, samples, week_scope)`` — restricted to the scope's account and
|
|
588
|
+
window, so the card reads the window its owner was chosen in rather than
|
|
589
|
+
one `_fetch_current_week_snapshots` would choose by its held-inclusive
|
|
590
|
+
``MAX(captured_at_utc)``. ``week_start_at`` is the NOMINAL start; the
|
|
591
|
+
caller filters from ``scope.effective_start_at``. Samples are filtered to
|
|
592
|
+
``captured_at <= now_utc``. Returns ``None`` when the window cannot be
|
|
593
|
+
resolved.
|
|
594
|
+
"""
|
|
595
|
+
floor_date = (now_utc.astimezone(dt.timezone.utc).date()
|
|
596
|
+
- dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
|
|
597
|
+
if scope.week_start_at is None:
|
|
598
|
+
drow = conn.execute(
|
|
599
|
+
"SELECT week_end_date FROM weekly_usage_snapshots "
|
|
600
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
601
|
+
"AND week_end_date IS NOT NULL "
|
|
602
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
603
|
+
(scope.week_start_date, scope.account_key),
|
|
604
|
+
).fetchone()
|
|
605
|
+
if drow is None:
|
|
606
|
+
return None
|
|
607
|
+
# internal fallback: host-local intentional (the date-only leg)
|
|
608
|
+
local_tz = dt.datetime.now().astimezone().tzinfo
|
|
609
|
+
ws_date = dt.date.fromisoformat(scope.week_start_date)
|
|
610
|
+
we_date = dt.date.fromisoformat(drow[0])
|
|
611
|
+
week_start_at = dt.datetime.combine(
|
|
612
|
+
ws_date, dt.time(0, 0), local_tz).astimezone(dt.timezone.utc)
|
|
613
|
+
week_end_at = dt.datetime.combine(
|
|
614
|
+
we_date + dt.timedelta(days=1), dt.time(0, 0),
|
|
615
|
+
local_tz).astimezone(dt.timezone.utc)
|
|
616
|
+
rows = conn.execute(
|
|
617
|
+
"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
618
|
+
" weekly_observation_held, account_key "
|
|
619
|
+
"FROM weekly_usage_snapshots "
|
|
620
|
+
"WHERE week_start_date = ? AND account_key = ? "
|
|
621
|
+
"ORDER BY captured_at_utc ASC, id ASC",
|
|
622
|
+
(scope.week_start_date, scope.account_key),
|
|
623
|
+
).fetchall()
|
|
624
|
+
samples = _shape_week_samples(rows, include_held=True,
|
|
625
|
+
include_account=True)
|
|
626
|
+
samples = [s for s in samples if s[0] <= now_utc]
|
|
627
|
+
return (week_start_at, week_end_at, samples,
|
|
628
|
+
(scope.week_start_date, None))
|
|
629
|
+
week_start_at = parse_iso_datetime(scope.week_start_at, "week_start_at")
|
|
630
|
+
week_end_at = parse_iso_datetime(scope.week_end_at, "week_end_at")
|
|
631
|
+
# Every textual spelling of the same start instant, as the merged read
|
|
632
|
+
# gathers them for an upgrade-in-progress window.
|
|
633
|
+
matching_texts: list[str] = []
|
|
634
|
+
for (text,) in conn.execute(
|
|
635
|
+
"SELECT DISTINCT week_start_at FROM weekly_usage_snapshots "
|
|
636
|
+
"WHERE week_start_date >= ? AND account_key = ? "
|
|
637
|
+
"AND week_start_at IS NOT NULL AND week_end_at IS NOT NULL",
|
|
638
|
+
(floor_date, scope.account_key)).fetchall():
|
|
639
|
+
try:
|
|
640
|
+
if parse_iso_datetime(text, "week_start_at") == week_start_at:
|
|
641
|
+
matching_texts.append(text)
|
|
642
|
+
except ValueError:
|
|
643
|
+
continue
|
|
644
|
+
if scope.week_start_at not in matching_texts:
|
|
645
|
+
matching_texts.append(scope.week_start_at)
|
|
646
|
+
matching_texts.sort()
|
|
647
|
+
placeholders = ",".join("?" * len(matching_texts))
|
|
648
|
+
rows = _week_rows_by_capture(conn.execute(
|
|
649
|
+
f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
|
|
650
|
+
f" weekly_observation_held, account_key, id "
|
|
651
|
+
f"FROM weekly_usage_snapshots "
|
|
652
|
+
f"WHERE (week_start_at IN ({placeholders}) "
|
|
653
|
+
f" OR (week_start_at IS NULL AND week_start_date = ?)) "
|
|
654
|
+
f" AND account_key = ?",
|
|
655
|
+
tuple(matching_texts) + (scope.week_start_date, scope.account_key),
|
|
656
|
+
), by_id=True)
|
|
657
|
+
samples = _shape_week_samples(rows, include_held=True,
|
|
658
|
+
include_account=True)
|
|
659
|
+
samples = [s for s in samples if s[0] <= now_utc]
|
|
660
|
+
return (week_start_at, week_end_at, samples,
|
|
661
|
+
(scope.week_start_date, tuple(matching_texts)))
|
|
662
|
+
|
|
663
|
+
|
|
392
664
|
def _current_week_five_hour_percent(conn, now_utc, week_scope, *,
|
|
393
665
|
account_key, week_start_at,
|
|
394
666
|
week_start_before_override):
|