cctally 1.81.0 → 1.82.1
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 +20 -0
- package/README.md +45 -74
- package/bin/_cctally_cache.py +51 -0
- package/bin/_cctally_dashboard_envelope.py +70 -0
- package/bin/_cctally_dashboard_share.py +10 -1
- package/bin/_cctally_dashboard_sources.py +209 -85
- package/bin/_cctally_statusline.py +1 -0
- package/bin/_cctally_tui.py +71 -10
- package/bin/_lib_conversation_query.py +45 -0
- package/bin/_lib_dashboard_sources.py +51 -1
- package/bin/_lib_pricing.py +27 -3
- package/bin/_lib_readme_refresh.py +401 -0
- package/dashboard/static/assets/{index-DJP4gEB7.js → index-BKM43pxK.js} +52 -52
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.82.1] - 2026-07-24
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- The public README is a fresh, shorter screenshot-led tour, and it now refreshes itself on every stable release: promoting a release regenerates the screenshots against that exact version and updates a "Latest stable" highlights block on the GitHub page automatically. (#354)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- The dashboard's Recent Sessions card shows session names again. Splitting transcript storage into its own database dropped the name lookup, so every row in the Session column had rendered a dash since then. Names come back from the stored conversation index, and a transcript store that is missing, locked, or rebuilding simply leaves the dash in place instead of holding up the rest of the dashboard. (#362)
|
|
15
|
+
- The All tab's Recent Sessions rows now show Claude session names too, matching the Codex rows beside them; previously only Codex rows were named there. Names still appear only for a local viewer, exactly as on the Claude tab. (#363)
|
|
16
|
+
- The Codex dashboard tab no longer blanks `SPENT THIS WEEK` (and `$/1% used`, `$/1% vs last week`, and the week label) after Codex has been idle for an hour. Codex only reports its quota while you are using it, so the weekly reading goes stale on an idle machine — the spend was never lost, just hidden. It now stays on screen, with the Snapshot chip naming the reading as stale and showing its age. (#350)
|
|
17
|
+
- Forecasts still pause on stale Codex quota evidence, on the dashboard and in a shared report: `Forecast @ reset` and a shared forecast's `Projected` column show `—` rather than projecting from a reading that may be an hour old. (#350)
|
|
18
|
+
- The All tab keeps withholding its combined total while a provider's quota evidence is stale, but now says that is the reason instead of reporting a generic degraded state. (#350)
|
|
19
|
+
|
|
20
|
+
## [1.82.0] - 2026-07-24
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Embedded pricing for Claude Opus 5 (`claude-opus-5`) so Opus 5 sessions are costed correctly instead of contributing $0 as an unrecognized model. Anthropic ships it at the standard Opus rate — $5 / MTok input, $25 / MTok output, $6.25 / MTok 5-minute cache write, $0.50 / MTok cache read — with the full 1M-token context window at standard pricing (no long-context premium), verified against Anthropic's published pricing table; the pricing snapshot date moves to 2026-07-24. LiteLLM carries no Opus 5 entry yet, so the embedded table is simply ahead of it and `pricing-check` does not report drift.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `cctally statusline`'s 🧠 context segment now measures a `claude-opus-5[1m]` session against its real 1,000,000-token window instead of silently falling back to the 200K family default, which inflated the reported context percentage roughly fivefold.
|
|
27
|
+
|
|
8
28
|
## [1.81.0] - 2026-07-24
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<strong>
|
|
9
|
+
<strong>See what Claude Code and Codex actually cost you: a local dashboard, conversation viewer, and CLI reports for your subscription quota.</strong>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -16,54 +16,58 @@
|
|
|
16
16
|
<a href="https://github.com/omrikais/cctally/stargazers"><img src="https://img.shields.io/github/stars/omrikais/cctally.svg" alt="GitHub stars"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
Claude Code
|
|
19
|
+
Your Claude Code plan meters you with a percentage that creeps up all week. cctally reads your local session logs and turns that percentage into dollars: what each percent of quota costs you, whether you are on track to cap before the reset, and where the spend is going. It does the same for OpenAI's Codex CLI. Everything runs on your own machine, against your own data. No account, no API key, and nothing is uploaded.
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<img src="docs/img/dashboard-desktop.png" alt="cctally dashboard, desktop view" width="900">
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<!-- cctally:latest-stable:begin -->
|
|
26
|
+
**Latest stable: v1.81.0** (2026-07-24)
|
|
27
|
+
|
|
28
|
+
- cctally now tracks usage per account for each provider: if you use more than one Claude or Codex account on this machine, each account's percent, 5-hour, and quota milestones, and their alerts, are recorded and fire independently instead of silently colliding.
|
|
29
|
+
- New `cctally account list|show|label` subcommand shows every observed account (provider, label, email, plan, first/last seen, and which is currently active) and lets you set a durable friendly label that survives a stats rebuild.
|
|
30
|
+
- Optional per-account weekly budgets via `config set budget.accounts` (Claude) and `config set budget.codex.accounts` (Codex); a budget targets an immutable account even after you rename its label, and a Codex per-account budget works without a vendor-wide amount.
|
|
31
|
+
<!-- cctally:latest-stable:end -->
|
|
32
|
+
|
|
25
33
|
## Quick start
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
Requirements: Python 3.11+, macOS or Linux, Claude Code installed and run at least once.
|
|
28
36
|
|
|
29
37
|
```bash
|
|
30
38
|
# Homebrew (macOS / Linux)
|
|
31
39
|
brew install omrikais/cctally/cctally && cctally setup
|
|
32
40
|
|
|
33
|
-
#
|
|
41
|
+
# or npm
|
|
34
42
|
npm install -g cctally && cctally setup
|
|
35
43
|
|
|
36
|
-
#
|
|
44
|
+
# or from source
|
|
37
45
|
git clone https://github.com/omrikais/cctally && cd cctally && ./bin/cctally setup
|
|
38
46
|
```
|
|
39
47
|
|
|
40
|
-
The reporting commands work
|
|
41
|
-
|
|
42
|
-
`cctally setup` symlinks the binaries into `~/.local/bin/`, adds three additive hooks to `~/.claude/settings.json` (it never overwrites existing entries), and bootstraps the local SQLite cache — on a large session history that first-run sync now shows live progress instead of sitting silent. If `~/.local/bin/` isn't on your `PATH`, the script prints the line to add. The npm install needs Python 3 on `PATH` - if setup reports "python3 not found", install it (`brew install python` on macOS) and re-run.
|
|
48
|
+
The reporting commands work immediately on your existing logs, before any setup. Running `cctally setup` once adds the hooks that record your quota percentage continuously as you work.
|
|
43
49
|
|
|
44
50
|
```bash
|
|
45
|
-
cctally
|
|
46
|
-
cctally daily # cost-by-day - your first table
|
|
51
|
+
cctally daily # cost by day: your first table
|
|
47
52
|
cctally dashboard # opens http://127.0.0.1:8789
|
|
53
|
+
cctally setup --status # verify the install
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
**Beta channel (opt-in).** Every release ships to a beta channel first; the maintainer promotes the ones that prove out to stable, which is the default. To ride along with the newest builds as they land, opt in with `cctally config set update.channel beta` (npm or source installs — Homebrew tracks stable). `cctally update` then installs the exact latest beta version, and `cctally config set update.channel stable` flips you back cleanly with no silent downgrade. See [docs/commands/update.md](docs/commands/update.md#beta-channel).
|
|
56
|
+
Install details (symlinks, PATH, Python version) live in [docs/installation.md](docs/installation.md). Every release ships to an opt-in beta channel first; see [docs/commands/update.md](docs/commands/update.md#beta-channel).
|
|
53
57
|
|
|
54
58
|
## The live dashboard
|
|
55
59
|
|
|
56
|
-
`cctally dashboard` serves a web app
|
|
60
|
+
`cctally dashboard` serves a local web app that updates live as you work, with no refresh and no polling. Panels cover the current week, the forecast, the cost trend, sessions, 5-hour blocks, projects, and alerts. Any panel expands into a focused view, sessions are searchable, and every report can be exported as shareable Markdown, HTML, or SVG with project names anonymized by default. It stays on your machine unless you choose to open it to your network.
|
|
57
61
|
|
|
58
62
|
<table>
|
|
59
63
|
<tr>
|
|
60
64
|
<td>
|
|
61
65
|
<img src="docs/img/dashboard-modal.png" alt="Dashboard with trend modal open">
|
|
62
|
-
<br><em>Any panel expands into a focused view
|
|
66
|
+
<br><em>Any panel expands into a focused view: here, twelve weeks of cost per percent.</em>
|
|
63
67
|
</td>
|
|
64
68
|
<td>
|
|
65
69
|
<img src="docs/img/dashboard-warn.png" alt="Dashboard in warn state">
|
|
66
|
-
<br><em>When the forecast projects a cap before the weekly reset, the modal
|
|
70
|
+
<br><em>When the forecast projects a cap before the weekly reset, the modal turns amber.</em>
|
|
67
71
|
</td>
|
|
68
72
|
</tr>
|
|
69
73
|
<tr>
|
|
@@ -76,16 +80,12 @@ For status-line integration and tuning, see [docs/installation.md](docs/installa
|
|
|
76
80
|
|
|
77
81
|
See [docs/commands/dashboard.md](docs/commands/dashboard.md).
|
|
78
82
|
|
|
79
|
-
## Conversation viewer
|
|
80
|
-
|
|
81
|
-
The dashboard's **Conversations** tab is a read-only reader for your Claude Code transcripts - your local session history, rendered the way you'd actually want to review it. A searchable rail lists every conversation with its project, branch, model chips, and cost; the reader shows the full turn-by-turn flow with thinking blocks, tool calls (with diffs and command output), and a per-turn cost-and-token breakdown. Parallel **subagent threads** render as their own nested threads, an outline pane jumps you to any turn, and an in-conversation find bar plus a faceted full-text search (prompts, assistant text, tools, thinking) make a month of sessions navigable. The open conversation **live-tails** as you work, updating within a second.
|
|
83
|
+
## Conversation viewer
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
The dashboard's Conversations tab is a read-only reader for your Claude Code transcripts. A searchable rail lists every conversation with its project, branch, models, and cost; the reader shows the full turn-by-turn flow with thinking blocks, tool calls, and per-turn cost. Subagent runs render as nested threads, and the open conversation live-tails as you work. It never modifies your transcripts, and it never leaves your machine.
|
|
84
86
|
|
|
85
87
|
<p align="center">
|
|
86
88
|
<img src="docs/img/conversation-reader.png" alt="Conversation viewer: rail, threaded reader, and outline" width="900">
|
|
87
|
-
<br>
|
|
88
|
-
<em>Rail, threaded reader with tool cards and a subagent thread, and the jump-to outline.</em>
|
|
89
89
|
</p>
|
|
90
90
|
|
|
91
91
|
<p align="center">
|
|
@@ -96,97 +96,68 @@ It only reads your transcripts - it never changes them - and everything stays on
|
|
|
96
96
|
|
|
97
97
|
## Cost per 1% of quota
|
|
98
98
|
|
|
99
|
-
The signature view. `cctally report` reframes each
|
|
99
|
+
The signature view. `cctally report` reframes each week's spend as dollars per percent of quota used, so you can watch your spending efficiency trend week over week instead of staring at a raw percentage.
|
|
100
100
|
|
|
101
101
|
<p align="center">
|
|
102
|
-
<img src="docs/img/cli-report.svg" alt="cctally report:
|
|
103
|
-
<br>
|
|
104
|
-
<em>Weekly cost as dollars per percent of quota, with the delta against the prior week.</em>
|
|
102
|
+
<img src="docs/img/cli-report.svg" alt="cctally report: dollars per 1% weekly trend">
|
|
105
103
|
</p>
|
|
106
104
|
|
|
107
105
|
See [docs/commands/report.md](docs/commands/report.md).
|
|
108
106
|
|
|
109
|
-
## Forecast
|
|
107
|
+
## Forecast, budget, and alerts
|
|
110
108
|
|
|
111
|
-
`cctally forecast` projects where your weekly percentage lands at the next reset
|
|
109
|
+
`cctally forecast` projects where your weekly percentage lands at the next reset and tells you the daily budget that keeps you under the cap. `cctally budget` tracks a dollar target per provider over a calendar period. Native desktop notifications fire the moment you cross a percent, 5-hour, or budget threshold, so a runaway week cannot sneak up on you.
|
|
112
110
|
|
|
113
111
|
<p align="center">
|
|
114
112
|
<img src="docs/img/cli-forecast.svg" alt="cctally forecast: will I cap this week?">
|
|
115
|
-
<br>
|
|
116
|
-
<em>Projected percent at the weekly reset, plus the daily budget to stay under the cap.</em>
|
|
117
113
|
</p>
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## Threshold alerts
|
|
115
|
+
See [docs/commands/forecast.md](docs/commands/forecast.md), [docs/commands/budget.md](docs/commands/budget.md), and [docs/commands/alerts.md](docs/commands/alerts.md).
|
|
122
116
|
|
|
123
|
-
|
|
117
|
+
## 5-hour blocks
|
|
124
118
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Claude Code's quota also runs on rolling 5-hour windows. `cctally blocks` and `cctally five-hour-blocks` break usage down per window - anchored to the real API resets where a recorded reset covers the window (`blocks` marks its heuristic fallback rows with `~`), not a re-sizable guess - with model and project rollups and cross-reset flags, and `cctally five-hour-breakdown` drills into the per-percent milestones inside a single block.
|
|
119
|
+
Claude Code's quota also runs on rolling 5-hour windows. `cctally blocks` and `cctally five-hour-blocks` break usage down per window, anchored to the real API resets, with model and project rollups.
|
|
128
120
|
|
|
129
121
|
<p align="center">
|
|
130
122
|
<img src="docs/img/cli-five-hour-blocks.svg" alt="cctally five-hour-blocks: 5h analytics with model breakdown">
|
|
131
|
-
<br>
|
|
132
|
-
<em>Each 5-hour window, with rollup totals and 7-day drift.</em>
|
|
133
123
|
</p>
|
|
134
124
|
|
|
135
125
|
See [docs/commands/blocks.md](docs/commands/blocks.md) and [docs/commands/five-hour-blocks.md](docs/commands/five-hour-blocks.md).
|
|
136
126
|
|
|
137
|
-
##
|
|
127
|
+
## Codex
|
|
138
128
|
|
|
139
|
-
|
|
129
|
+
If you also use OpenAI's Codex CLI, cctally tracks it with the same depth. `cctally codex daily`, `monthly`, and `session` are drop-in replacements for the ccusage codex commands, reading from your local `~/.codex/sessions/`. `cctally codex weekly` adds a subscription week rollup, and `cctally codex quota` shows your native Codex rate limit windows.
|
|
140
130
|
|
|
141
131
|
<p align="center">
|
|
142
|
-
<img src="docs/img/cli-
|
|
143
|
-
<br>
|
|
144
|
-
<em>The same data in the terminal, refreshed live.</em>
|
|
132
|
+
<img src="docs/img/cli-codex-daily.svg" alt="cctally codex daily: Codex cost by day">
|
|
145
133
|
</p>
|
|
146
134
|
|
|
147
|
-
See [docs/commands/
|
|
148
|
-
|
|
149
|
-
## Shareable reports
|
|
150
|
-
|
|
151
|
-
All eight reporting commands (`report`, `daily`, `monthly`, `weekly`, `forecast`, `project`, `five-hour-blocks`, `session`) can render to shareable Markdown, HTML, or SVG with `--format`, a light/dark `--theme`, and `--output` / `--copy` / `--open`. Project names anonymize by default (`--reveal-projects` opts in), so you can post a snapshot without leaking where you work. See [docs/commands/share.md](docs/commands/share.md).
|
|
152
|
-
|
|
153
|
-
## Codex parity
|
|
154
|
-
|
|
155
|
-
If you also use OpenAI's Codex CLI, `cctally codex daily / monthly / session` are drop-ins for `ccusage codex daily / monthly / session`, reading from `~/.codex/sessions/`. The flat `codex-*` forms (drop-ins for the standalone `ccusage-codex` binary) stay as aliases, and `cctally codex weekly` adds a subscription-week rollup that upstream doesn't have. See [docs/commands/codex.md](docs/commands/codex.md).
|
|
135
|
+
See [docs/commands/codex.md](docs/commands/codex.md) and [docs/commands/codex-quota.md](docs/commands/codex-quota.md).
|
|
156
136
|
|
|
157
|
-
##
|
|
137
|
+
## Terminal UI
|
|
158
138
|
|
|
159
|
-
|
|
139
|
+
Prefer to stay in the terminal, or working over SSH? `cctally tui` shows the same live data as a refreshing terminal dashboard. It is the one feature that needs the optional `rich` library; everything else runs on a plain Python install.
|
|
160
140
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
141
|
+
<p align="center">
|
|
142
|
+
<img src="docs/img/cli-tui.svg" alt="cctally tui: live terminal dashboard">
|
|
143
|
+
</p>
|
|
164
144
|
|
|
165
|
-
|
|
145
|
+
See [docs/commands/tui.md](docs/commands/tui.md).
|
|
166
146
|
|
|
167
|
-
##
|
|
147
|
+
## ccusage compatibility
|
|
168
148
|
|
|
169
|
-
|
|
149
|
+
cctally started as a local replacement for [ccusage](https://github.com/ryoppippi/ccusage) and stays drop-in compatible: `cctally claude <cmd>` and `cctally codex <cmd>` accept your ccusage commands verbatim. It is also fast: first table on 30 days of session data took about 2.6 seconds against about 31 seconds for ccusage (about 12x, measured 2026-05-05; methodology in [bench/README.md](https://github.com/omrikais/cctally/blob/main/bench/README.md)).
|
|
170
150
|
|
|
171
|
-
|
|
151
|
+
## Privacy
|
|
172
152
|
|
|
173
|
-
|
|
174
|
-
Footnote target uses an absolute URL because GitHub's relative-link
|
|
175
|
-
rewriter doesn't traverse into GFM footnote `<li>` content; on the
|
|
176
|
-
repo home page (`/omrikais/cctally`, no trailing slash) the browser
|
|
177
|
-
would resolve `bench/README.md` against the page URL and produce
|
|
178
|
-
`/omrikais/bench/README.md` - a broken path. Regular paragraph links
|
|
179
|
-
are unaffected.
|
|
180
|
-
-->
|
|
181
|
-
[^bench]: Methodology and reproduction: [`bench/README.md`](https://github.com/omrikais/cctally/blob/main/bench/README.md).
|
|
153
|
+
Everything runs locally against your own `~/.claude` and `~/.codex` data; session content is never uploaded. The only telemetry is an anonymous, opt-out install-count beat (a rotating one-way token, the version, and a coarse OS family, at most once a day). Turn it off any time with `cctally telemetry off`. The full transparency page is [docs/telemetry.md](docs/telemetry.md).
|
|
182
154
|
|
|
183
155
|
## Documentation
|
|
184
156
|
|
|
185
157
|
- [Installation](docs/installation.md): symlinks, status-line wiring, Python version.
|
|
186
|
-
- [Configuration](docs/configuration.md):
|
|
158
|
+
- [Configuration](docs/configuration.md): config.json shape and week-start rules.
|
|
187
159
|
- [Architecture](docs/architecture.md): data flow, caches, week boundaries.
|
|
188
|
-
- [
|
|
189
|
-
- [Telemetry](docs/telemetry.md): the anonymous install-count beat, in full — and how to opt out.
|
|
160
|
+
- [Telemetry](docs/telemetry.md): the anonymous install-count beat, in full.
|
|
190
161
|
- [Command reference](docs/commands/): one page per subcommand.
|
|
191
162
|
|
|
192
163
|
## License
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -5569,6 +5569,57 @@ def open_conversations_db(*, attach_cache: bool = True) -> sqlite3.Connection:
|
|
|
5569
5569
|
return conn
|
|
5570
5570
|
|
|
5571
5571
|
|
|
5572
|
+
def read_session_titles_bounded(
|
|
5573
|
+
session_ids,
|
|
5574
|
+
*,
|
|
5575
|
+
timeout_s: float = 0.05,
|
|
5576
|
+
) -> dict:
|
|
5577
|
+
"""{session_id: title} for the dashboard Sessions panel — bounded, fail-soft.
|
|
5578
|
+
|
|
5579
|
+
The Sessions panel is an ACCOUNTING surface that shows one piece of
|
|
5580
|
+
transcript-derived decoration (the session title). #320 made the transcript
|
|
5581
|
+
corpus an independent store precisely so accounting can never wait on it, so
|
|
5582
|
+
this read is deliberately not ``open_conversations_db``: that opener applies
|
|
5583
|
+
the schema, runs the migration dispatcher, attaches ``cache.db``, and carries
|
|
5584
|
+
the 15s store-policy ``busy_timeout`` — a locked or rebuilding store would
|
|
5585
|
+
stall the whole sync tick before a fail-soft caller could give up.
|
|
5586
|
+
|
|
5587
|
+
Instead: one RAW ``mode=ro`` connection with a ``timeout_s`` busy timeout
|
|
5588
|
+
(the same idiom the Codex sessions rows use for Codex's own
|
|
5589
|
+
``state_5.sqlite``), reading only the two INDEXED title sources via
|
|
5590
|
+
``session_titles_indexed_map`` — never the windowed ``conversation_messages``
|
|
5591
|
+
scan. Every failure path — no store on disk, locked store, absent tables,
|
|
5592
|
+
corruption — degrades to ``{}`` and the panel renders its em-dash fallback,
|
|
5593
|
+
which self-heals on a later tick. Never creates the store.
|
|
5594
|
+
"""
|
|
5595
|
+
ids = [sid for sid in dict.fromkeys(session_ids or ()) if sid]
|
|
5596
|
+
if not ids:
|
|
5597
|
+
return {}
|
|
5598
|
+
path = _cctally_core.CONVERSATIONS_DB_PATH
|
|
5599
|
+
try:
|
|
5600
|
+
if not path.is_file():
|
|
5601
|
+
return {}
|
|
5602
|
+
uri = f"{path.resolve().as_uri()}?mode=ro"
|
|
5603
|
+
except OSError:
|
|
5604
|
+
return {}
|
|
5605
|
+
conn: sqlite3.Connection | None = None
|
|
5606
|
+
try:
|
|
5607
|
+
conn = sqlite3.connect(uri, uri=True, timeout=max(timeout_s, 0.0))
|
|
5608
|
+
return dict(
|
|
5609
|
+
_load_lib("_lib_conversation_query").session_titles_indexed_map(
|
|
5610
|
+
conn, ids,
|
|
5611
|
+
)
|
|
5612
|
+
)
|
|
5613
|
+
except (sqlite3.Error, OSError):
|
|
5614
|
+
return {}
|
|
5615
|
+
finally:
|
|
5616
|
+
if conn is not None:
|
|
5617
|
+
try:
|
|
5618
|
+
conn.close()
|
|
5619
|
+
except sqlite3.Error:
|
|
5620
|
+
pass
|
|
5621
|
+
|
|
5622
|
+
|
|
5572
5623
|
def _import_legacy_conversation_rows(conn: sqlite3.Connection) -> None:
|
|
5573
5624
|
"""Bridge pre-028/compatibility rows into an empty conversation store.
|
|
5574
5625
|
|
|
@@ -52,6 +52,9 @@ from _cctally_core import (
|
|
|
52
52
|
_get_alerts_config,
|
|
53
53
|
_get_budget_config,
|
|
54
54
|
)
|
|
55
|
+
from _lib_dashboard_sources import (
|
|
56
|
+
dashboard_resource_key as _dashboard_resource_key,
|
|
57
|
+
)
|
|
55
58
|
from _lib_display_tz import _compute_display_block, format_display_dt
|
|
56
59
|
from _lib_pricing import _chip_for_model, _short_model_name
|
|
57
60
|
|
|
@@ -1647,9 +1650,76 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1647
1650
|
**sys.modules["_cctally_dashboard"]._channel_env_fragment(),
|
|
1648
1651
|
}
|
|
1649
1652
|
envelope.update(_source_bundle_to_envelope(getattr(snap, "source_bundle", None)))
|
|
1653
|
+
_overlay_claude_source_session_titles(envelope, snap, transcripts_visible)
|
|
1650
1654
|
return envelope
|
|
1651
1655
|
|
|
1652
1656
|
|
|
1657
|
+
def _claude_source_session_rows(envelope: dict) -> list:
|
|
1658
|
+
"""The SOURCE-scoped Claude session row lists, both of the places the All
|
|
1659
|
+
tab may read them from (``sourceRows.ts::collectSourceSessionRows`` prefers
|
|
1660
|
+
the nested provider payload and falls back to the sibling source entry)."""
|
|
1661
|
+
sources = envelope.get("sources")
|
|
1662
|
+
if not isinstance(sources, Mapping):
|
|
1663
|
+
return []
|
|
1664
|
+
out = []
|
|
1665
|
+
candidates = [(sources.get("claude") or {}).get("data")]
|
|
1666
|
+
all_data = (sources.get("all") or {}).get("data")
|
|
1667
|
+
if isinstance(all_data, Mapping):
|
|
1668
|
+
providers = all_data.get("providers")
|
|
1669
|
+
if isinstance(providers, Mapping):
|
|
1670
|
+
candidates.append(providers.get("claude"))
|
|
1671
|
+
for data in candidates:
|
|
1672
|
+
if not isinstance(data, Mapping):
|
|
1673
|
+
continue
|
|
1674
|
+
sessions = data.get("sessions")
|
|
1675
|
+
if not isinstance(sessions, Mapping):
|
|
1676
|
+
continue
|
|
1677
|
+
rows = sessions.get("rows")
|
|
1678
|
+
if isinstance(rows, list):
|
|
1679
|
+
out.append(rows)
|
|
1680
|
+
return out
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
def _overlay_claude_source_session_titles(
|
|
1684
|
+
envelope: dict, snap, transcripts_visible: bool,
|
|
1685
|
+
) -> None:
|
|
1686
|
+
"""Inject the gated session title into the source-scoped Claude rows (#363).
|
|
1687
|
+
|
|
1688
|
+
The Sessions card reads a DIFFERENT payload per tab: the legacy
|
|
1689
|
+
``sessions.rows`` block on the Claude tab, but the source-scoped rows on the
|
|
1690
|
+
All tab. Those source rows are projected into the published source bundle on
|
|
1691
|
+
the sync thread — one frozen bundle shared by every SSE client — so the
|
|
1692
|
+
bundle deliberately carries NO transcript content: a build-time gate
|
|
1693
|
+
decision cannot be right for every later connection, and the projection is
|
|
1694
|
+
fed a fail-closed envelope precisely so nothing leaks into it.
|
|
1695
|
+
|
|
1696
|
+
The title therefore rides the SAME per-request gate as the legacy block, by
|
|
1697
|
+
injection here rather than publication upstream: gate closed → the key stays
|
|
1698
|
+
absent, exactly as before. Injection (rather than publish-then-strip) is what
|
|
1699
|
+
makes it fail closed — a missed site omits a title, it never leaks one.
|
|
1700
|
+
|
|
1701
|
+
Safe to mutate: ``_source_wire_value`` rebuilds plain dicts/lists per call,
|
|
1702
|
+
so these rows are this request's own copies.
|
|
1703
|
+
"""
|
|
1704
|
+
if not transcripts_visible:
|
|
1705
|
+
return
|
|
1706
|
+
titles = {}
|
|
1707
|
+
for session in getattr(snap, "sessions", ()) or ():
|
|
1708
|
+
session_id = getattr(session, "session_id", None)
|
|
1709
|
+
title = getattr(session, "title", None)
|
|
1710
|
+
if isinstance(session_id, str) and session_id and title is not None:
|
|
1711
|
+
titles[_dashboard_resource_key("session", "claude", session_id)] = title
|
|
1712
|
+
if not titles:
|
|
1713
|
+
return
|
|
1714
|
+
for rows in _claude_source_session_rows(envelope):
|
|
1715
|
+
for row in rows:
|
|
1716
|
+
if not isinstance(row, dict):
|
|
1717
|
+
continue
|
|
1718
|
+
title = titles.get(row.get("key"))
|
|
1719
|
+
if title is not None:
|
|
1720
|
+
row["title"] = title
|
|
1721
|
+
|
|
1722
|
+
|
|
1653
1723
|
def _session_detail_to_envelope(detail: "TuiSessionDetail") -> dict:
|
|
1654
1724
|
"""Serialize TuiSessionDetail for GET /api/session/:id (spec §3.2, §4.6.4).
|
|
1655
1725
|
|
|
@@ -1504,7 +1504,16 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
|
|
|
1504
1504
|
continue
|
|
1505
1505
|
forecast = row.get("forecast") if isinstance(row.get("forecast"), Mapping) else {}
|
|
1506
1506
|
current = row.get("current_percent")
|
|
1507
|
-
|
|
1507
|
+
# #350 spec §3.6 — "Projections blank. Actuals stay." BOTH the build
|
|
1508
|
+
# and the idle clock deliberately preserve `projected_percent`
|
|
1509
|
+
# alongside a non-ok `status`, and this site formatted it with no
|
|
1510
|
+
# status check at all. That was masked while a stale Codex source
|
|
1511
|
+
# collapsed to `unavailable` here; §3.4 keeps the source coherent, so
|
|
1512
|
+
# a shared forecast would otherwise publish a stale projection.
|
|
1513
|
+
projected = (
|
|
1514
|
+
forecast.get("projected_percent")
|
|
1515
|
+
if forecast.get("status") == "ok" else None
|
|
1516
|
+
)
|
|
1508
1517
|
rows.append(ls.Row(cells={
|
|
1509
1518
|
"limit": ls.TextCell(str(row.get("label") or "Codex quota")),
|
|
1510
1519
|
"current": ls.TextCell("—" if current is None else f"{float(current):.1f}%"),
|