cctally 1.50.0 → 1.52.0
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 +18 -0
- package/bin/_cctally_core.py +76 -0
- package/bin/_cctally_dashboard.py +22 -1
- package/bin/_cctally_parser.py +45 -0
- package/bin/_cctally_project.py +46 -3
- package/bin/_cctally_record.py +519 -25
- package/bin/_cctally_statusline.py +16 -14
- package/bin/cctally +20 -0
- package/dashboard/static/assets/index-Psq3Ym5M.js +68 -0
- package/dashboard/static/assets/index-klTHJA52.css +1 -0
- package/dashboard/static/dashboard.html +3 -2
- package/dashboard/static/favicon.svg +1 -0
- package/package.json +1 -1
- package/dashboard/static/assets/index-C96dGQ7N.js +0 -68
- package/dashboard/static/assets/index-ChyJiUWl.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.52.0] - 2026-06-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Dashboard (mobile) — prev/next day stepper on the Daily modal.** On a phone the Daily modal's thirty selectable day bars don't fit, so the modal now shows ‹ prev / next › day-stepper controls at the mobile breakpoint (desktop keeps the full bar row and the existing ↑↓ keyboard nav, which the stepper shares its `stepDay()` helper with) (#214).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Dashboard (mobile) — modal & overlay correctness.** Opening any of the seven overlays (modal, share, composer, settings, update, doctor, help) now locks page scroll on both `<html>` and `<body>`, so the page no longer scrolls behind the overlay, and each overlay's own scroll region contains its overscroll. The Session and Block detail modals reflow their token grid two-up so an 8-digit CACHE READ value no longer clips, long modal titles ellipsize to one line, and the share composer preview is framed as a labeled "Preview · <theme>" document (#214).
|
|
15
|
+
- **Dashboard (mobile) — touch-target ergonomics.** Controls that fell below the 44×44px touch-target minimum at phone widths — session-list buttons, the open-conversation affordance, the sortable column header, the doctor chip, and settings checkboxes/radios — are lifted to ≥44px, and the session-row model chip is edge-anchored so it no longer steals taps meant for the row's project link (#214).
|
|
16
|
+
- **Dashboard (mobile) — single-column layout & content flow.** Panels now size to their content instead of reserving the desktop 320px floor, so sparse panels (Projects, Cache Report, Recent Alerts) stop showing large dead space; every panel header stays on one line; the Daily heatmap cell is `$`-prefixed; the session card uses a deterministic two-row grid (project + cost, then started · duration · model); and the Projects panel gives the name column the flexible track so common names are no longer clipped. All changes are gated to ≤640px, so desktop output is byte-unchanged (#214).
|
|
17
|
+
|
|
18
|
+
## [1.51.0] - 2026-06-19
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`record-credit` — manually record an in-place weekly credit the auto-detector misses.** When Anthropic lowers your 7-day usage counter mid-window by a sub-25pp, non-zero amount (e.g. 46%→31%) instead of a clean reset, neither auto-detector fires and the monotonic clamp pins every display at the stale peak. `cctally record-credit --to 31` records the credit explicitly and repairs the data while keeping the **same week** (no re-anchor): it records a clamp floor for the current week, lowers `hwm-7d`, clears stale replays, and inserts a post-credit snapshot so reports and the statusline read the credited value while the week boundaries and the forecast rate stay correct. As usage resumes, a later reading below the pre-credit peak (e.g. 37%) is stored normally rather than suppressed. Preview-and-confirm by default (`--dry-run` / `--yes`), with `--from` / `--at` / `--week` overrides, a `--force` clean re-record that never touches real history, and a `--json` (schemaVersion 1) envelope. See `docs/commands/record-credit.md`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Dashboard conversation viewer — the mobile fix that stops the sticky topbar from hiding `← Back` now holds on notched phones, and in-page panel jumps land correctly.** The mobile topbar height budget (`--topbar-h`) was a flat 104px, but the real two-row mobile header renders ~148px; the #205 reader-height fix compensated only for the no-safe-area (headless) case, so on a notched device (`env(safe-area-inset-top)`) roughly 30px of document overflow returned and the sticky topbar could again scroll over the reader's `← Back` button. `--topbar-h` is now measured safe-area-aware (`calc(142px + max(6px, env(safe-area-inset-top)))`) so it tracks the real header height at any inset — the conversation reader fills the viewport with no document overflow on every phone, and because that same variable is every panel's scroll-anchor floor, in-page jumps and `scrollIntoView()` now land panel titles flush just below the header instead of ~44px under it. Desktop is byte-identical (#206).
|
|
25
|
+
|
|
8
26
|
## [1.50.0] - 2026-06-18
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/bin/_cctally_core.py
CHANGED
|
@@ -1365,6 +1365,34 @@ def open_db() -> sqlite3.Connection:
|
|
|
1365
1365
|
"""
|
|
1366
1366
|
)
|
|
1367
1367
|
|
|
1368
|
+
# In-place weekly partial-credit floor (issue #209, record-credit M2).
|
|
1369
|
+
# Plain CREATE TABLE IF NOT EXISTS, NO migration handler / NO user_version
|
|
1370
|
+
# bump — the same framework-untracked posture as `project_budget_milestones`
|
|
1371
|
+
# above. A `record-credit` invocation records a weekly credit (e.g.
|
|
1372
|
+
# 46% -> 31%) WITHOUT writing a `week_reset_events` row: a credit lowers the
|
|
1373
|
+
# current-7d clamp floor only and must NOT re-anchor the week window (the
|
|
1374
|
+
# `week_reset_events`-driven window-resolution code would otherwise show a
|
|
1375
|
+
# spurious "new week" and corrupt the forecast rate). `_reset_aware_floor`
|
|
1376
|
+
# (below) unions this table with `week_reset_events` so the four MAX-clamp
|
|
1377
|
+
# sites floor the current % to the post-credit value while the window stays
|
|
1378
|
+
# put. `effective_at_utc` is `floor_to_hour(at)` in UTC; `applied_at_utc` is
|
|
1379
|
+
# audit-only (kept out of goldens). Lives BEFORE the migration dispatcher: a
|
|
1380
|
+
# plain CREATE on a framework-untracked table never touches
|
|
1381
|
+
# `schema_migrations`, so the dispatcher's fresh-install snapshot is
|
|
1382
|
+
# unaffected. See docs/superpowers/specs/2026-06-19-record-credit-weekly-design.md §2/§4a.
|
|
1383
|
+
conn.execute(
|
|
1384
|
+
"""
|
|
1385
|
+
CREATE TABLE IF NOT EXISTS weekly_credit_floors (
|
|
1386
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1387
|
+
week_start_date TEXT NOT NULL,
|
|
1388
|
+
effective_at_utc TEXT NOT NULL,
|
|
1389
|
+
observed_pre_credit_pct REAL NOT NULL,
|
|
1390
|
+
applied_at_utc TEXT NOT NULL,
|
|
1391
|
+
UNIQUE(week_start_date, effective_at_utc)
|
|
1392
|
+
)
|
|
1393
|
+
"""
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1368
1396
|
# Migration framework dispatcher. Replaces the prior inline gate stack
|
|
1369
1397
|
# (has_blocks + _migration_done) with the framework's _run_pending_-
|
|
1370
1398
|
# migrations entry point. See spec §2.3, §5.2 + the migration handlers
|
|
@@ -1523,6 +1551,54 @@ def _get_latest_row_for_week(
|
|
|
1523
1551
|
).fetchone()
|
|
1524
1552
|
|
|
1525
1553
|
|
|
1554
|
+
def _reset_aware_floor(
|
|
1555
|
+
conn: sqlite3.Connection,
|
|
1556
|
+
week_start_date: str,
|
|
1557
|
+
week_start_at: str,
|
|
1558
|
+
week_end_at: str,
|
|
1559
|
+
) -> str | None:
|
|
1560
|
+
"""Return the latest in-week clamp floor (an ISO timestamp) across BOTH
|
|
1561
|
+
`week_reset_events` and `weekly_credit_floors`, or None when neither has a
|
|
1562
|
+
row for this week.
|
|
1563
|
+
|
|
1564
|
+
This is the single chokepoint the four MAX-clamp sites consult to floor the
|
|
1565
|
+
current 7d % to the most-recent in-place credit / reset effective moment
|
|
1566
|
+
(record-credit M2, issue #209, spec §4a):
|
|
1567
|
+
- statusline `_hwm_clamp` 7d (bin/_cctally_statusline.py)
|
|
1568
|
+
- the record-usage write-site monotonic clamp (bin/_cctally_record.py)
|
|
1569
|
+
- `_resolve_reset_aware_hwm` (the --from default helper)
|
|
1570
|
+
- `project`'s `_load_week_snapshots` per-week MAX (bin/_cctally_project.py)
|
|
1571
|
+
|
|
1572
|
+
A `week_reset_events` row counts iff its `effective_reset_at_utc` falls in
|
|
1573
|
+
`[week_start_at, week_end_at)`; a `weekly_credit_floors` row counts iff its
|
|
1574
|
+
`week_start_date` matches (record-credit always stamps `effective_at_utc`
|
|
1575
|
+
inside the week, validated at plan-build time).
|
|
1576
|
+
|
|
1577
|
+
The latest floor wins via `ORDER BY unixepoch(floor_at) DESC LIMIT 1` —
|
|
1578
|
+
`unixepoch()`, NOT a textual `MAX(...)`: the two legs carry mixed offset
|
|
1579
|
+
spellings (`Z` / `+00:00`), and a lexical MAX would silently mis-order them
|
|
1580
|
+
on a non-UTC host (the same gotcha as the statusline clamp / 5h-block
|
|
1581
|
+
cross-reset flag; see the comment at bin/_cctally_statusline.py)."""
|
|
1582
|
+
row = conn.execute(
|
|
1583
|
+
"""
|
|
1584
|
+
SELECT floor_at FROM (
|
|
1585
|
+
SELECT effective_reset_at_utc AS floor_at
|
|
1586
|
+
FROM week_reset_events
|
|
1587
|
+
WHERE unixepoch(effective_reset_at_utc) >= unixepoch(?)
|
|
1588
|
+
AND unixepoch(effective_reset_at_utc) < unixepoch(?)
|
|
1589
|
+
UNION ALL
|
|
1590
|
+
SELECT effective_at_utc AS floor_at
|
|
1591
|
+
FROM weekly_credit_floors
|
|
1592
|
+
WHERE week_start_date = ?
|
|
1593
|
+
)
|
|
1594
|
+
ORDER BY unixepoch(floor_at) DESC
|
|
1595
|
+
LIMIT 1
|
|
1596
|
+
""",
|
|
1597
|
+
(week_start_at, week_end_at, week_start_date),
|
|
1598
|
+
).fetchone()
|
|
1599
|
+
return row[0] if row and row[0] else None
|
|
1600
|
+
|
|
1601
|
+
|
|
1526
1602
|
def get_latest_usage_for_week(
|
|
1527
1603
|
conn: sqlite3.Connection,
|
|
1528
1604
|
week_ref: WeekRef,
|
|
@@ -4988,6 +4988,18 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
4988
4988
|
"_error": f"{type(exc).__name__}: {exc}",
|
|
4989
4989
|
}
|
|
4990
4990
|
|
|
4991
|
+
# B1 (#207): the "vs last week" header delta reuses the is_current trend
|
|
4992
|
+
# row's delta_dpp ($/1% vs the previous trend row — normally the prior
|
|
4993
|
+
# subscription week). Select by the is_current FLAG, not snap.trend[-1]:
|
|
4994
|
+
# snap.trend is oldest-first by week_start_date and reset/credit handling
|
|
4995
|
+
# can synthesize an intervening row, so position -1 is not guaranteed to
|
|
4996
|
+
# be the current week (Codex P1). reversed() picks the latest if ever
|
|
4997
|
+
# multiple are flagged. Null-safe: None when no row is current, or when
|
|
4998
|
+
# the current row's delta_dpp is itself None — both hide the stat.
|
|
4999
|
+
_current_trend = next(
|
|
5000
|
+
(r for r in reversed(snap.trend) if r.is_current), None
|
|
5001
|
+
) if snap.trend else None
|
|
5002
|
+
|
|
4991
5003
|
return {
|
|
4992
5004
|
"envelope_version": 2,
|
|
4993
5005
|
"generated_at": _iso_z(snap.generated_at),
|
|
@@ -5011,7 +5023,9 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
5011
5023
|
"dollar_per_pct": dollar_pp,
|
|
5012
5024
|
"forecast_pct": header_fcast_pct,
|
|
5013
5025
|
"forecast_verdict": verdict,
|
|
5014
|
-
"vs_last_week_delta":
|
|
5026
|
+
"vs_last_week_delta": (
|
|
5027
|
+
_current_trend.delta_dpp if _current_trend is not None else None
|
|
5028
|
+
),
|
|
5015
5029
|
},
|
|
5016
5030
|
|
|
5017
5031
|
"current_week":
|
|
@@ -5387,6 +5401,13 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
5387
5401
|
if path == "/":
|
|
5388
5402
|
self._serve_static_file(self.static_dir / "dashboard.html",
|
|
5389
5403
|
"text/html; charset=utf-8")
|
|
5404
|
+
elif path == "/favicon.ico":
|
|
5405
|
+
# #207 D11 — serve the SVG favicon for the browser's default
|
|
5406
|
+
# /favicon.ico request so it stops 404-ing even absent the
|
|
5407
|
+
# <link rel="icon"> in dashboard.html. Vite copies public/ verbatim
|
|
5408
|
+
# into the build output, so favicon.svg lands under static_dir.
|
|
5409
|
+
self._serve_static_file(self.static_dir / "favicon.svg",
|
|
5410
|
+
"image/svg+xml")
|
|
5390
5411
|
elif path.startswith("/static/"):
|
|
5391
5412
|
# Defense in depth: a lexical ".." pre-check is bypassable via
|
|
5392
5413
|
# percent-encoding (e.g., %2e%2e), so the authoritative guard is the
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -1610,6 +1610,51 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1610
1610
|
)
|
|
1611
1611
|
ru.set_defaults(func=c.cmd_record_usage)
|
|
1612
1612
|
|
|
1613
|
+
rc = sub.add_parser(
|
|
1614
|
+
"record-credit",
|
|
1615
|
+
help="Record an in-place weekly credit the auto-detector misses",
|
|
1616
|
+
formatter_class=CLIHelpFormatter,
|
|
1617
|
+
description=textwrap.dedent(
|
|
1618
|
+
"""\
|
|
1619
|
+
Record an in-place weekly (7d) credit that the auto-detector
|
|
1620
|
+
misses (a sub-25pp, non-zero drop — e.g. Anthropic lowered your
|
|
1621
|
+
7d % from 46 to 31 without a clean reset). Writes a
|
|
1622
|
+
weekly_credit_floors clamp row (no week re-anchor — the same week
|
|
1623
|
+
continues), lowers hwm-7d, and inserts a post-credit snapshot so
|
|
1624
|
+
reports and the statusline read the credited value.
|
|
1625
|
+
Preview + confirm by default.
|
|
1626
|
+
"""
|
|
1627
|
+
),
|
|
1628
|
+
epilog=textwrap.dedent(
|
|
1629
|
+
"""\
|
|
1630
|
+
Examples:
|
|
1631
|
+
cctally record-credit --to 31 # baseline auto-read from HWM
|
|
1632
|
+
cctally record-credit --to 31 --dry-run # preview, write nothing
|
|
1633
|
+
cctally record-credit --to 31 --yes # apply without prompting
|
|
1634
|
+
|
|
1635
|
+
Exit codes: 0 success (incl. --dry-run and an interactive decline),
|
|
1636
|
+
2 validation/refuse, 3 on a database error.
|
|
1637
|
+
"""
|
|
1638
|
+
),
|
|
1639
|
+
)
|
|
1640
|
+
rc.add_argument("--to", required=True, type=float,
|
|
1641
|
+
help="New post-credit weekly %% (0-100).")
|
|
1642
|
+
rc.add_argument("--from", dest="from_pct", metavar="FROM", type=float, default=None,
|
|
1643
|
+
help="Pre-credit baseline %% (default: current HWM for the week).")
|
|
1644
|
+
rc.add_argument("--at", default=None,
|
|
1645
|
+
help="Effective credit moment (ISO; naive=UTC; default now).")
|
|
1646
|
+
rc.add_argument("--week", default=None,
|
|
1647
|
+
help="week_start_date YYYY-MM-DD (default: current week).")
|
|
1648
|
+
rc.add_argument("--dry-run", action="store_true",
|
|
1649
|
+
help="Preview only; write nothing.")
|
|
1650
|
+
rc.add_argument("--yes", action="store_true",
|
|
1651
|
+
help="Apply without the confirm prompt.")
|
|
1652
|
+
rc.add_argument("--force", action="store_true",
|
|
1653
|
+
help="Re-record when a credit is already fully recorded for the week.")
|
|
1654
|
+
rc.add_argument("--json", action="store_true",
|
|
1655
|
+
help="Machine output (schemaVersion 1).")
|
|
1656
|
+
rc.set_defaults(func=c.cmd_record_credit)
|
|
1657
|
+
|
|
1613
1658
|
rfu = sub.add_parser(
|
|
1614
1659
|
"refresh-usage",
|
|
1615
1660
|
help="Force-fetch 7d/5h percent from OAuth API and record it",
|
package/bin/_cctally_project.py
CHANGED
|
@@ -26,7 +26,13 @@ import json
|
|
|
26
26
|
import os
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
|
-
from _cctally_core import
|
|
29
|
+
from _cctally_core import (
|
|
30
|
+
_command_as_of,
|
|
31
|
+
_reset_aware_floor,
|
|
32
|
+
eprint,
|
|
33
|
+
open_db,
|
|
34
|
+
parse_iso_datetime,
|
|
35
|
+
)
|
|
30
36
|
from _lib_fmt import stable_sum
|
|
31
37
|
|
|
32
38
|
|
|
@@ -53,22 +59,59 @@ def _load_week_snapshots(
|
|
|
53
59
|
`+03:00` from pre-UTC-cast canonicalizer history) coalesce into one
|
|
54
60
|
bucket instead of splitting and silently dropping the higher value.
|
|
55
61
|
|
|
62
|
+
Reset-aware (record-credit M2, #209): each week's MAX is restricted to
|
|
63
|
+
snapshots captured at-or-after that week's latest in-place clamp floor
|
|
64
|
+
(`_reset_aware_floor`, the union of `week_reset_events` +
|
|
65
|
+
`weekly_credit_floors`). Without this, a credited week's per-project Used %
|
|
66
|
+
would read the stale pre-credit peak (e.g. 46) instead of the post-credit
|
|
67
|
+
value (e.g. 31) — the same floor the statusline / write-clamp / `--from`
|
|
68
|
+
helper apply (spec §4a, test S15). The floor compare uses `unixepoch()` on
|
|
69
|
+
both sides (mixed `Z` / `+00:00` offset spellings).
|
|
70
|
+
|
|
56
71
|
Returns an empty dict if the stats DB has no relevant rows.
|
|
57
72
|
"""
|
|
58
73
|
conn = open_db()
|
|
59
74
|
try:
|
|
60
75
|
cur = conn.execute(
|
|
61
|
-
"SELECT week_start_at,
|
|
76
|
+
"SELECT week_start_date, week_start_at, week_end_at, "
|
|
77
|
+
" captured_at_utc, weekly_percent "
|
|
78
|
+
"FROM weekly_usage_snapshots "
|
|
62
79
|
"WHERE week_start_at IS NOT NULL "
|
|
63
80
|
"AND week_end_at IS NOT NULL "
|
|
64
81
|
"AND datetime(week_start_at) < datetime(?) "
|
|
65
82
|
"AND datetime(week_end_at) > datetime(?)",
|
|
66
83
|
(until.isoformat(), since.isoformat()),
|
|
67
84
|
)
|
|
85
|
+
rows = cur.fetchall()
|
|
86
|
+
# Resolve each week's clamp floor once (keyed on week_start_date) so a
|
|
87
|
+
# mid-week credit doesn't re-inflate the per-project Used %. None means
|
|
88
|
+
# "no floor" — every captured row counts.
|
|
89
|
+
floor_epoch: dict[str, int | None] = {}
|
|
90
|
+
|
|
91
|
+
def _floor_for(wsd, ws_iso, we_iso):
|
|
92
|
+
if wsd not in floor_epoch:
|
|
93
|
+
f_iso = _reset_aware_floor(conn, wsd, ws_iso, we_iso)
|
|
94
|
+
floor_epoch[wsd] = (
|
|
95
|
+
int(parse_iso_datetime(f_iso, "project.floor").timestamp())
|
|
96
|
+
if f_iso else None
|
|
97
|
+
)
|
|
98
|
+
return floor_epoch[wsd]
|
|
99
|
+
|
|
68
100
|
result: dict[dt.datetime, float] = {}
|
|
69
|
-
for ws_iso, pct in
|
|
101
|
+
for wsd, ws_iso, we_iso, cap_iso, pct in rows:
|
|
70
102
|
if ws_iso is None or pct is None:
|
|
71
103
|
continue
|
|
104
|
+
floor = _floor_for(wsd, ws_iso, we_iso)
|
|
105
|
+
if floor is not None and cap_iso is not None:
|
|
106
|
+
try:
|
|
107
|
+
cap_epoch = int(
|
|
108
|
+
parse_iso_datetime(str(cap_iso), "project.cap").timestamp()
|
|
109
|
+
)
|
|
110
|
+
except ValueError:
|
|
111
|
+
cap_epoch = None
|
|
112
|
+
# Drop pre-floor (stale pre-credit) snapshots from the MAX.
|
|
113
|
+
if cap_epoch is not None and cap_epoch < floor:
|
|
114
|
+
continue
|
|
72
115
|
ws = dt.datetime.fromisoformat(
|
|
73
116
|
str(ws_iso).replace("Z", "+00:00")
|
|
74
117
|
)
|