cctally 1.8.1 → 1.8.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 +11 -0
- package/bin/_cctally_dashboard.py +158 -98
- package/bin/_cctally_tui.py +156 -31
- package/bin/_lib_view_models.py +784 -0
- package/bin/cctally +118 -34
- package/dashboard/static/assets/{index-CfXu9Fx_.js → index-cWE5HB8O.js} +2 -2
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +1 -1
package/bin/cctally
CHANGED
|
@@ -359,6 +359,19 @@ TrendView = _lib_view_models.TrendView
|
|
|
359
359
|
build_trend_view = _lib_view_models.build_trend_view
|
|
360
360
|
SessionsView = _lib_view_models.SessionsView
|
|
361
361
|
build_sessions_view = _lib_view_models.build_sessions_view
|
|
362
|
+
BlocksView = _lib_view_models.BlocksView
|
|
363
|
+
build_blocks_view = _lib_view_models.build_blocks_view
|
|
364
|
+
build_blocks_view_from_table_rows = _lib_view_models.build_blocks_view_from_table_rows
|
|
365
|
+
ForecastView = _lib_view_models.ForecastView
|
|
366
|
+
build_forecast_view = _lib_view_models.build_forecast_view
|
|
367
|
+
CodexDailyView = _lib_view_models.CodexDailyView
|
|
368
|
+
build_codex_daily_view = _lib_view_models.build_codex_daily_view
|
|
369
|
+
CodexMonthlyView = _lib_view_models.CodexMonthlyView
|
|
370
|
+
build_codex_monthly_view = _lib_view_models.build_codex_monthly_view
|
|
371
|
+
CodexWeeklyView = _lib_view_models.CodexWeeklyView
|
|
372
|
+
build_codex_weekly_view = _lib_view_models.build_codex_weekly_view
|
|
373
|
+
CodexSessionView = _lib_view_models.CodexSessionView
|
|
374
|
+
build_codex_session_view = _lib_view_models.build_codex_session_view
|
|
362
375
|
|
|
363
376
|
_lib_render = _load_sibling("_lib_render")
|
|
364
377
|
_CODEX_MONTHS = _lib_render._CODEX_MONTHS
|
|
@@ -777,6 +790,7 @@ _dashboard_build_monthly_periods = _cctally_dashboard._dashboard_build_monthly_p
|
|
|
777
790
|
_dashboard_build_weekly_periods = _cctally_dashboard._dashboard_build_weekly_periods
|
|
778
791
|
_build_block_detail = _cctally_dashboard._build_block_detail
|
|
779
792
|
_dashboard_build_blocks_panel = _cctally_dashboard._dashboard_build_blocks_panel
|
|
793
|
+
_dashboard_build_blocks_view = _cctally_dashboard._dashboard_build_blocks_view
|
|
780
794
|
_dashboard_build_daily_panel = _cctally_dashboard._dashboard_build_daily_panel
|
|
781
795
|
_empty_dashboard_snapshot = _cctally_dashboard._empty_dashboard_snapshot
|
|
782
796
|
_iso_z = _cctally_dashboard._iso_z
|
|
@@ -5016,13 +5030,30 @@ def cmd_blocks(args: argparse.Namespace) -> int:
|
|
|
5016
5030
|
range_start - BLOCK_DURATION, range_end + BLOCK_DURATION,
|
|
5017
5031
|
)
|
|
5018
5032
|
|
|
5019
|
-
# Group into blocks
|
|
5020
|
-
|
|
5021
|
-
|
|
5033
|
+
# Group into blocks via the view-model kernel (issue #56). The
|
|
5034
|
+
# heuristic-aware ``aggregated`` tuple holds the full Block list
|
|
5035
|
+
# (gaps included, oldest-first) — same shape the JSON / table
|
|
5036
|
+
# renderers expect. We materialize back to a list because
|
|
5037
|
+
# ``_maybe_swap_active_block_to_canonical`` mutates in-place.
|
|
5038
|
+
#
|
|
5039
|
+
# ``skip_rows=True`` (issue #60 review fix) opts out of the
|
|
5040
|
+
# dashboard-row construction inside ``build_blocks_view`` — the
|
|
5041
|
+
# per-block per-model enrichment that scans every entry per
|
|
5042
|
+
# non-gap block (O(B × N)). The CLI never reads ``view.rows``
|
|
5043
|
+
# (only ``view.aggregated`` here), so on large all-history blocks
|
|
5044
|
+
# runs we avoid quadratic-ish work we'd discard.
|
|
5045
|
+
view = build_blocks_view(
|
|
5046
|
+
all_entries,
|
|
5047
|
+
now_utc=now_utc,
|
|
5022
5048
|
recorded_windows=recorded_windows,
|
|
5023
5049
|
block_start_overrides=block_start_overrides,
|
|
5024
|
-
|
|
5050
|
+
range_start=range_start,
|
|
5051
|
+
range_end=range_end,
|
|
5052
|
+
display_tz=tz,
|
|
5053
|
+
mode="auto",
|
|
5054
|
+
skip_rows=True,
|
|
5025
5055
|
)
|
|
5056
|
+
blocks = list(view.aggregated)
|
|
5026
5057
|
|
|
5027
5058
|
# Bug E (v1.7.2 round-4): when the ACTIVE block is heuristic-anchored
|
|
5028
5059
|
# but a canonical ``five_hour_blocks`` row exists for the current 5h
|
|
@@ -5537,7 +5568,14 @@ def cmd_codex_daily(args: argparse.Namespace) -> int:
|
|
|
5537
5568
|
range_start, range_end = range
|
|
5538
5569
|
|
|
5539
5570
|
entries = get_codex_entries(range_start, range_end)
|
|
5540
|
-
|
|
5571
|
+
# Route through ``build_codex_daily_view`` (issue #58). The View
|
|
5572
|
+
# wraps ``_aggregate_codex_daily`` without changing it — preserves
|
|
5573
|
+
# LiteLLM token semantics, intentional dedup vs upstream, and
|
|
5574
|
+
# ``CODEX_LEGACY_FALLBACK_MODEL`` warning end-to-end.
|
|
5575
|
+
view = build_codex_daily_view(
|
|
5576
|
+
entries, now_utc=_command_as_of(), tz_name=tz_name,
|
|
5577
|
+
)
|
|
5578
|
+
days = list(view.rows) # asc — matches aggregator default
|
|
5541
5579
|
if args.order == "desc":
|
|
5542
5580
|
days = list(reversed(days))
|
|
5543
5581
|
|
|
@@ -5559,7 +5597,7 @@ def cmd_codex_daily(args: argparse.Namespace) -> int:
|
|
|
5559
5597
|
y, m, d = bucket.split("-")
|
|
5560
5598
|
return f"{_CODEX_MONTHS[int(m) - 1]} {int(d):02d},\n{y}"
|
|
5561
5599
|
|
|
5562
|
-
tz_label =
|
|
5600
|
+
tz_label = view.display_tz_label
|
|
5563
5601
|
title = f"Codex Token Usage Report - Daily (Timezone: {tz_label})"
|
|
5564
5602
|
print(_render_codex_bucket_table(
|
|
5565
5603
|
days,
|
|
@@ -5589,7 +5627,11 @@ def cmd_codex_monthly(args: argparse.Namespace) -> int:
|
|
|
5589
5627
|
range_start, range_end = range
|
|
5590
5628
|
|
|
5591
5629
|
entries = get_codex_entries(range_start, range_end)
|
|
5592
|
-
|
|
5630
|
+
# Route through ``build_codex_monthly_view`` (issue #58).
|
|
5631
|
+
view = build_codex_monthly_view(
|
|
5632
|
+
entries, now_utc=_command_as_of(), tz_name=tz_name,
|
|
5633
|
+
)
|
|
5634
|
+
months = list(view.rows)
|
|
5593
5635
|
if args.order == "desc":
|
|
5594
5636
|
months = list(reversed(months))
|
|
5595
5637
|
|
|
@@ -5611,7 +5653,7 @@ def cmd_codex_monthly(args: argparse.Namespace) -> int:
|
|
|
5611
5653
|
y, m = bucket.split("-")
|
|
5612
5654
|
return f"{_CODEX_MONTHS[int(m) - 1]}\n{y}"
|
|
5613
5655
|
|
|
5614
|
-
tz_label =
|
|
5656
|
+
tz_label = view.display_tz_label
|
|
5615
5657
|
title = f"Codex Token Usage Report - Monthly (Timezone: {tz_label})"
|
|
5616
5658
|
print(_render_codex_bucket_table(
|
|
5617
5659
|
months,
|
|
@@ -5644,7 +5686,12 @@ def cmd_codex_weekly(args: argparse.Namespace) -> int:
|
|
|
5644
5686
|
week_start_idx = WEEKDAY_MAP[week_start_name]
|
|
5645
5687
|
|
|
5646
5688
|
entries = get_codex_entries(range_start, range_end)
|
|
5647
|
-
|
|
5689
|
+
# Route through ``build_codex_weekly_view`` (issue #58).
|
|
5690
|
+
view = build_codex_weekly_view(
|
|
5691
|
+
entries, now_utc=now_utc, tz_name=tz_name,
|
|
5692
|
+
week_start_idx=week_start_idx,
|
|
5693
|
+
)
|
|
5694
|
+
weeks = list(view.rows)
|
|
5648
5695
|
if args.order == "desc":
|
|
5649
5696
|
weeks = list(reversed(weeks))
|
|
5650
5697
|
|
|
@@ -5669,7 +5716,7 @@ def cmd_codex_weekly(args: argparse.Namespace) -> int:
|
|
|
5669
5716
|
y, m, d = bucket.split("-")
|
|
5670
5717
|
return f"{_CODEX_MONTHS[int(m) - 1]} {int(d):02d},\n{y}"
|
|
5671
5718
|
|
|
5672
|
-
tz_label =
|
|
5719
|
+
tz_label = view.display_tz_label
|
|
5673
5720
|
title = f"Codex Token Usage Report - Weekly (Timezone: {tz_label})"
|
|
5674
5721
|
print(_render_codex_bucket_table(
|
|
5675
5722
|
weeks,
|
|
@@ -5699,8 +5746,13 @@ def cmd_codex_session(args: argparse.Namespace) -> int:
|
|
|
5699
5746
|
range_start, range_end = range
|
|
5700
5747
|
|
|
5701
5748
|
entries = get_codex_entries(range_start, range_end)
|
|
5702
|
-
|
|
5703
|
-
#
|
|
5749
|
+
# Route through ``build_codex_session_view`` (issue #58). View rows
|
|
5750
|
+
# come descending by last_activity (aggregator default + upstream
|
|
5751
|
+
# parity); --order asc reverses.
|
|
5752
|
+
view = build_codex_session_view(
|
|
5753
|
+
entries, now_utc=_command_as_of(), tz_name=tz_name,
|
|
5754
|
+
)
|
|
5755
|
+
sessions = list(view.rows)
|
|
5704
5756
|
if args.order == "asc":
|
|
5705
5757
|
sessions = list(reversed(sessions))
|
|
5706
5758
|
|
|
@@ -5714,7 +5766,7 @@ def cmd_codex_session(args: argparse.Namespace) -> int:
|
|
|
5714
5766
|
print(_codex_sessions_to_json(sessions))
|
|
5715
5767
|
return 0
|
|
5716
5768
|
|
|
5717
|
-
tz_label =
|
|
5769
|
+
tz_label = view.display_tz_label
|
|
5718
5770
|
# Upstream uses "Sessions" (plural) in the session banner title.
|
|
5719
5771
|
title = f"Codex Token Usage Report - Sessions (Timezone: {tz_label})"
|
|
5720
5772
|
print(_render_codex_session_table(
|
|
@@ -8270,7 +8322,16 @@ def cmd_forecast(args: argparse.Namespace) -> int:
|
|
|
8270
8322
|
# sync_cache(conn) here — that prior call ran on the stats DB connection
|
|
8271
8323
|
# (wrong conn) and was a no-op for the real cache anyway.
|
|
8272
8324
|
|
|
8273
|
-
|
|
8325
|
+
# Route through ``build_forecast_view`` (issue #57). The View is the
|
|
8326
|
+
# kernel-pattern wrapper; ``view.output`` carries the existing
|
|
8327
|
+
# ``ForecastOutput`` math result so every downstream renderer here
|
|
8328
|
+
# (text / JSON / status-line / share) reuses the same projection,
|
|
8329
|
+
# verdict, budgets, and per-method rate fields without recomputing.
|
|
8330
|
+
view = build_forecast_view(
|
|
8331
|
+
conn, now_utc=now_utc, targets=tuple(targets),
|
|
8332
|
+
skip_sync=args.no_sync, display_tz=args._resolved_tz,
|
|
8333
|
+
)
|
|
8334
|
+
inputs = view.output.inputs if view.output is not None else None
|
|
8274
8335
|
if inputs is None:
|
|
8275
8336
|
# No snapshot for the current week.
|
|
8276
8337
|
if getattr(args, "format", None):
|
|
@@ -8336,12 +8397,12 @@ def cmd_forecast(args: argparse.Namespace) -> int:
|
|
|
8336
8397
|
print("forecast: no data for current week yet")
|
|
8337
8398
|
return 0
|
|
8338
8399
|
|
|
8339
|
-
output =
|
|
8400
|
+
output = view.output
|
|
8340
8401
|
|
|
8341
8402
|
# Shareable-reports gate: --format short-circuits the JSON / status-line /
|
|
8342
8403
|
# terminal dispatch via `_share_render_and_emit`. The mutex in
|
|
8343
8404
|
# `_add_share_args(has_status_line=True)` keeps `--format`, `--json`, and
|
|
8344
|
-
# `--status-line` from coexisting. The gate fires AFTER
|
|
8405
|
+
# `--status-line` from coexisting. The gate fires AFTER ``build_forecast_view``
|
|
8345
8406
|
# so the snapshot reuses the same projection math as the terminal/JSON
|
|
8346
8407
|
# paths — no parallel computation.
|
|
8347
8408
|
if getattr(args, "format", None):
|
|
@@ -8886,10 +8947,21 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
8886
8947
|
)
|
|
8887
8948
|
else:
|
|
8888
8949
|
period_end = _share_now_utc()
|
|
8889
|
-
|
|
8950
|
+
# Build a BlocksView from the API-anchored table rows
|
|
8951
|
+
# (issue #56). Reset-aware totals come from the table's
|
|
8952
|
+
# per-block columns (CLAUDE.md 5-hour gotcha block) so the
|
|
8953
|
+
# share snapshot's footer reads from the single typed
|
|
8954
|
+
# source rather than re-summing inline.
|
|
8955
|
+
view = build_blocks_view_from_table_rows(
|
|
8890
8956
|
block_dicts,
|
|
8891
8957
|
period_start=period_start,
|
|
8892
8958
|
period_end=period_end,
|
|
8959
|
+
display_tz=args._resolved_tz,
|
|
8960
|
+
)
|
|
8961
|
+
snap = _build_five_hour_blocks_snapshot(
|
|
8962
|
+
view,
|
|
8963
|
+
period_start=period_start,
|
|
8964
|
+
period_end=period_end,
|
|
8893
8965
|
display_tz=display_tz_str,
|
|
8894
8966
|
version=_share_resolve_version(),
|
|
8895
8967
|
theme=args.theme,
|
|
@@ -13211,7 +13283,7 @@ def _build_project_snapshot(
|
|
|
13211
13283
|
|
|
13212
13284
|
|
|
13213
13285
|
def _build_five_hour_blocks_snapshot(
|
|
13214
|
-
|
|
13286
|
+
view: "BlocksView",
|
|
13215
13287
|
*,
|
|
13216
13288
|
period_start: dt.datetime,
|
|
13217
13289
|
period_end: dt.datetime,
|
|
@@ -13223,13 +13295,18 @@ def _build_five_hour_blocks_snapshot(
|
|
|
13223
13295
|
) -> "ShareSnapshot":
|
|
13224
13296
|
"""Build a ShareSnapshot for `cctally five-hour-blocks`.
|
|
13225
13297
|
|
|
13226
|
-
`
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13298
|
+
`view` is the ``BlocksView`` produced by
|
|
13299
|
+
``build_blocks_view_from_table_rows`` (issue #56). The
|
|
13300
|
+
API-anchored block dicts (sqlite Row → dict with the
|
|
13301
|
+
``__is_active`` / ``__credits`` side-channels attached) live on
|
|
13302
|
+
``view.aggregated``; reset-aware totals come from
|
|
13303
|
+
``view.total_cost_usd`` so the share footer reads from the typed
|
|
13304
|
+
single source rather than re-summing inline. Schema fields used
|
|
13305
|
+
from each dict: ``block_start_at`` (ISO timestamp),
|
|
13306
|
+
``total_cost_usd``, ``final_five_hour_percent``,
|
|
13307
|
+
``crossed_seven_day_reset`` (0/1 int),
|
|
13308
|
+
``seven_day_pct_at_block_start``, ``seven_day_pct_at_block_end``,
|
|
13309
|
+
plus the synthetic ``__is_active`` flag.
|
|
13233
13310
|
|
|
13234
13311
|
Deviations from the plan sketch (which assumed dict rows with keys
|
|
13235
13312
|
`block_start` / `cost_usd` / `used_pct_5h` / `top_model` /
|
|
@@ -13261,11 +13338,12 @@ def _build_five_hour_blocks_snapshot(
|
|
|
13261
13338
|
the builder owns the canonical subtitle shape — no post-build
|
|
13262
13339
|
re-stamp at the gate site.
|
|
13263
13340
|
|
|
13264
|
-
Caller MUST pass
|
|
13265
|
-
|
|
13266
|
-
BarChart bars line up
|
|
13267
|
-
|
|
13268
|
-
|
|
13341
|
+
Caller MUST pass a view whose ``aggregated`` block dicts are
|
|
13342
|
+
already in the desired chronological order (cmd_five_hour_blocks
|
|
13343
|
+
pulls newest-first; we reverse here so the BarChart bars line up
|
|
13344
|
+
oldest→newest left-to-right). Tabular row order in the snapshot is
|
|
13345
|
+
irrelevant because the snapshot is what gets rendered (the gate
|
|
13346
|
+
site short-circuits the table renderer).
|
|
13269
13347
|
"""
|
|
13270
13348
|
_lib_share = _share_load_lib()
|
|
13271
13349
|
columns = (
|
|
@@ -13278,9 +13356,11 @@ def _build_five_hour_blocks_snapshot(
|
|
|
13278
13356
|
_lib_share.ColumnSpec(key="cross_reset", label="Reset",
|
|
13279
13357
|
align="left"),
|
|
13280
13358
|
)
|
|
13281
|
-
#
|
|
13282
|
-
#
|
|
13283
|
-
# chart order so consumer
|
|
13359
|
+
# `view.aggregated` carries the newest-first DESC block dicts the
|
|
13360
|
+
# caller built from the SELECT. Reverse so BarChart x-axis runs
|
|
13361
|
+
# oldest→newest; table-row order tracks chart order so consumer
|
|
13362
|
+
# expectations align.
|
|
13363
|
+
rows = list(view.aggregated)
|
|
13284
13364
|
chrono_rows = list(reversed(rows))
|
|
13285
13365
|
snap_rows: list = []
|
|
13286
13366
|
chart_pts: list = []
|
|
@@ -13336,7 +13416,11 @@ def _build_five_hour_blocks_snapshot(
|
|
|
13336
13416
|
_lib_share.BarChart(points=tuple(chart_pts), y_label="$")
|
|
13337
13417
|
if chart_pts else None
|
|
13338
13418
|
)
|
|
13339
|
-
|
|
13419
|
+
# Reset-aware total comes from the BlocksView (issue #56); avg
|
|
13420
|
+
# divides by `chart_pts` count so the share footer "Sum" totalled
|
|
13421
|
+
# and the per-block `chart_pts` cost values share a single source-
|
|
13422
|
+
# of-truth at `view.total_cost_usd`.
|
|
13423
|
+
sum_cost = view.total_cost_usd
|
|
13340
13424
|
avg_cost = (sum_cost / len(chart_pts)) if chart_pts else 0.0
|
|
13341
13425
|
crossed_count = sum(
|
|
13342
13426
|
1 for r in chrono_rows if bool(r.get("crossed_seven_day_reset"))
|