Skip to content

feat(dashboard): cumulative cost chart + per-skill spend breakdown#5

Open
ezwep wants to merge 4 commits into
mainfrom
feat/cumulative-cost-and-skill-breakdown
Open

feat(dashboard): cumulative cost chart + per-skill spend breakdown#5
ezwep wants to merge 4 commits into
mainfrom
feat/cumulative-cost-and-skill-breakdown

Conversation

@ezwep

@ezwep ezwep commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Two additions to the usage overview to make spend obvious at a glance — both directly motivated by realising the dashboard was understating real cost by 10× until PR #4 landed.

What you see

Cumulative cost — running total alongside the existing daily-cost chart. Header chip shows the final total so you do not have to read off the right edge.

Cost by skill / slash-command — groups sessions by the leading slash-command in their title (`/gsd-resume-work`, `/gh-controlcenter`, etc.) with a fallback "(interactive)" bucket. Rows show total cost, share-of-range percentage, and session count, bars scaled to the heaviest skill.

On the live index (last 7 days):
```
/gsd-resume-work $954.50 (28.0%) 32 sess
(interactive) $748.64 (22.0%) 588 sess
/gh-controlcenter-join $569.25 (16.7%) 16 sess
/gh-controlcenter $169.76 ( 5.0%) 11 sess
/gsd-execute-phase $136.00 ( 4.0%) 3 sess
/plan $126.96 ( 3.7%) 6 sess
```

Implementation notes

  • `_build_cumulative_cost` reuses the existing per-day rollup — zero extra queries.
  • `_build_skill_breakdown` does one extra grouped SELECT and classifies in Python via a single compiled regex (`_SKILL_RE`).
  • Both new fields piggyback on the existing `get_dashboard_payload` response — no API signature change.
  • `_extract_skill_key` is reusable by the project digest if we want a similar per-project breakdown later.

Test plan

  • `get_dashboard_payload` returns `cumulative_cost` and `skill_breakdown` arrays
  • Cumulative total matches sum of daily costs
  • Skill percentages sum to 100% within rounding
  • Interactive fallback bucket aggregates correctly
  • Manual UI smoke test in pywebview window

ezwep added 4 commits April 27, 2026 10:56
- Replace UNION-then-date-sort with two FTS5 queries that yield BM25
  scores per matching session. Title hits get a 200pt boost so a query
  match in the conversation title outranks a body-only match.
- Add a "Best match" sort option (default for active searches) and
  preserve existing date/token/cost sorts when the user picks them.
- Pass the FTS5 snippet() output (with <mark> tags) back to the UI,
  rendered as a highlighted excerpt under each card so users can see
  why a session matches.
- Smarter query parser: multi-word input becomes implicit-AND with
  prefix matching on each token; quoted phrases, NEAR, NOT, and column
  filters still pass through to FTS5 verbatim.
- Surface invalid FTS syntax (e.g. unbalanced quotes) as an inline
  status banner under the search input instead of silently returning
  zero results.
- Return shape changed from list to {conversations, search} so the UI
  can show match counts and errors. Backward-compatible array fallback
  in the JS callsite.
When a single project is selected, the main panel now defaults to a
heuristic project digest that answers "what have I been working on
here." The existing usage dashboard is preserved as a peer sub-view
behind a small toggle.

Backend (dashboard_data.py):
- New get_project_digest() sources entirely from the conversations
  table (one row per session) so it stays fast even on a 53k-message
  index — measured ~12ms on the largest project (612 sessions).
- Computes a momentum summary: total sessions, active days, tokens,
  cost, sessions in last 7d vs prior 7d, plus a trend label
  ("picking up" / "steady" / "winding down" / "new" / "dormant").
- 42-day daily activity timeline with session count, tokens, cost.
- Heuristic topic extraction: tokenizes titles + excerpts of the most
  recent ~60 sessions, filters a curated stopword set (English + Dutch
  + dev/Claude-Code jargon), returns top 10 terms by frequency.
