current.pullRequests survived both sanitizeForSharing and
redactProjectNames(include_project_names: false) verbatim, so each row's
full GitHub url and owner/repo#123 label named the repository to a paired
peer device and to a name-redacted MCP client — the same class of leak
Sharing drops the block like it drops the project, session and per-branch
rows. The MCP pass pseudonymizes url and label from the url (the
aggregation key) with the same salted, domain-separated hash as the other
pseudonyms, so rows referring to the same PR still line up and every
number stays untouched.
Closes#1346
redactProjectNames rebuilt every row with a spread and replaced only the
fields it knew about, so identities added to the payload later rode
through untouched with include_project_names: false:
- topSessions[].projectKey, the raw sessions-list row key, is a
dash-encoded absolute working directory (-Users-<user>-Projects-<repo>).
Hash it with the project pseudonym, the way topProjects[].id already is.
- topSessions[].sessionId and topProjects[].sessionDetails[].sessionId are
the drill-through identity; a Codex id also carries the rollout
timestamp this pass blanks out of `date`. Hash them as session-xxxxxx.
provider stays: it is claude/codex, not identity.
- byBranch[].branch is the raw git branch, which encodes ticket ids,
customer names and feature codenames. Give each distinct branch a
stable branch-xxxxxx pseudonym and keep the row's numbers. A null
branch stays null: that is unbranched spend, not a name.
The new pseudonyms are domain-separated inside the digest so a branch and
a project of the same name do not share a hash; the project pseudonym
itself is unchanged. redactSessionDetails now takes its type from the
payload instead of a hand-copied inline type that had drifted from it,
which is why the leak type-checked. include_project_names: true is
unchanged.
Refs #1325, #1321
The Capacity Dock hover panel becomes a glance: a provider header, the
sessions running right now as pills with their context rings, a Today
strip, and a row of quota-window percentages, separated by hairlines.
Window percentages stay plain text until 70 percent so a healthy row
reads as text rather than a wall of colour. The panel stays 350pt wide;
every block height is a whole point and is summed into the panel frame,
since the hosting view does not size itself.
Running sessions are new CLI surface. `liveSessions` is add-only on the
menubar payload: sessions whose transcript was appended inside a 120s
window, carrying the branch, the model that answered last, and the
context that turn reported. Absence means "unknown", so the section
hides rather than claiming nothing is running. The block names projects
and branches, so sharing and MCP output strip it.
Today's totals needed no CLI change; the app simply was not decoding the
cache token counts the payload already carried.
* feat(dashboard): add granular usage timelines
* fix(dashboard): readable timeline legend and month-scale buckets
Session legend labels now use the real projectPath's last two segments
(app/web) instead of the sanitized project dir, which rendered as an
unreadable -Users-name-... dump and truncated in the legend.
Hourly buckets now cap at 8 days; longer ranges bucket daily. A 30-day
window rendered 720 overlapping hourly spikes, unreadable as a chart.
The browser no longer gives the backend session_other/model_other
aggregate a top-N slot, which rendered two identical Other legend
entries whenever the backend fold out-ranked a real series.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
- Add per-install random salt to pseudonym hash to prevent rainbow table
reversal of common project names
- Redact session details (dates, models) when project names are hashed
to prevent re-identification via cost fingerprinting
- Return structuredContent in error responses to satisfy strict MCP
clients that validate against declared outputSchema
- Remove pre-warm fire-and-forget that raced with the inflight map
- Fix empty markdown table producing malformed cells
- Add tests for session detail redaction and cross-field consistency