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/bin/_cctally_tui.py
CHANGED
|
@@ -808,55 +808,18 @@ class TuiCurrentWeek:
|
|
|
808
808
|
five_hour_block: dict | None = None
|
|
809
809
|
|
|
810
810
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
@dataclass
|
|
824
|
-
class WeeklyPeriodRow:
|
|
825
|
-
"""One subscription-week row for the dashboard's Weekly panel/modal.
|
|
826
|
-
|
|
827
|
-
`models` is a list of `{model, display, chip, cost_usd, cost_pct}`
|
|
828
|
-
dicts sorted by `cost_usd` descending. Pre-bucketed in Python so
|
|
829
|
-
the React layer never re-derives per-model coloring.
|
|
830
|
-
"""
|
|
831
|
-
label: str # "04-23" — MM-DD of the week start
|
|
832
|
-
cost_usd: float
|
|
833
|
-
total_tokens: int
|
|
834
|
-
input_tokens: int
|
|
835
|
-
output_tokens: int
|
|
836
|
-
cache_creation_tokens: int
|
|
837
|
-
cache_read_tokens: int
|
|
838
|
-
used_pct: float | None # from weekly_usage_snapshots overlay
|
|
839
|
-
dollar_per_pct: float | None # cost / used_pct when used_pct > 0
|
|
840
|
-
delta_cost_pct: float | None # (cost - prev_cost) / prev_cost
|
|
841
|
-
is_current: bool
|
|
842
|
-
models: list[dict[str, Any]]
|
|
843
|
-
week_start_at: str # ISO-8601 with tz, from SubWeek.start_ts
|
|
844
|
-
week_end_at: str # ISO-8601 with tz, from SubWeek.end_ts
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
@dataclass
|
|
848
|
-
class MonthlyPeriodRow:
|
|
849
|
-
"""One calendar-month row for the dashboard's Monthly panel/modal."""
|
|
850
|
-
label: str # "YYYY-MM"
|
|
851
|
-
cost_usd: float
|
|
852
|
-
total_tokens: int
|
|
853
|
-
input_tokens: int
|
|
854
|
-
output_tokens: int
|
|
855
|
-
cache_creation_tokens: int
|
|
856
|
-
cache_read_tokens: int
|
|
857
|
-
delta_cost_pct: float | None
|
|
858
|
-
is_current: bool
|
|
859
|
-
models: list[dict[str, Any]]
|
|
811
|
+
# ---- View-model row dataclasses moved to bin/_lib_view_models.py ----
|
|
812
|
+
# Re-exported here so `from _cctally_tui import TuiTrendRow` and
|
|
813
|
+
# `ns["TuiTrendRow"]` direct-dict reads in tests keep resolving. The
|
|
814
|
+
# **extended** TuiTrendRow (spec §4.1: +10 nullable fields) is imported
|
|
815
|
+
# from the same module; the new fields default to None so existing TUI
|
|
816
|
+
# / dashboard fixtures that construct TuiTrendRow positionally stay
|
|
817
|
+
# byte-stable.
|
|
818
|
+
from _lib_view_models import ( # noqa: E402
|
|
819
|
+
TuiTrendRow,
|
|
820
|
+
WeeklyPeriodRow,
|
|
821
|
+
MonthlyPeriodRow,
|
|
822
|
+
)
|
|
860
823
|
|
|
861
824
|
|
|
862
825
|
@dataclass
|
|
@@ -878,43 +841,8 @@ class BlocksPanelRow:
|
|
|
878
841
|
label: str # "HH:MM MMM DD" in local tz, e.g. "14:00 Apr 26"
|
|
879
842
|
|
|
880
843
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
"""One row of the dashboard's Daily heatmap panel.
|
|
884
|
-
|
|
885
|
-
`intensity_bucket` is the server-computed quintile bucket (0..5) —
|
|
886
|
-
bucket 0 is reserved for zero-cost days; buckets 1..5 are quintiles
|
|
887
|
-
over non-zero days.
|
|
888
|
-
|
|
889
|
-
v2.3: Added per-day token rollup + `cache_hit_pct` so the Daily
|
|
890
|
-
detail modal can surface the same fields the CLI's `daily` command
|
|
891
|
-
shows. Defaults preserve compatibility with `_empty_dashboard_snapshot`
|
|
892
|
-
and any pre-v2.3 fixture that omits the new fields.
|
|
893
|
-
"""
|
|
894
|
-
date: str # local-tz YYYY-MM-DD
|
|
895
|
-
label: str # "MM-DD" — pre-formatted, mirrors Weekly/Monthly idiom
|
|
896
|
-
cost_usd: float
|
|
897
|
-
is_today: bool
|
|
898
|
-
intensity_bucket: int # 0..5
|
|
899
|
-
models: list[dict[str, Any]] # ModelCostRow shape, sorted desc by cost
|
|
900
|
-
# ---- v2.3 additions: Daily modal token + cache rollup ----
|
|
901
|
-
input_tokens: int = 0
|
|
902
|
-
output_tokens: int = 0
|
|
903
|
-
cache_creation_tokens: int = 0
|
|
904
|
-
cache_read_tokens: int = 0
|
|
905
|
-
total_tokens: int = 0
|
|
906
|
-
cache_hit_pct: float | None = None
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
@dataclass
|
|
910
|
-
class TuiSessionRow:
|
|
911
|
-
started_at: dt.datetime
|
|
912
|
-
duration_minutes: float
|
|
913
|
-
model_primary: str # first model used in the session
|
|
914
|
-
cost_usd: float
|
|
915
|
-
cache_hit_pct: float | None
|
|
916
|
-
project_label: str # basename of project_path
|
|
917
|
-
session_id: str # full session UUID (v2: needed for session-detail modal)
|
|
844
|
+
# DailyPanelRow + TuiSessionRow moved to bin/_lib_view_models.py — re-export.
|
|
845
|
+
from _lib_view_models import DailyPanelRow, TuiSessionRow # noqa: E402
|
|
918
846
|
|
|
919
847
|
|
|
920
848
|
@dataclass
|
|
@@ -1096,6 +1024,26 @@ class DataSnapshot:
|
|
|
1096
1024
|
# at sync-thread time so ``snapshot_to_envelope`` stays a pure
|
|
1097
1025
|
# renderer; empty list when no current 5h block is bound.
|
|
1098
1026
|
five_hour_milestones: list[dict] = field(default_factory=list)
|
|
1027
|
+
# ---- view-model unification (Bundle 1): pre-computed totals ----
|
|
1028
|
+
# Populated by the sync thread as sum-over-visible-rows over the
|
|
1029
|
+
# panel rows ``_dashboard_build_{daily,monthly,weekly}_periods``
|
|
1030
|
+
# returned (see ``_tui_build_snapshot``); the dashboard envelope
|
|
1031
|
+
# adapter emits these as ``<domain>.total_cost_usd`` /
|
|
1032
|
+
# ``total_tokens`` so the React panels stop running
|
|
1033
|
+
# ``rows.reduce(...)`` in JS. Sum-over-visible-rows is a structural
|
|
1034
|
+
# invariant: ``total === sum(rows[*].cost_usd)`` by construction —
|
|
1035
|
+
# see ``test_weekly_envelope_total_matches_sum_of_visible_rows``.
|
|
1036
|
+
# ``trend_avg_dollars_per_pct`` is sourced from ``build_trend_view``
|
|
1037
|
+
# (3-sample-rule mean per spec §4.3). Defaults preserve
|
|
1038
|
+
# compatibility with pre-Bundle-1 fixture modules that construct
|
|
1039
|
+
# ``DataSnapshot`` positionally. Spec §6.6.
|
|
1040
|
+
daily_total_cost_usd: float = 0.0
|
|
1041
|
+
daily_total_tokens: int = 0
|
|
1042
|
+
monthly_total_cost_usd: float = 0.0
|
|
1043
|
+
monthly_total_tokens: int = 0
|
|
1044
|
+
weekly_total_cost_usd: float = 0.0
|
|
1045
|
+
weekly_total_tokens: int = 0
|
|
1046
|
+
trend_avg_dollars_per_pct: float | None = None
|
|
1099
1047
|
|
|
1100
1048
|
@classmethod
|
|
1101
1049
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -1495,158 +1443,16 @@ def _tui_build_trend(
|
|
|
1495
1443
|
) -> list[TuiTrendRow]:
|
|
1496
1444
|
"""Build the last `count` trend rows, chronological (oldest first).
|
|
1497
1445
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
`cmd_report` byte-for-byte — verified in the bundle regression diff.
|
|
1446
|
+
Bundle 1 / Task 10: wraps the unified ``build_trend_view`` kernel
|
|
1447
|
+
(spec §5.4) — the loop body that used to live here moved into
|
|
1448
|
+
``bin/_lib_view_models.build_trend_view``. The TUI snapshot module
|
|
1449
|
+
consumes the first 7 ``TuiTrendRow`` fields and ignores the 10
|
|
1450
|
+
extended fields (which exist for cmd_report's JSON contract).
|
|
1504
1451
|
"""
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
week_refs = get_recent_weeks(conn, max(1, count))
|
|
1510
|
-
|
|
1511
|
-
# Figure out which week_ref corresponds to the current subscription week.
|
|
1512
|
-
# Mirrors `cmd_report`'s Bug D pattern: build a current_ref from the
|
|
1513
|
-
# latest usage snapshot, route it through `_apply_reset_events_to_weekrefs`
|
|
1514
|
-
# so its `week_start_at` reflects the post-credit segment (or the
|
|
1515
|
-
# original start for non-credit weeks), then disambiguate the
|
|
1516
|
-
# synthesized pre-credit ref from the live post-credit ref via BOTH
|
|
1517
|
-
# `key` AND `week_start_at`. Key-only equality marks both segments
|
|
1518
|
-
# as current, which is why the dashboard's trend panel previously
|
|
1519
|
-
# showed two adjacent rows both highlighted as "current" with
|
|
1520
|
-
# identical 4.0% values on the user's live in-place credit data.
|
|
1521
|
-
latest_usage = conn.execute(
|
|
1522
|
-
"SELECT week_start_date, week_end_date "
|
|
1523
|
-
"FROM weekly_usage_snapshots "
|
|
1524
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
1525
|
-
).fetchone()
|
|
1526
|
-
current_key: str | None = None
|
|
1527
|
-
current_week_start_at: str | None = None
|
|
1528
|
-
if latest_usage is not None and latest_usage["week_start_date"] is not None:
|
|
1529
|
-
current_key = latest_usage["week_start_date"]
|
|
1530
|
-
try:
|
|
1531
|
-
c = _cctally()
|
|
1532
|
-
canon_start, canon_end = c._get_canonical_boundary_for_date(
|
|
1533
|
-
conn, latest_usage["week_start_date"]
|
|
1534
|
-
)
|
|
1535
|
-
current_ref = make_week_ref(
|
|
1536
|
-
week_start_date=latest_usage["week_start_date"],
|
|
1537
|
-
week_end_date=latest_usage["week_end_date"],
|
|
1538
|
-
week_start_at=canon_start,
|
|
1539
|
-
week_end_at=canon_end,
|
|
1540
|
-
)
|
|
1541
|
-
_adjusted = c._apply_reset_events_to_weekrefs(conn, [current_ref])
|
|
1542
|
-
if _adjusted:
|
|
1543
|
-
current_week_start_at = _adjusted[0].week_start_at
|
|
1544
|
-
except (ValueError, sqlite3.DatabaseError, AttributeError):
|
|
1545
|
-
current_week_start_at = None
|
|
1546
|
-
|
|
1547
|
-
# Build an intermediate list of (week_ref, used_pct, dpp) in oldest-first
|
|
1548
|
-
# chronological order.
|
|
1549
|
-
chrono = list(reversed(week_refs))
|
|
1550
|
-
# Split-key set (Bug D): credited weeks appear twice in `week_refs`
|
|
1551
|
-
# with identical `WeekRef.key`. For those keys ONLY, pin
|
|
1552
|
-
# `as_of_utc=week_ref.week_end_at` so each segment finds its own
|
|
1553
|
-
# latest snapshot — without this both segments collapse to the
|
|
1554
|
-
# post-credit snapshot's weekly_percent. Non-credit weeks (single
|
|
1555
|
-
# ref per key) keep the legacy unfiltered lookup.
|
|
1556
|
-
_split_keys = {
|
|
1557
|
-
r.key
|
|
1558
|
-
for r in week_refs
|
|
1559
|
-
if sum(1 for x in week_refs if x.key == r.key) > 1
|
|
1560
|
-
}
|
|
1561
|
-
intermediate: list[tuple[Any, float | None, float | None]] = []
|
|
1562
|
-
for week_ref in chrono:
|
|
1563
|
-
usage = get_latest_usage_for_week(
|
|
1564
|
-
conn,
|
|
1565
|
-
week_ref,
|
|
1566
|
-
as_of_utc=(
|
|
1567
|
-
week_ref.week_end_at if week_ref.key in _split_keys else None
|
|
1568
|
-
),
|
|
1569
|
-
)
|
|
1570
|
-
# See cmd_report for why reset-affected weeks skip the cost cache
|
|
1571
|
-
# and live-compute from session_entries over the effective range.
|
|
1572
|
-
if _week_ref_has_reset_event(conn, week_ref):
|
|
1573
|
-
cost_usd = _compute_cost_for_weekref(week_ref)
|
|
1574
|
-
else:
|
|
1575
|
-
cost = get_latest_cost_for_week(conn, week_ref)
|
|
1576
|
-
cost_usd = float(cost["cost_usd"]) if cost else None
|
|
1577
|
-
percent = float(usage["weekly_percent"]) if usage else None
|
|
1578
|
-
ratio = (cost_usd / percent) if (
|
|
1579
|
-
cost_usd is not None and percent and percent > 0
|
|
1580
|
-
) else None
|
|
1581
|
-
intermediate.append((week_ref, percent, ratio))
|
|
1582
|
-
|
|
1583
|
-
# Normalize dpp into spark heights 1..8 across the window.
|
|
1584
|
-
dpps = [d for _, _, d in intermediate if d is not None]
|
|
1585
|
-
if dpps:
|
|
1586
|
-
lo, hi = min(dpps), max(dpps)
|
|
1587
|
-
span = (hi - lo) or 1e-9
|
|
1588
|
-
else:
|
|
1589
|
-
lo, hi, span = 0.0, 1.0, 1e-9
|
|
1590
|
-
|
|
1591
|
-
out: list[TuiTrendRow] = []
|
|
1592
|
-
prev_dpp: float | None = None
|
|
1593
|
-
for week_ref, percent, dpp in intermediate:
|
|
1594
|
-
delta = (dpp - prev_dpp) if (dpp is not None and prev_dpp is not None) else None
|
|
1595
|
-
spark = 1
|
|
1596
|
-
if dpp is not None:
|
|
1597
|
-
spark = int(round((dpp - lo) / span * 7)) + 1
|
|
1598
|
-
spark = max(1, min(8, spark))
|
|
1599
|
-
# WeekRef.week_start is a date; synthesize a UTC datetime so
|
|
1600
|
-
# TuiTrendRow carries a timezone-aware instant (prefer the explicit
|
|
1601
|
-
# week_start_at if present).
|
|
1602
|
-
if week_ref.week_start_at:
|
|
1603
|
-
week_start_dt = parse_iso_datetime(
|
|
1604
|
-
week_ref.week_start_at, "week_start_at"
|
|
1605
|
-
)
|
|
1606
|
-
week_label = format_display_dt(
|
|
1607
|
-
week_start_dt, display_tz, fmt="%b %d", suffix=False,
|
|
1608
|
-
)
|
|
1609
|
-
else:
|
|
1610
|
-
week_start_dt = dt.datetime.combine(
|
|
1611
|
-
week_ref.week_start, dt.time(0, 0), dt.timezone.utc
|
|
1612
|
-
)
|
|
1613
|
-
# No real boundary instant — format the calendar date directly so
|
|
1614
|
-
# localizing midnight-UTC doesn't shift it to the prior day in
|
|
1615
|
-
# zones west of UTC (e.g. 2026-04-14 → "Apr 13" in America/New_York).
|
|
1616
|
-
week_label = week_ref.week_start.strftime("%b %d")
|
|
1617
|
-
# Bug G (v1.7.2 round-5): match on BOTH `key` AND `week_start_at`
|
|
1618
|
-
# for credited weeks so the pre-credit synthesized ref doesn't
|
|
1619
|
-
# also light up as "current" — both refs share `key`, only their
|
|
1620
|
-
# `week_start_at` differs (post-credit = effective reset moment,
|
|
1621
|
-
# pre-credit = original API-derived start). Non-credit weeks
|
|
1622
|
-
# have only one ref per key so `week_start_at` matching is
|
|
1623
|
-
# automatic. When `current_week_start_at` is None (no reset
|
|
1624
|
-
# event for the current week, or the resolution above failed),
|
|
1625
|
-
# falls back to legacy key-only matching.
|
|
1626
|
-
is_cur = (
|
|
1627
|
-
current_key is not None
|
|
1628
|
-
and week_ref.key == current_key
|
|
1629
|
-
and (
|
|
1630
|
-
current_week_start_at is None
|
|
1631
|
-
or week_ref.week_start_at == current_week_start_at
|
|
1632
|
-
)
|
|
1633
|
-
)
|
|
1634
|
-
out.append(TuiTrendRow(
|
|
1635
|
-
week_label=week_label,
|
|
1636
|
-
week_start_at=week_start_dt,
|
|
1637
|
-
# Preserve None when no usage snapshot exists for this week —
|
|
1638
|
-
# matches `cmd_report`'s "n/a" rendering (9980) and avoids
|
|
1639
|
-
# fabricating a 0.0% row for the phantom-week case (cost
|
|
1640
|
-
# snapshot present, usage snapshot absent).
|
|
1641
|
-
used_pct=float(percent) if percent is not None else None,
|
|
1642
|
-
dollars_per_percent=dpp,
|
|
1643
|
-
delta_dpp=delta,
|
|
1644
|
-
spark_height=spark,
|
|
1645
|
-
is_current=is_cur,
|
|
1646
|
-
))
|
|
1647
|
-
if dpp is not None:
|
|
1648
|
-
prev_dpp = dpp
|
|
1649
|
-
return out
|
|
1452
|
+
c = _cctally()
|
|
1453
|
+
view = c.build_trend_view(conn, now_utc=now_utc, n=max(1, count),
|
|
1454
|
+
display_tz=display_tz)
|
|
1455
|
+
return list(view.rows)
|
|
1650
1456
|
|
|
1651
1457
|
|
|
1652
1458
|
def _tui_build_weekly_history(
|
|
@@ -1685,6 +1491,14 @@ def _tui_build_sessions(
|
|
|
1685
1491
|
|
|
1686
1492
|
When `skip_sync=True`, honors the parent's `--no-sync` intent: no
|
|
1687
1493
|
ingest pass, just read whatever is already cached.
|
|
1494
|
+
|
|
1495
|
+
Bundle 2 / Task 15: wraps the unified ``build_sessions_view``
|
|
1496
|
+
kernel — the prior 40-line inline-derivation body now lives at
|
|
1497
|
+
``bin/_lib_view_models.build_sessions_view``. The TUI keeps
|
|
1498
|
+
ownership of the bounded 365-day scan window (rationale below) and
|
|
1499
|
+
consumes ``view.rows`` (the typed ``TuiSessionRow`` tuple). The
|
|
1500
|
+
view's parallel ``view.aggregated`` is reserved for the CLI / share
|
|
1501
|
+
surfaces; the TUI doesn't need ``ClaudeSessionUsage`` fields.
|
|
1688
1502
|
"""
|
|
1689
1503
|
# Bounded scan window — the sessions pane promises "last `limit`". A
|
|
1690
1504
|
# 365-day scan covers virtually all users (even one-session-every-few-days
|
|
@@ -1693,23 +1507,11 @@ def _tui_build_sessions(
|
|
|
1693
1507
|
# on every entry in the window before slicing.
|
|
1694
1508
|
range_start = now_utc - dt.timedelta(days=365)
|
|
1695
1509
|
entries = get_claude_session_entries(range_start, now_utc, skip_sync=skip_sync)
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
total_io = s.input_tokens + s.cache_creation_tokens + s.cache_read_tokens
|
|
1702
|
-
cache_pct = (total_read / total_io * 100) if total_io > 0 else None
|
|
1703
|
-
out.append(TuiSessionRow(
|
|
1704
|
-
started_at=s.first_activity,
|
|
1705
|
-
duration_minutes=duration_min,
|
|
1706
|
-
model_primary=(s.models[0] if s.models else "—"),
|
|
1707
|
-
cost_usd=s.cost_usd,
|
|
1708
|
-
cache_hit_pct=cache_pct,
|
|
1709
|
-
project_label=os.path.basename(s.project_path) or s.project_path,
|
|
1710
|
-
session_id=s.session_id,
|
|
1711
|
-
))
|
|
1712
|
-
return out
|
|
1510
|
+
c = _cctally()
|
|
1511
|
+
view = c.build_sessions_view(
|
|
1512
|
+
entries, now_utc=now_utc, limit=limit, display_tz=None,
|
|
1513
|
+
)
|
|
1514
|
+
return list(view.rows)
|
|
1713
1515
|
|
|
1714
1516
|
|
|
1715
1517
|
@dataclass
|
|
@@ -1853,10 +1655,19 @@ def _tui_build_snapshot(
|
|
|
1853
1655
|
fc = _tui_build_forecast(conn, now_utc, skip_sync=skip_sync)
|
|
1854
1656
|
except Exception as exc:
|
|
1855
1657
|
errors.append(f"forecast: {exc}")
|
|
1658
|
+
# Trend: source from build_trend_view so we capture the 3-sample
|
|
1659
|
+
# avg_dollars_per_pct alongside the rows. The TUI build path
|
|
1660
|
+
# historically called _tui_build_trend (which now wraps the
|
|
1661
|
+
# builder); calling the builder directly here saves one
|
|
1662
|
+
# `_aggregate_*` round-trip.
|
|
1663
|
+
trend_avg_dpp = None
|
|
1856
1664
|
try:
|
|
1857
|
-
|
|
1858
|
-
|
|
1665
|
+
c = _cctally()
|
|
1666
|
+
_trend_view = c.build_trend_view(
|
|
1667
|
+
conn, now_utc=now_utc, n=8, display_tz=_build_display_tz,
|
|
1859
1668
|
)
|
|
1669
|
+
trend = list(_trend_view.rows)
|
|
1670
|
+
trend_avg_dpp = _trend_view.avg_dollars_per_pct
|
|
1860
1671
|
except Exception as exc:
|
|
1861
1672
|
errors.append(f"trend: {exc}")
|
|
1862
1673
|
try:
|
|
@@ -1881,17 +1692,53 @@ def _tui_build_snapshot(
|
|
|
1881
1692
|
except Exception as exc:
|
|
1882
1693
|
errors.append(f"weekly-history: {exc}")
|
|
1883
1694
|
# ---- v2.1 additions: dashboard Weekly / Monthly panels ----
|
|
1695
|
+
# Sync-thread view-model totals (spec §6.6): sum directly over
|
|
1696
|
+
# the panel rows the dashboard ACTUALLY renders. The previous
|
|
1697
|
+
# implementation called ``build_weekly_view`` a second time to
|
|
1698
|
+
# capture totals, but that builder doesn't see the Bug-K
|
|
1699
|
+
# pre-credit synthesized rows that ``_dashboard_build_weekly_periods``
|
|
1700
|
+
# layers on top (``_apply_reset_events_to_subweeks`` shifts the
|
|
1701
|
+
# post-reset SubWeek's ``start_ts`` so the pre-credit interval
|
|
1702
|
+
# has no SubWeek for ``_aggregate_weekly`` to bucket). On credit
|
|
1703
|
+
# weeks the sync-thread total understated the rendered footer by
|
|
1704
|
+
# hundreds of dollars (~$372 in the v1.7.2 round-5 data).
|
|
1705
|
+
# Sum-over-visible-rows is a structural invariant — see
|
|
1706
|
+
# ``test_weekly_envelope_total_matches_sum_of_visible_rows``.
|
|
1707
|
+
weekly_total_cost_usd = 0.0
|
|
1708
|
+
weekly_total_tokens = 0
|
|
1884
1709
|
try:
|
|
1885
1710
|
weekly_periods = _dashboard_build_weekly_periods(
|
|
1886
1711
|
conn, now_utc, n=12, skip_sync=skip_sync
|
|
1887
1712
|
)
|
|
1713
|
+
# ``sum(..., 0.0)`` pins the type to ``float`` on empty rows so
|
|
1714
|
+
# the envelope stays byte-stable with the pre-fix ``0.0`` shape
|
|
1715
|
+
# (the dashboard fixture goldens assert exact JSON match).
|
|
1716
|
+
weekly_total_cost_usd = sum(
|
|
1717
|
+
(r.cost_usd for r in weekly_periods), 0.0,
|
|
1718
|
+
)
|
|
1719
|
+
weekly_total_tokens = sum(
|
|
1720
|
+
(r.total_tokens for r in weekly_periods), 0,
|
|
1721
|
+
)
|
|
1888
1722
|
except Exception as exc:
|
|
1889
1723
|
errors.append(f"weekly-periods: {exc}")
|
|
1724
|
+
# Sync-thread view-model totals (spec §6.6): sum-over-visible-rows
|
|
1725
|
+
# (same invariant as weekly above). Monthly has no Bug-K analogue,
|
|
1726
|
+
# but coupling the footer total to the panel-row source of truth
|
|
1727
|
+
# eliminates a parallel ``build_monthly_view`` pass that did the
|
|
1728
|
+
# same arithmetic with no behavioral upside.
|
|
1729
|
+
monthly_total_cost_usd = 0.0
|
|
1730
|
+
monthly_total_tokens = 0
|
|
1890
1731
|
try:
|
|
1891
1732
|
monthly_periods = _dashboard_build_monthly_periods(
|
|
1892
1733
|
conn, now_utc, n=12, skip_sync=skip_sync,
|
|
1893
1734
|
display_tz=_build_display_tz,
|
|
1894
1735
|
)
|
|
1736
|
+
monthly_total_cost_usd = sum(
|
|
1737
|
+
(r.cost_usd for r in monthly_periods), 0.0,
|
|
1738
|
+
)
|
|
1739
|
+
monthly_total_tokens = sum(
|
|
1740
|
+
(r.total_tokens for r in monthly_periods), 0,
|
|
1741
|
+
)
|
|
1895
1742
|
except Exception as exc:
|
|
1896
1743
|
errors.append(f"monthly-periods: {exc}")
|
|
1897
1744
|
# ---- v2.2 additions: dashboard Blocks / Daily panels ----
|
|
@@ -1906,11 +1753,25 @@ def _tui_build_snapshot(
|
|
|
1906
1753
|
)
|
|
1907
1754
|
except Exception as exc:
|
|
1908
1755
|
errors.append(f"blocks-panel: {exc}")
|
|
1756
|
+
# Sync-thread view-model totals (Bundle 1 / spec §6.6):
|
|
1757
|
+
# sum-over-visible-rows (same invariant as weekly/monthly above).
|
|
1758
|
+
# Gap days in the materialized panel carry ``cost_usd=0.0`` /
|
|
1759
|
+
# ``total_tokens=0``, so summing the panel rows preserves the
|
|
1760
|
+
# gap-free totals semantically — and removes a duplicate
|
|
1761
|
+
# ``build_daily_view`` pass that did the same arithmetic.
|
|
1762
|
+
daily_total_cost_usd = 0.0
|
|
1763
|
+
daily_total_tokens = 0
|
|
1909
1764
|
try:
|
|
1910
1765
|
daily_panel = _dashboard_build_daily_panel(
|
|
1911
1766
|
conn, now_utc, n=30, skip_sync=skip_sync,
|
|
1912
1767
|
display_tz=_build_display_tz,
|
|
1913
1768
|
)
|
|
1769
|
+
daily_total_cost_usd = sum(
|
|
1770
|
+
(r.cost_usd for r in daily_panel), 0.0,
|
|
1771
|
+
)
|
|
1772
|
+
daily_total_tokens = sum(
|
|
1773
|
+
(r.total_tokens for r in daily_panel), 0,
|
|
1774
|
+
)
|
|
1914
1775
|
except Exception as exc:
|
|
1915
1776
|
errors.append(f"daily-panel: {exc}")
|
|
1916
1777
|
# ---- threshold-actions T5: alerts envelope array ----
|
|
@@ -1951,6 +1812,13 @@ def _tui_build_snapshot(
|
|
|
1951
1812
|
daily_panel=daily_panel,
|
|
1952
1813
|
alerts=alerts,
|
|
1953
1814
|
five_hour_milestones=fh_milestones,
|
|
1815
|
+
daily_total_cost_usd=daily_total_cost_usd,
|
|
1816
|
+
daily_total_tokens=daily_total_tokens,
|
|
1817
|
+
monthly_total_cost_usd=monthly_total_cost_usd,
|
|
1818
|
+
monthly_total_tokens=monthly_total_tokens,
|
|
1819
|
+
weekly_total_cost_usd=weekly_total_cost_usd,
|
|
1820
|
+
weekly_total_tokens=weekly_total_tokens,
|
|
1821
|
+
trend_avg_dollars_per_pct=trend_avg_dpp,
|
|
1954
1822
|
)
|
|
1955
1823
|
finally:
|
|
1956
1824
|
conn.close()
|