- Returns {unavailable: true} for "all" / null scopes so the UI can
  fall back to the existing dashboard view cleanly.

API (app.py):
- New ConversationAPI.get_project_digest() exposed to the webview
  frontend, mirroring the get_dashboard_payload pattern.

Frontend (templates/index.html):
- New state.mainSubView ('digest' | 'overview') persisted to
  localStorage. When a single project is selected the digest is the
  default; "All projects" forces the overview.
- renderMainPanel routes to renderProjectDigest when in dashboard view,
  a real project is selected, and the sub-view is 'digest'.
- renderProjectDigest builds a hero card (trend chip + 6 headline
  metrics), an activity timeline bar strip, a topic chip cloud sized by
  frequency, and a clickable recent-sessions list that opens the
  existing session detail view.
- All user-derived strings escaped via existing escHtml(); no new
  dependencies; no indexer or schema changes; fully offline.

Risks addressed:
- state.view regression: every existing assignment to state.view in
  the project click handler, escape handler, and overview button click
  still routes correctly; the dashboard remains the fallback for "all".
- Performance: digest only reads conversations (not messages); topic
  extraction capped at 60 sessions and 10 terms.
- XSS: title/excerpt/topic content all interpolated through escHtml().
Three related improvements bundled together so the dashboard keeps
working when the index is broken and can show the full history when
"last 90 days" is too narrow.

1. **All-time range option**
   - dashboard_data.py: new _earliest_indexed_day() helper plus an
     "all" branch in _resolve_range() that anchors start_day to the
     oldest assistant-message timestamp (scoped to the selected
     project). End day stays "tomorrow". Label reports the actual
     span in days.
   - templates/index.html: extra <option value="all"> on the range
     select; loadUiState() accepts it as a persisted value.
   - Performance: measured ~600 ms for 1812 sessions across 97 days
     on the live index — well within the existing rollup budget.

2. **WAL + synchronous=NORMAL + integrity check** (indexer.py)
   - PRAGMA synchronous=NORMAL pairs with the already-enabled WAL to
     keep durability while reducing fsync churn.
   - New check_db_integrity() runs PRAGMA integrity_check and
     returns (ok, message). Called once at startup from
     ConversationAPI._ensure_db.
   - Without this the corrupt-DB event from today silently produced
     tracebacks in stderr with zero UI signal.

3. **UI surfacing of DB status** (app.py + templates/index.html)
   - ConversationAPI now caches {ok, message, checked_at} from the
     initial integrity check and exposes it in get_stats().
   - New recheck_db_integrity() method for on-demand verification.
   - get_stats() wraps its query batch in try/except sqlite3.DatabaseError
     so a corrupt DB no longer crashes the bridge — it updates the
     status and returns zeroed counters.
   - Frontend adds a red banner above the statusbar when integrity
     fails, with Recheck and Rebuild-index buttons. The statusbar
     gains a "DB: ok / DB: corrupt" indicator that's green when fine.
Two additions to the usage overview to make spend obvious at a glance:

1. **Cumulative cost chart** — running total across the selected range,
   sitting next to the daily-cost chart. Header chip shows the final
   total so you don't have to read off the right edge.

2. **Cost by skill / slash-command** — groups sessions by the leading
   slash-command in their title (e.g. /gsd-resume-work, /gh-controlcenter)
   with a fallback bucket for free-form prompts ("(interactive)"). Each
   row shows total cost, share-of-range percentage, and session count;
   bars are scaled to the heaviest skill so the dominant ones jump out.

Implementation notes:
- _build_cumulative_cost reuses the existing per-day rollup (zero extra
  queries).
- _build_skill_breakdown does one extra grouped SELECT and classifies
  in Python via a single compiled regex (_SKILL_RE).
- Both new fields piggyback on the existing get_dashboard_payload
  response; no API method signature change.
- _extract_skill_key is also reusable by the project digest down the
  line if we want a similar per-project breakdown.
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