cctally 1.82.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 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.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
+
8
20
  ## [1.82.0] - 2026-07-24
9
21
 
10
22
  ### Added
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <strong>Local-first usage tracker for Claude Code on Pro/Max - live dashboard, conversation viewer, weekly cost-per-percent trend, quota forecast, and threshold alerts. ccusage-compatible.</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's Pro/Max plans meter you on a weekly quota, but the only signal you get is a percentage that creeps up until you cap. `cctally` turns that percentage into something you can act on: it reads your local session logs, computes cost in-process, and shows you **how much each percent of quota is actually costing you, whether you're on track to cap this week, and exactly where the spend is going** - across a live web dashboard, a read-only conversation viewer, and a full set of CLI reports. Everything runs locally against your own `~/.claude` data. No account, no API key, no cloud processing — cctally never uploads your session data, and its only telemetry is an anonymous, opt-out [install count](#privacy--telemetry).
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
- **Requirements:** Python 3.11+, macOS or Linux, Claude Code installed and run at least once.
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
- # or npm
41
+ # or npm
34
42
  npm install -g cctally && cctally setup
35
43
 
36
- # or from source
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 right away — you don't need `cctally setup` first. Straight after an install, `npx cctally daily` (or, once it's global, `cctally daily`) reads your existing `~/.claude` session logs directly and prints a cost table; `cctally dashboard` and `cctally tui` work the same way. `cctally setup` is what adds *auto-recording* the live status-line quota percentage and the hooks that keep data flowing as you work — so run it once you want continuous tracking rather than on-demand reports.
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 setup --status # verify hooks + symlinks
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
- For status-line integration and tuning, see [docs/installation.md](docs/installation.md) and [docs/configuration.md](docs/configuration.md).
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 at `localhost:8789` that updates live as you work - no refresh, no polling. Eleven panels cover the whole picture: **current week**, **forecast**, **$/1% trend**, **recent sessions**, **weekly**, **monthly**, **5-hour blocks**, **daily heatmap**, **projects**, **cache report**, and **recent alerts**. Any panel expands into a focused view, sessions are filterable and searchable, and a settings drawer tunes alerts and display options on the fly. It runs only on your own machine by default; one flag opens it to the other devices on your network when you want that.
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 - here, twelve weeks of cost per percent.</em>
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 goes amber.</em>
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
- It only reads your transcripts - it never changes them - and everything stays on your own machine. Nothing is uploaded, and the viewer is reachable only from your computer unless you choose to share the dashboard with your network.
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 subscription week's spend as **dollars per percent of quota used**, so you can see your spending efficiency trend week over week - and whether this week is pulling above or below the line - instead of staring at a raw percentage. Persisted to SQLite, so the comparison survives across runs.
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: $ per 1% weekly trend">
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 & budget
107
+ ## Forecast, budget, and alerts
110
108
 
111
- `cctally forecast` projects where your weekly percentage lands at the next reset - using both a week-average and a recent-24h rate - and tells you the daily $/% budget you'd need to stay under the 100% and 90% ceilings. When the data is thin it says so (`LOW CONF`) rather than guessing.
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
- For dollar budgets, `cctally budget` tracks an equivalent-vs-actual spend target over a configurable calendar period - per vendor, for both Claude and Codex - with a pace projection and an `ok`/`warn`/`over` verdict. See [docs/commands/forecast.md](docs/commands/forecast.md) and [docs/commands/budget.md](docs/commands/budget.md).
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
- Get a native desktop notification the moment you cross a percent milestone, so a runaway week can't sneak up on you. Alerts fire on weekly percent, 5-hour blocks, and budget thresholds, with three severity levels (info / warn / critical) and native popups on macOS and Linux - or run a command of your own. `cctally alerts test` confirms it's wired up. See [docs/commands/alerts.md](docs/commands/alerts.md).
117
+ ## 5-hour blocks
124
118
 
125
- ## 5-hour analytics
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
- ## Live terminal UI
127
+ ## Codex
138
128
 
139
- Prefer to stay in the terminal, or working over SSH? `cctally tui` shows the same live data as a refreshing terminal dashboard. It's the one feature that needs the optional `rich` library; every other command runs on a plain Python install with nothing else to set up.
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-tui.svg" alt="cctally tui: live terminal dashboard">
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/tui.md](docs/commands/tui.md).
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
- ## Diagnostics & upkeep
137
+ ## Terminal UI
158
138
 
159
- `cctally doctor` gives you a read-only health check - install, hooks, sign-in, database, data freshness, pricing, and safety - and the same status shows up in the dashboard. `cctally pricing-check` warns you when the built-in model pricing is getting stale, `cctally db` manages the local database, and `cctally update` keeps your install current. The local cache is always safe to delete or rebuild.
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
- ## Privacy & telemetry
162
-
163
- cctally sends an anonymous, opt-out **install-count beat** — at most once a day — so the project can gauge how many people actually use it (npm and GitHub numbers are drowned in bots and mirrors). The entire payload is a one-way token that rotates every month (derived from a random local id that never leaves your machine and can't be recovered from the token), the cctally version, and a coarse OS family (`macos`/`linux`/`windows`/`other`). No identity, file paths, prompts, usage data, or stored IP — ever. The first beat is held for at least 24 hours after first run, so you always have a window to opt out first.
141
+ <p align="center">
142
+ <img src="docs/img/cli-tui.svg" alt="cctally tui: live terminal dashboard">
143
+ </p>
164
144
 
165
- Turn it off any time with `cctally telemetry off` (or `cctally config set telemetry.enabled false`, `CCTALLY_DISABLE_TELEMETRY=1`, or the community-standard `DO_NOT_TRACK=1`); it's also off automatically in dev checkouts. Run `cctally telemetry` to see the current state and exactly what would be sent. The full transparency page — token construction, retention, and an honest threat model — is [docs/telemetry.md](docs/telemetry.md).
145
+ See [docs/commands/tui.md](docs/commands/tui.md).
166
146
 
167
- ## A faster, local-first ccusage
147
+ ## ccusage compatibility
168
148
 
169
- `cctally` started as a local replacement for [`ccusage`](https://github.com/ryoppippi/ccusage) and stays compatible at the level of common flows: `cctally claude <cmd>` is a drop-in for `ccusage claude <cmd>` (and `cctally codex <cmd>` for `ccusage codex <cmd>`), with the flat forms (`cctally daily`, `cctally codex-daily`, …) kept as aliases. Paste your ccusage commands verbatim - then reach for the dashboard, forecast, trend, conversation viewer, and alerts that ccusage doesn't have.
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
- It's also fast. Pricing is built in and computed in-process from a local SQLite cache, with no external tools to spawn. First table on 30 days of session data: **~2.6s (cctally) vs ~31s (ccusage)**, about 12× faster. Measured by `bench/cctally-vs-ccusage.sh` on macOS arm64, 2026-05-05; your numbers will vary.[^bench]
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): `config.json` shape and week-start rules.
158
+ - [Configuration](docs/configuration.md): config.json shape and week-start rules.
187
159
  - [Architecture](docs/architecture.md): data flow, caches, week boundaries.
188
- - [Runtime data](docs/runtime-data.md): what lives in `~/.local/share/cctally/`.
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
@@ -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
- projected = forecast.get("projected_percent")
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}%"),