Skip to content

Daily Activity Summaries view#2

Open
paump wants to merge 26 commits into
mainfrom
feature/daily-activity-summaries
Open

Daily Activity Summaries view#2
paump wants to merge 26 commits into
mainfrom
feature/daily-activity-summaries

Conversation

@paump
Copy link
Copy Markdown

@paump paump commented Apr 27, 2026

Summary

  • Daily Activities view — clicking a day in the dashboard expands a per-project list of inferred activity bullets (2-5 per cell). Summaries come from the local claude CLI running Haiku and are cached in a new daily_summaries table keyed by sha256 of the day's prompts.
  • Per-cell parallel fetch — clicking a day fans out one /api/cell-summary request per project, so summaries stream in instead of blocking on a sequential per-cell loop.
  • Turn-based day cost — day-row cost now matches the sum of per-cell costs (sessions that span multiple days no longer pile their entire cost onto their last day).

Notable design choices

  • No eager pre-summarization pass. The original plan summarized the top 20% cost cells at startup; in practice it added a 30-60 s wall before the dashboard came up and duplicated work the on-click endpoint already does. Lazy summarization on click is fast enough — the sha256 cache makes re-clicks instant.
  • CLI subprocess uses --output-format json without --json-schema — the schema flag returned an empty result field on the current Claude Code CLI. We parse the JSON object out of the freeform result string instead.
  • Idempotent daily-list render — the 30 s auto-refresh updates day-row metadata in place instead of rebuilding the DOM, so an open day stays open and in-flight cell-summary fetches aren't abandoned.

New surface

  • New module summarizer.py (prompt collection, noise filter, claude subprocess, cache write)
  • New daily_summaries table (auto-created)
  • New endpoints: /api/daily-summaries?date=… (instant, marks pending cells), /api/cell-summary?date=&cwd= (per-cell)
  • New env var: SUMMARY_MODEL (default: haiku)
  • Requires the claude CLI on PATH; absence shows a per-cell notice instead of breaking the dashboard

Test plan

  • python3 -m pytest tests/ -q — 136 tests pass
  • python3 cli.py dashboard — dashboard comes up immediately (no eager-pass wall)
  • Open a day with multiple projects — each project's spinner is replaced with bullets independently as summaries land
  • Re-open the same day — bullets render instantly from cache (no spinner)
  • Day-row cost matches the sum of cells underneath
  • Leave a day expanded for 30+ s — auto-refresh does not collapse it
  • mv $(which claude) /tmp/ and reload — dashboard still renders, cells show a "claude CLI required" notice instead of crashing

🤖 Generated with Claude Code

paump and others added 26 commits April 27, 2026 13:08
10 TDD-shaped tasks covering the new summarizer module, scanner table,
cli eager pass, dashboard endpoint, UI, and v0.3.0-launchmetrics.1 tag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move `import time` to the top of the file with other module-level
imports, remove the two inline `import scanner, time` duplicates, and
fix the f-string in `_seed_jsonl_for_cell` to use `{i:02d}` so minute
values are always two digits (prevents malformed timestamps for i>=10).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stub threading.Thread in both cmd_dashboard tests so the browser-open
  daemon thread never actually runs during test execution
- Assert projects_dirs is passed correctly to run_eager_pass
- Add docstring + assert progress format (1 / 3, no \\r) to the
  stderr-progress test
