cctally 1.7.4 → 1.8.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 +12 -0
- package/README.md +1 -1
- package/bin/_cctally_dashboard.py +135 -123
- package/bin/_cctally_tui.py +124 -256
- package/bin/_lib_view_models.py +993 -0
- package/bin/cctally +289 -233
- package/dashboard/static/assets/{index-DhCnIFq9.js → index-CfXu9Fx_.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2026-05-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- dashboard envelope: new `daily.total_cost_usd` and `daily.total_tokens` fields exposed alongside the existing `daily.rows[]` so `DailyPanel` and downstream consumers can read the panel's total from the envelope instead of summing rows client-side. Backward-compatible additive change — consumers that ignore the new fields keep working. Sourced from the new `DailyView.total_cost_usd` / `DailyView.total_tokens` view-model fields populated by `build_daily_view` at envelope-construction time.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- view-model kernel: introduce `bin/_lib_view_models.py` as the single canonical builder for each report command's render dataset — adds `DailyView` + `build_daily_view`, `MonthlyView` + `build_monthly_view`, `WeeklyView` + `build_weekly_view`, `TrendView` + `build_trend_view`, and `SessionsView` + `build_sessions_view`. CLI consumers (`cmd_daily`, `cmd_monthly`, `cmd_weekly`, `cmd_report`, `cmd_session`), dashboard envelope builders (`_dashboard_build_*`), share-output snapshots (`_build_*_snapshot`), and TUI builders (`_tui_build_*`) now all route through the same kernel instead of re-aggregating from `iter_entries()` independently. Collapses the prior 3× re-totaling cost across CLI / dashboard / share for daily, monthly, weekly, and sessions; downstream renderers consume the dataclass directly and the camelCase dict workaround between `cmd_report` and the dashboard trend panel is removed. Refactor-only externally — behavior changes are limited to the additive envelope fields above and the credit-week footer fix below.
|
|
15
|
+
- Refine npm package description and README header for discoverability: front-load high-intent search terms ("Claude Code usage tracker", "dashboard", "Pro/Max subscription limits", "quota forecasts", "ccusage-compatible") while preserving the distinctive "cost-per-percent trend" hook. Swap five `package.json` keywords — drop generic noise (`usage`, `cost`, `tracker`, `llm`, `ai-tools`) and add high-intent terms (`claude-code-dashboard`, `claude-code-quota`, `claude-code-cost`, `ccusage-alternative`, `quota-tracking`). GitHub repo About and topics updated to match in lockstep. Description re-indexes on npm at next publish.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- dashboard weekly panel: footer total now matches the synthesized rows on credit weeks. Pre-fix, the panel's row list included `_apply_midweek_reset_override`'s synthesized pre-credit + post-credit rows split at `effective_reset_at_utc`, but the footer total was read from `weekly_cost_snapshots.cost_usd` (the snapshotted total over the original whole week), so the displayed rows summed to a different number than the footer. Now the footer reads `WeeklyView.total_cost_usd`, which sums over the same synthesized rows the panel renders, so the two always match by construction.
|
|
19
|
+
|
|
8
20
|
## [1.7.4] - 2026-05-17
|
|
9
21
|
|
|
10
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<strong>
|
|
9
|
+
<strong>Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.</strong>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -1719,10 +1719,14 @@ def _dashboard_build_monthly_periods(conn: "sqlite3.Connection",
|
|
|
1719
1719
|
display_tz: "ZoneInfo | None" = None) -> "list[MonthlyPeriodRow]":
|
|
1720
1720
|
"""Latest n calendar months as MonthlyPeriodRow, newest-first.
|
|
1721
1721
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1722
|
+
Thin wrapper around ``build_monthly_view`` (spec §6.2). The view
|
|
1723
|
+
owns the aggregator call, boundary-spillover drop, delta_cost_pct
|
|
1724
|
+
derivation, and ``is_current`` flagging — this function just
|
|
1725
|
+
fetches the trailing window of entries and returns ``view.rows``.
|
|
1726
|
+
|
|
1727
|
+
The sync thread separately captures ``view.total_cost_usd`` /
|
|
1728
|
+
``view.total_tokens`` for the envelope; see
|
|
1729
|
+
``_tui_build_snapshot`` and ``DataSnapshot.monthly_total_*``.
|
|
1726
1730
|
"""
|
|
1727
1731
|
# Compute window start = first day of the month that is (n - 1) months
|
|
1728
1732
|
# before the current month. _aggregate_monthly walks all entries and
|
|
@@ -1741,44 +1745,10 @@ def _dashboard_build_monthly_periods(conn: "sqlite3.Connection",
|
|
|
1741
1745
|
range_end = now_utc
|
|
1742
1746
|
|
|
1743
1747
|
entries = get_entries(range_start, range_end, skip_sync=skip_sync)
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
# as a (n+1)th `'YYYY-MM'` row. Slicing here drops that partial bucket,
|
|
1749
|
-
# which (a) keeps the visible history at the requested length and
|
|
1750
|
-
# (b) makes the oldest visible row's delta `None` (prev = None) rather
|
|
1751
|
-
# than a wildly wrong delta vs. a few-hour spillover bucket.
|
|
1752
|
-
buckets = list(reversed(buckets))[:n]
|
|
1753
|
-
rows: list[MonthlyPeriodRow] = []
|
|
1754
|
-
# `_aggregate_monthly` keys buckets by `display_tz` (or local-tz when
|
|
1755
|
-
# unset) month. Mirror that here so `is_current` matches even when
|
|
1756
|
-
# now_utc straddles a tz month boundary (e.g. 23:30 UTC on the last
|
|
1757
|
-
# of the month in a UTC+1 zone).
|
|
1758
|
-
cur_label = (
|
|
1759
|
-
now_utc.astimezone(display_tz) if display_tz is not None
|
|
1760
|
-
# internal fallback: host-local intentional
|
|
1761
|
-
else now_utc.astimezone()
|
|
1762
|
-
).strftime("%Y-%m")
|
|
1763
|
-
for i, b in enumerate(buckets):
|
|
1764
|
-
# b.bucket is the YYYY-MM string for monthly aggregation.
|
|
1765
|
-
prev = buckets[i + 1] if i + 1 < len(buckets) else None
|
|
1766
|
-
delta = None
|
|
1767
|
-
if prev is not None and prev.cost_usd > 0:
|
|
1768
|
-
delta = (b.cost_usd - prev.cost_usd) / prev.cost_usd
|
|
1769
|
-
rows.append(MonthlyPeriodRow(
|
|
1770
|
-
label=b.bucket,
|
|
1771
|
-
cost_usd=b.cost_usd,
|
|
1772
|
-
total_tokens=b.total_tokens,
|
|
1773
|
-
input_tokens=b.input_tokens,
|
|
1774
|
-
output_tokens=b.output_tokens,
|
|
1775
|
-
cache_creation_tokens=b.cache_creation_tokens,
|
|
1776
|
-
cache_read_tokens=b.cache_read_tokens,
|
|
1777
|
-
delta_cost_pct=delta,
|
|
1778
|
-
is_current=(b.bucket == cur_label),
|
|
1779
|
-
models=_model_breakdowns_to_models(b.model_breakdowns, b.cost_usd),
|
|
1780
|
-
))
|
|
1781
|
-
return rows
|
|
1748
|
+
c = _cctally()
|
|
1749
|
+
view = c.build_monthly_view(entries, now_utc=now_utc, n=n,
|
|
1750
|
+
display_tz=display_tz)
|
|
1751
|
+
return list(view.rows)
|
|
1782
1752
|
|
|
1783
1753
|
|
|
1784
1754
|
def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
@@ -1787,10 +1757,15 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
1787
1757
|
skip_sync: bool = False) -> "list[WeeklyPeriodRow]":
|
|
1788
1758
|
"""Latest n subscription weeks as WeeklyPeriodRow, newest-first.
|
|
1789
1759
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1760
|
+
Thin builder-using prelude + Bug-K pre-credit synthesis on top of
|
|
1761
|
+
``view.rows``. ``build_weekly_view`` (in ``bin/_lib_view_models.py``)
|
|
1762
|
+
owns the bucket+overlay walk — this function calls it, swaps two
|
|
1763
|
+
presentation fields (``label`` ← ``display_start_date`` for post-
|
|
1764
|
+
early-reset weeks; ``is_current`` ← SubWeek-containing-now_utc with
|
|
1765
|
+
snapshot fallback so the "Now" pill tracks wall time), layers Bug-K
|
|
1766
|
+
pre-credit synthesized rows over the natural rows, then recomputes
|
|
1767
|
+
``delta_cost_pct`` newest-first so the synthesized rows participate
|
|
1768
|
+
in the deltas.
|
|
1794
1769
|
|
|
1795
1770
|
Note: weekly bucketing intentionally does NOT take ``display_tz`` —
|
|
1796
1771
|
SubWeek bucket keys come from server-anchored stored anchors and the
|
|
@@ -1808,10 +1783,6 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
1808
1783
|
parse_iso_datetime(weeks[0].start_ts, "week_start_at"),
|
|
1809
1784
|
)
|
|
1810
1785
|
entries = get_entries(fetch_start, range_end, skip_sync=skip_sync)
|
|
1811
|
-
buckets = _aggregate_weekly(entries, weeks)
|
|
1812
|
-
if not buckets:
|
|
1813
|
-
return []
|
|
1814
|
-
|
|
1815
1786
|
as_of_utc = (
|
|
1816
1787
|
range_end.astimezone(dt.timezone.utc)
|
|
1817
1788
|
.replace(microsecond=0)
|
|
@@ -1819,6 +1790,15 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
1819
1790
|
.replace("+00:00", "Z")
|
|
1820
1791
|
)
|
|
1821
1792
|
|
|
1793
|
+
# Builder prelude: produce the natural newest-first rows + overlay.
|
|
1794
|
+
c = _cctally()
|
|
1795
|
+
view = c.build_weekly_view(
|
|
1796
|
+
conn, entries, weeks=weeks, now_utc=now_utc,
|
|
1797
|
+
display_tz=None, as_of_utc=as_of_utc,
|
|
1798
|
+
)
|
|
1799
|
+
if not view.rows:
|
|
1800
|
+
return []
|
|
1801
|
+
|
|
1822
1802
|
# Prefer the SubWeek that actually contains `now_utc` so the "Now" pill
|
|
1823
1803
|
# tracks wall time even when `weekly_usage_snapshots` is stale (e.g.,
|
|
1824
1804
|
# status line hasn't fired yet this week but cost entries already exist).
|
|
@@ -1843,47 +1823,36 @@ def _dashboard_build_weekly_periods(conn: "sqlite3.Connection",
|
|
|
1843
1823
|
else:
|
|
1844
1824
|
cur_week_start = max(weeks, key=lambda w: w.start_date).start_date.isoformat()
|
|
1845
1825
|
|
|
1826
|
+
# SubWeek lookup by (start_ts, end_ts) — the builder identifies each row
|
|
1827
|
+
# by these ISO strings on ``WeeklyPeriodRow``, which match SubWeek 1:1
|
|
1828
|
+
# post _aggregate_weekly invariant.
|
|
1829
|
+
sw_by_window = {(w.start_ts, w.end_ts): w for w in weeks}
|
|
1830
|
+
|
|
1831
|
+
# Convert builder rows (newest-first) → oldest-first so the existing
|
|
1832
|
+
# Bug-K insertion logic (oldest-first indices) stays unchanged. Override
|
|
1833
|
+
# the two presentation fields that diverge between CLI/share (which use
|
|
1834
|
+
# ``start_date`` + "now in window" semantics) and the dashboard panel
|
|
1835
|
+
# (which uses ``display_start_date`` + "current SubWeek" semantics).
|
|
1846
1836
|
rows_oldest_first: list[WeeklyPeriodRow] = []
|
|
1847
|
-
for
|
|
1848
|
-
sw =
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
week_start_date=sw.start_date.isoformat(),
|
|
1857
|
-
week_end_date=sw.end_date.isoformat(),
|
|
1858
|
-
week_start_at=sw.start_ts,
|
|
1859
|
-
week_end_at=sw.end_ts,
|
|
1860
|
-
)
|
|
1861
|
-
usage_row = get_latest_usage_for_week(conn, ref, as_of_utc=as_of_utc)
|
|
1862
|
-
used_pct = None
|
|
1863
|
-
dpp = None
|
|
1864
|
-
if usage_row is not None and usage_row["weekly_percent"] is not None:
|
|
1865
|
-
used_pct = float(usage_row["weekly_percent"])
|
|
1866
|
-
dpp = (bucket.cost_usd / used_pct) if used_pct > 0 else None
|
|
1867
|
-
rows_oldest_first.append(WeeklyPeriodRow(
|
|
1868
|
-
label=label,
|
|
1869
|
-
cost_usd=bucket.cost_usd,
|
|
1870
|
-
total_tokens=bucket.total_tokens,
|
|
1871
|
-
input_tokens=bucket.input_tokens,
|
|
1872
|
-
output_tokens=bucket.output_tokens,
|
|
1873
|
-
cache_creation_tokens=bucket.cache_creation_tokens,
|
|
1874
|
-
cache_read_tokens=bucket.cache_read_tokens,
|
|
1875
|
-
used_pct=used_pct,
|
|
1876
|
-
dollar_per_pct=dpp,
|
|
1877
|
-
delta_cost_pct=None, # filled below in newest-first pass
|
|
1837
|
+
for r in reversed(view.rows):
|
|
1838
|
+
sw = sw_by_window.get((r.week_start_at, r.week_end_at))
|
|
1839
|
+
if sw is not None:
|
|
1840
|
+
# Label = MM-DD of the week's display_start_date — for non-reset
|
|
1841
|
+
# weeks this equals start_date; for post-early-reset weeks the
|
|
1842
|
+
# post-processor shifts it forward to the effective reset moment
|
|
1843
|
+
# so the user sees the date the week actually began (04-23 vs the
|
|
1844
|
+
# API-derived backdated 04-18).
|
|
1845
|
+
r.label = sw.display_start_date.strftime("%m-%d")
|
|
1878
1846
|
# is_current keys on start_date (the bucket / lookup key) on both
|
|
1879
1847
|
# sides of the comparison; display_start_date may diverge for
|
|
1880
1848
|
# reset-event weeks but that is intentional — display vs. lookup
|
|
1881
1849
|
# are kept separate.
|
|
1882
|
-
is_current=(sw.start_date.isoformat() == cur_week_start)
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1850
|
+
r.is_current = (sw.start_date.isoformat() == cur_week_start)
|
|
1851
|
+
# delta_cost_pct: builder computed it in asc order; reset and
|
|
1852
|
+
# recompute newest-first below AFTER Bug-K rows merge in, so the
|
|
1853
|
+
# synthesized rows participate in the deltas.
|
|
1854
|
+
r.delta_cost_pct = None
|
|
1855
|
+
rows_oldest_first.append(r)
|
|
1887
1856
|
|
|
1888
1857
|
# Bug K (v1.7.2 round-5): synthesize a pre-credit segment row for
|
|
1889
1858
|
# each in-place credit event. Without this the credited week shows
|
|
@@ -2250,27 +2219,51 @@ def _dashboard_build_daily_panel(conn: "sqlite3.Connection",
|
|
|
2250
2219
|
display_tz: "ZoneInfo | None" = None) -> "list[DailyPanelRow]":
|
|
2251
2220
|
"""Latest n display-tz dates as DailyPanelRow, newest-first.
|
|
2252
2221
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2222
|
+
Two-layer composition (spec §4.4, §6.1):
|
|
2223
|
+
|
|
2224
|
+
1. ``build_daily_view`` (in ``bin/_lib_view_models.py``) is the
|
|
2225
|
+
data plane — gap-free rows + totals derived from the
|
|
2226
|
+
aggregator output.
|
|
2227
|
+
2. This function is the presentation adapter — materializes the
|
|
2228
|
+
contiguous N-day calendar window (gap days as zero-cost rows
|
|
2229
|
+
so the heatmap shows them as faded cells), fills the
|
|
2230
|
+
presentation-only ``label`` (``MM-DD``) and
|
|
2231
|
+
``intensity_bucket`` (quintile via
|
|
2232
|
+
``_compute_intensity_buckets``) fields.
|
|
2233
|
+
|
|
2234
|
+
Bucketing and the ``is_today`` reference both follow
|
|
2235
|
+
``display_tz`` so users on a non-host display zone see days
|
|
2236
|
+
grouped consistently with the rest of the UI.
|
|
2237
|
+
|
|
2238
|
+
Sync-thread totals: the caller sums over the materialized rows
|
|
2239
|
+
this function returns and stashes the result on
|
|
2240
|
+
``DataSnapshot.daily_total_cost_usd`` / ``daily_total_tokens`` for
|
|
2241
|
+
the envelope adapter. Sum-over-visible-rows preserves the
|
|
2242
|
+
structural-equality invariant the dashboard footer reads
|
|
2243
|
+
(`total === rows.reduce(...)`); gap days carry ``cost_usd=0.0`` /
|
|
2244
|
+
``total_tokens=0`` so the sum stays gap-free semantically. Doing
|
|
2245
|
+
it at the sync-thread site keeps this function's return type
|
|
2246
|
+
stable (preserving the dashboard / TUI / test fixture monkeypatch
|
|
2247
|
+
surface that consumes a plain ``list[DailyPanelRow]``).
|
|
2259
2248
|
"""
|
|
2260
2249
|
# Wide trailing window — n days of slack on either side keeps it
|
|
2261
2250
|
# forgiving of tz boundary issues.
|
|
2262
2251
|
range_start = now_utc - dt.timedelta(days=n + 1)
|
|
2263
2252
|
range_end = now_utc
|
|
2264
2253
|
entries = get_entries(range_start, range_end, skip_sync=skip_sync)
|
|
2265
|
-
|
|
2266
|
-
|
|
2254
|
+
|
|
2255
|
+
c = _cctally()
|
|
2256
|
+
view = c.build_daily_view(entries, now_utc=now_utc,
|
|
2257
|
+
display_tz=display_tz)
|
|
2258
|
+
if not view.rows:
|
|
2267
2259
|
return []
|
|
2268
2260
|
|
|
2269
|
-
# Materialize the
|
|
2270
|
-
#
|
|
2271
|
-
#
|
|
2272
|
-
#
|
|
2273
|
-
|
|
2261
|
+
# Materialize the contiguous N-day window. ``view.rows`` is gap-free
|
|
2262
|
+
# (newest-first) and carries the data-plane fields; the adapter
|
|
2263
|
+
# overlays it onto the calendar window and fills the presentation-
|
|
2264
|
+
# only ``label`` / ``intensity_bucket`` (which the builder left at
|
|
2265
|
+
# dataclass defaults per spec §4.4).
|
|
2266
|
+
rows_by_date = {r.date: r for r in view.rows}
|
|
2274
2267
|
today_local = (
|
|
2275
2268
|
now_utc.astimezone(display_tz) if display_tz is not None
|
|
2276
2269
|
# internal fallback: host-local intentional
|
|
@@ -2281,28 +2274,12 @@ def _dashboard_build_daily_panel(conn: "sqlite3.Connection",
|
|
|
2281
2274
|
for i in range(n):
|
|
2282
2275
|
d = today_local - dt.timedelta(days=i)
|
|
2283
2276
|
date_str = d.isoformat()
|
|
2284
|
-
|
|
2285
|
-
if
|
|
2286
|
-
#
|
|
2287
|
-
#
|
|
2288
|
-
#
|
|
2289
|
-
|
|
2290
|
-
denom = b.input_tokens + b.cache_creation_tokens + b.cache_read_tokens
|
|
2291
|
-
cache_hit = (b.cache_read_tokens / denom * 100.0) if denom > 0 else None
|
|
2292
|
-
rows.append(DailyPanelRow(
|
|
2293
|
-
date=date_str,
|
|
2294
|
-
label=date_str[5:], # YYYY-MM-DD → MM-DD
|
|
2295
|
-
cost_usd=b.cost_usd,
|
|
2296
|
-
is_today=(d == today_local),
|
|
2297
|
-
intensity_bucket=0, # set by _compute_intensity_buckets below
|
|
2298
|
-
models=_model_breakdowns_to_models(b.model_breakdowns, b.cost_usd),
|
|
2299
|
-
input_tokens=b.input_tokens,
|
|
2300
|
-
output_tokens=b.output_tokens,
|
|
2301
|
-
cache_creation_tokens=b.cache_creation_tokens,
|
|
2302
|
-
cache_read_tokens=b.cache_read_tokens,
|
|
2303
|
-
total_tokens=b.total_tokens,
|
|
2304
|
-
cache_hit_pct=cache_hit,
|
|
2305
|
-
))
|
|
2277
|
+
existing = rows_by_date.get(date_str)
|
|
2278
|
+
if existing is not None:
|
|
2279
|
+
# Use the view-model row but fill the presentation-only
|
|
2280
|
+
# ``label`` (intensity_bucket is set by
|
|
2281
|
+
# ``_compute_intensity_buckets`` below).
|
|
2282
|
+
rows.append(dataclasses.replace(existing, label=date_str[5:]))
|
|
2306
2283
|
else:
|
|
2307
2284
|
# Zero-cost gap day: tokens default to 0, cache_hit_pct to None
|
|
2308
2285
|
# (avoids /0 and signals 'no data' cleanly to the modal tile).
|
|
@@ -2877,8 +2854,28 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2877
2854
|
# can distinguish "loading / not synced yet" (null parent) from
|
|
2878
2855
|
# "synced + no data" (empty rows). For Weekly/Monthly, sync always
|
|
2879
2856
|
# builds the rows list (even if empty), so we always emit the object.
|
|
2880
|
-
weekly_env = {
|
|
2881
|
-
|
|
2857
|
+
weekly_env = {
|
|
2858
|
+
"rows": [_weekly_row_to_dict(r) for r in snap.weekly_periods],
|
|
2859
|
+
# View-model unification (Bundle 1; spec §6.6): pre-computed
|
|
2860
|
+
# totals. Sourced from sum-over-visible-rows in the sync thread
|
|
2861
|
+
# (``_tui_build_snapshot``) so the footer total is structurally
|
|
2862
|
+
# equal to the React panel's ``rows.reduce(...)``. The earlier
|
|
2863
|
+
# ``build_weekly_view``-sourced totals undercounted Bug-K
|
|
2864
|
+
# pre-credit synthesized rows on credit weeks; the regression is
|
|
2865
|
+
# captured by
|
|
2866
|
+
# ``test_weekly_envelope_total_matches_sum_of_visible_rows``.
|
|
2867
|
+
"total_cost_usd": snap.weekly_total_cost_usd,
|
|
2868
|
+
"total_tokens": snap.weekly_total_tokens,
|
|
2869
|
+
}
|
|
2870
|
+
monthly_env = {
|
|
2871
|
+
"rows": [_monthly_row_to_dict(r) for r in snap.monthly_periods],
|
|
2872
|
+
# View-model unification (Bundle 1; spec §6.6): pre-computed
|
|
2873
|
+
# totals via sum-over-visible-rows so the React MonthlyPanel's
|
|
2874
|
+
# smoking-gun ``rows.reduce(...)`` collapses to a single envelope
|
|
2875
|
+
# read with the structural-equality invariant preserved.
|
|
2876
|
+
"total_cost_usd": snap.monthly_total_cost_usd,
|
|
2877
|
+
"total_tokens": snap.monthly_total_tokens,
|
|
2878
|
+
}
|
|
2882
2879
|
|
|
2883
2880
|
blocks_env = {"rows": [_blocks_row_to_dict(r) for r in snap.blocks_panel]}
|
|
2884
2881
|
|
|
@@ -2898,6 +2895,15 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
2898
2895
|
"rows": [_daily_row_to_dict(r) for r in daily_rows],
|
|
2899
2896
|
"quantile_thresholds": daily_thresholds,
|
|
2900
2897
|
"peak": daily_peak,
|
|
2898
|
+
# View-model unification (Bundle 1; spec §6.6): pre-computed
|
|
2899
|
+
# totals via sum-over-visible-rows in the sync thread. Gap days
|
|
2900
|
+
# carry ``cost_usd=0.0`` and ``total_tokens=0`` so summing the
|
|
2901
|
+
# materialized panel rows preserves the gap-free semantics. The
|
|
2902
|
+
# React panel's `rows.reduce(...)` collapses to a single
|
|
2903
|
+
# envelope read with the structural-equality invariant
|
|
2904
|
+
# preserved.
|
|
2905
|
+
"total_cost_usd": snap.daily_total_cost_usd,
|
|
2906
|
+
"total_tokens": snap.daily_total_tokens,
|
|
2901
2907
|
}
|
|
2902
2908
|
|
|
2903
2909
|
# ---- threshold-actions T5: alerts envelope + settings mirror ----
|
|
@@ -3099,6 +3105,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
3099
3105
|
}
|
|
3100
3106
|
for w in (snap.weekly_history or [])
|
|
3101
3107
|
],
|
|
3108
|
+
# View-model unification (Bundle 1; spec §6.6): the
|
|
3109
|
+
# pre-computed 3-sample $/% mean. TrendPanel can stop
|
|
3110
|
+
# re-deriving the panel-average; TrendModal's median
|
|
3111
|
+
# over trend.history is out of scope for this refactor
|
|
3112
|
+
# (separate dataset, separate follow-up).
|
|
3113
|
+
"avg_dollars_per_pct": snap.trend_avg_dollars_per_pct,
|
|
3102
3114
|
},
|
|
3103
3115
|
|
|
3104
3116
|
"weekly": weekly_env,
|