- Add comment explaining the done % 5 == 0 magic number in cli.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns cached summaries for a given date and lazily summarizes any
(date, cwd) cell with activity but no cached summary via summarize_cell.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ate HTTP path

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…CSS tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the existing scan_meta _table_exists guard pattern. Protects
the rare case where the dashboard is started against a usage.db that
predates v0.3.0 without first running a scan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude Code's per-project JSONL directories under ~/.claude/projects/
encode every "/", "." and whitespace in the cwd as "-". Our
_encoded_dirname only replaced "/", so for any cwd containing dots
(e.g. "/Users/pau.montero/...") or spaces (e.g. ".../AIpril
retrospectives"), collect_prompts walked into a non-existent dir and
returned no prompts. The dashboard then surfaced "Summary unavailable:
no_prompts" for every cell.

Add a regression test for the encoding and an end-to-end test that
seeds a dotted-cwd dir and verifies collect_prompts finds it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-type JSONL records contain a lot of non-prompt text that Claude
Code captures from the input stream: slash-command wrappers
(<command-name>...), bash invocations and stdout/stderr blocks,
local-command artefacts, task-notifications and system-reminders that
land in user records, the [Request interrupted by user] marker, and
the auto-context-continuation prelude. Inferring activities from this
material wastes tokens and produces nonsense.

Add a NOISE_PREFIXES tuple and skip any user prompt whose stripped
text starts with one of those tags. Real prose still falls through.

Verified against the live JSONL: collected prompts went from
4 KB of mixed garbage to 1.7 KB of legitimate user messages
(real questions, design discussion, task hand-offs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Expanding a day was hanging on a single "Summarizing…" spinner for
2-5 minutes because /api/daily-summaries ran summarize_cell
synchronously for every uncached (date, cwd) before sending the
response — sequential 30-60 s claude -p calls add up fast.

Split the API: /api/daily-summaries now returns the cell list
instantly with pending=true on uncached cells. The new
/api/cell-summary?date=&cwd= endpoint runs summarize_cell for one
cell. The frontend fires these in parallel and replaces each pending
block as its summary arrives, so summaries stream in instead of all
or nothing.

Side benefit: the user sees structure (which projects worked that
day) immediately, even before any summary completes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Claude Code CLI returns an empty "result" field when invoked with
--json-schema (verified against the live CLI: result="", stop_reason
="end_turn", output_tokens=1317), which made every lazy summary fail
with parse_error.

Switch to instructing JSON output via the system prompt and parse the
result string ourselves, stripping optional ``` fences and falling
back to the first balanced {...} span. Also wrap the collected prompts
in a <prompts> block so Haiku treats them as data instead of
answering the last user message.

New error code "empty_result" to distinguish a CLI that returned no
content from a malformed payload.

Verified end-to-end: a 2.3 KB real prompt set now yields four
coherent activity bullets in ~25 s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
renderDailyList rebuilt the entire <details> list on every 30 s
auto-refresh, which collapsed any expanded day and abandoned the
in-flight /api/cell-summary requests for its cells. The visible
result was "day closes itself after a while" plus stale "No
activities inferred" labels for cells whose summarization never
got to land.

Make the renderer idempotent: update <summary> metadata on
existing rows in place, only build fresh DOM for genuinely new
dates, and remove rows for dates that fell out of the range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The daily list header used session.last_date attribution, which
credits an entire session to whatever day it ended on. Cells in
the expanded view use per-turn timestamps. For long-running
sessions that span multiple days, this made the day header
much higher than the sum of cells underneath — the user saw
\$120.92 in the header against \$26.32 of cells.

Switch the header cost to use the existing turn-grouped
daily_by_model data so it matches the per-cell sum exactly.
Project count keeps using sessions (it's a per-day count of
distinct projects, not a cost figure).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per user feedback: "if you are able to click a date and it works,
this is enough." The eager pass added a 30-60 s blocking step at
dashboard startup that pre-summarized the top-20% cost cells, then
duplicated work the lazy on-click endpoint already does. Worse, on
slower days users sat staring at "Generating activity summaries…"
before the dashboard even came up.

Lazy summarization is fast enough on its own: clicking a day fans
out one /api/cell-summary request per project in parallel and the
sha256 cache makes re-clicks instant. Drop the eager machinery,
the SUMMARY_MAX_CELLS env var, the rank_cells_by_cost ranking, and
the ★ "pre-summarized" UI marker that was only meaningful when
both passes coexisted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Recent Sessions used to show only the top 20 rows of the filtered
list with no way to drill in. Two changes here:

1. Pagination — render the full filtered list 50 rows per page with
   a prev/next pager. Page resets to 1 whenever the filter, sort, or
   auto-refresh changes the underlying list.

2. Click-to-expand — clicking a row inserts a detail row underneath
   showing 2-5 activity bullets summarizing what the user actually
   worked on in that session. Bullets come from the local claude CLI
   (Haiku via SUMMARY_MODEL) running over the session's user prompts,
   cached in a new session_summaries table keyed by session_id.

Reuses summarizer.py's prompt extraction, noise filter, run_claude
subprocess and prompt_hash cache logic — only the persistence shape
changes (one row per session_id instead of per (date, cwd)). Locates
the JSONL via cwd_hint when known, falls back to globbing every
project dir for <session_id>.jsonl.

Path-traversal guard on /api/session-summary?id= rejects ids
containing '/', '..' or whitespace before the value reaches the
filesystem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant