cctally 1.9.0 → 1.10.0
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 +24 -0
- package/bin/_cctally_dashboard.py +1095 -0
- package/bin/_cctally_tui.py +228 -10
- package/bin/_lib_share_templates.py +190 -0
- package/bin/_lib_view_models.py +20 -0
- package/bin/cctally +9 -0
- package/dashboard/static/assets/index-DUKjFlG8.js +18 -0
- package/dashboard/static/assets/index-Dp14ELVt.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-Dv5Dzag5.css +0 -1
- package/dashboard/static/assets/index-cWE5HB8O.js +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.10.0] - 2026-05-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Add Projects panel + modal to the dashboard — top-5 current-week leaderboard with horizontal bars and attributed `Used %`, a configurable `1w` / `4w` / `8w` / `12w` Projects modal with a stacked-area trend chart, 7-column per-project table (cost desc by default), and an in-place drill into model breakdown + recent sessions; two-way navigation with Sessions (clicking a project cell in Sessions opens the Projects modal pre-expanded on that project's drill, and clicking a session in the drill opens the SessionModal as a replace). New global `5` shortcut opens the Projects modal directly (the panel sits at index 4 of `DEFAULT_PANEL_ORDER`); `0` now opens whichever panel occupies position 10 (vim-style "10 wraps to 0"; `alerts` in the default order). A lazy `GET /api/project/<key>?weeks=N` endpoint mirrors the SessionModal stale-while-revalidate pattern — the modal renders from the envelope's top-N summary first, then hydrates the full drill on demand. Three new share templates (`projects-recap`, `projects-visual`, `projects-detail`) join the kernel and carry the active `windowWeeks` into the share flow.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- ProjectsModal mobile (≤640px) is no longer cramped: the 7-column table reflows to a stacked-card layout (project key + cost on row 1; sessions · used % · % week · first → last seen on row 2), a sort-cycle pill replaces the SortableHeader, and the per-project drill renders inline directly under the selected card. Closes #73.
|
|
15
|
+
- dashboard Projects modal: `$/1%` column replaced with `% of week`. The original column was mathematically degenerate — `attributed_pct` is computed as `(cost[p] / total_cost) * weekly_used_pct`, so `cost[p] / attributed_pct` collapses to the constant `total_cost / weekly_used_pct` for every project (independent of `p`). Every row in the table showed the same value at every window pill, adding no per-row signal. Replaced with `% of week` = `windowCost / sum(windowCost)` over the active window — the one orthogonal axis the data already carries — rendered as a 0–100 percent and sorted desc by default (same shape as the column it replaces). `windowPct` (the "Used %" column) is unchanged. Column id `share_of_window`; field `ProjectsTableRow.shareOfWindow` (replacing `dollarsPerPct`). Spec §3.4 row updated; the F5 follow-up `$/1%` scatter chart deferred in §1 / §A.7 is unrelated and intact. Regression `<ProjectsModal /> > '% of week' column (#72)` in `dashboard/web/src/modals/ProjectsModal.test.tsx` asserts the new header, the absence of the legacy `$/1%` header, and the per-row share values on a 3-project / 1w fixture. Closes #72.
|
|
16
|
+
- dashboard Projects modal: stacked-area trend chart now renders visibly when only one week of data is in scope (fresh installs or the `1w` pill). Previously `ProjectsTrendChart`'s `xFor` collapsed every point to `VW/2`, drawing each polygon as a zero-width vertical line; the chart appeared empty even though the table and totals showed real activity. `xFor` now synthesizes a `[VW*0.1, VW*0.9]` horizontal span when `weekCount === 1`, and the polygon walk emits both edges of that span at each accum-y so the series closes as a 4-corner rectangle (a wide stacked-bar segment) instead of a line. Multi-week renders (`4w` / `8w` / `12w`) are byte-identical — the new branch is gated on `weekCount === 1`. Regression `<ProjectsTrendChart /> 1-week render` in `dashboard/web/src/modals/ProjectsTrendChart.test.tsx` asserts non-trivial x-extent + closed-quad point counts on the 1-week path and pins multi-week geometry as a sanity check. Closes #68.
|
|
17
|
+
- Mirror tool now propagates file deletions/additions when `.mirror-allowlist` edits change classification, with a release-time fingerprint precheck + `--reconcile` recovery mode. Closes #62.
|
|
18
|
+
- dashboard Projects panel: `projects.current_week.rows` now populates correctly after an Anthropic mid-week reset. Previously `_build_projects_envelope` left the shifted `TuiCurrentWeek.week_start_at` (e.g. Friday 13:00 UTC post `_apply_midweek_reset_override`) in place as the bucket-lookup key while the entry aggregator anchored every row to its ISO-Monday — the lookup missed every Monday-keyed bucket and emitted `rows: []` / `total_cost_usd: 0.0` despite real activity. The envelope now canonicalizes `cw_start` through `_projects_week_start_monday_utc` so the lookup matches whichever bucket the aggregator wrote, matching the documented invariant: "`TuiCurrentWeek.week_start_at` is NOT a valid `week_start_date` lookup key after a mid-week reset." Regression `test_current_week_rows_populated_after_midweek_reset` in `tests/test_projects_envelope.py` drives a Friday-13:00-UTC stub and asserts populated rows; `tests/fixtures/dashboard/{reset-week,ok,warn,over,tz-override}/golden-data.json` re-rolled to capture the corrected output.
|
|
19
|
+
- dashboard share modal: `windowWeeks` selected via the Projects modal's `1w` / `4w` / `8w` / `12w` pill now reaches the server. Previously `shareModal.params.windowWeeks` was set by the modal but never read by the share path (`ShareModal`/`ActionBar`/`PreviewPane` ignored `slot.params`), so the server's `options.get("windowWeeks", 1)` fallback rendered every share artifact as 1w regardless of the pill. `windowWeeks` is now a typed field on `ShareOptions`; `ShareModalRoot` forwards `slot.params` as `initialParams` to `ShareModal`, which seeds it into the initial options state — the existing preview + export fetches naturally include it in the request body. Regression `I1 regression: windowWeeks param threads through /api/share/render body` in `dashboard/web/src/share/ShareModalRoot.test.tsx` asserts on the captured fetch body, not the store slot.
|
|
20
|
+
- dashboard Projects modal: "Sessions" / "First seen" / "Last seen" columns are now window-scoped per spec §3.4 — they reflect the active `1w` / `4w` / `8w` / `12w` pill instead of fixed 12w / all-time values. `_build_projects_envelope` emits new per-week arrays `sessions_per_week` / `first_seen_per_week` / `last_seen_per_week` on each `trend.projects[]` entry (replacing the window-unaware `sessions_count_12w` / `first_seen_at` / `last_seen_at` scalars); `ProjectsModal.tsx` slices these to the active window pill and reduces them client-side. Column labels revert to bare "Sessions" / "First seen" / "Last seen" (the I2 stopgap's "(12w)" / "(all-time)" widening is gone). Reconcile R-PROJ6 in `bin/cctally-reconcile-test` locks in the per-week-array invariants (length == `len(weeks)`, count ≥ 0, presence parity between sessions count and first/last seen). Regression `flipping the window pill rescopes Sessions / First seen / Last seen cells` in `dashboard/web/src/modals/ProjectsModal.test.tsx`. The four dashboard goldens (`tests/fixtures/dashboard/{ok,warn,over,reset-week}/golden-data.json`) re-rolled. Closes #71.
|
|
21
|
+
- ProjectsTrendChart legend on mobile (≤640px): replaced the 2-line silent clamp with a horizontal-scroll row (`scroll-snap-type: x proximity`), `(other)` pinned last, right-edge fade-mask affordance, and tap-to-drill on each project's legend item. Closes #74.
|
|
22
|
+
- dashboard share Projects path: thin-history dashboards no longer mis-label exported artifacts. `_build_projects_share_panel_data` clamped its row aggregation via `take = min(weeks_back, n_weeks)` but historically returned the unclamped `weeks_back` for both `window_weeks` and the `period_start` bound, so a fresh install requesting the 12-week share template rendered as `Last 12 weeks` with a 12-week date range over only (say) 3 weeks of data. The builder now derives `effective_weeks = max(1, take)` and routes it through both the label and the period bounds so the share artifact's framing always matches the rows it carries. Regression: `tests/test_share_projects_window_clamp.py` (4 cases — clamp, no-clamp, empty-trend, panel-mode unaffected).
|
|
23
|
+
- dashboard Projects modal sort: descending sorts on nullable columns (`Last seen`, `Used %`, `% of week`) now keep `—` rows pinned at the bottom. The previous `nullsLast` helper returned `1` / `-1` from each column's comparator, but `applyTableSort` multiplies the comparator's return by `sign` to flip direction — under desc the null-parking constants flipped sign too, pulling the `—` rows to the top. Null-last semantics moved off the comparator and onto a new `TableColumn.nullKey` extractor that `applyTableSort` checks BEFORE applying the direction sign, so null rows are parked at the end unconditionally. Regression: `dashboard/web/__tests__/tableSort.test.ts` (asc + desc both place null rows last on a mixed-null fixture).
|
|
24
|
+
- dashboard Projects panel: credited weeks no longer overstate `Used %`. `_build_projects_envelope` historically loaded the per-week `weekly_percent` via `MAX(weekly_percent) GROUP BY week_start_date`, which relies on the "weekly_percent is monotonic within a week" invariant that **breaks** on weeks where Anthropic ships an in-place goodwill credit (the same v1.7.2 surface that `week_reset_events` tracks): pre-credit `60.0%` and post-credit `5.0%` coexist for the same `week_start_date`, MAX returns the stale pre-credit high-water mark, and every per-project `attributed_pct` plus the trend's `total_pct` get pinned at the HWM forever. Switched to the canonical "latest snapshot per week" pattern (read rows ordered by `captured_at_utc ASC, id ASC` and let later non-NULL values overwrite — same shape as `_select_last_known_snapshot` and the doctor credited-week check). The `reset-week` dashboard fixture's golden flipped from `total_pct: 60.0` to `total_pct: 5.0` (and the two contributing projects' `attributed_pct` rescaled proportionally from `37.9% / 22.1%` to `3.16% / 1.84%`), confirming the post-credit values now drive the Projects panel/modal.
|
|
25
|
+
- dashboard Projects modal: keyboard shortcuts (`1` / `4` / `8` / `0` / `s` / `↑` / `↓` / `Enter`) no longer leak through the share / composer overlay when it's layered above the modal. The share overlay only registers `Escape` at `overlay` scope, so single-char and named keys previously fell through to the `modal`-scope bindings and silently mutated `projectsWindowWeeks` / `projectsTrendYMode` / row selection on the hidden Projects modal underneath — dismissing the share dialog then dropped the user into a different view than the one they were sharing. Each binding now gates on `shareModal === null && composerModal === null` via a shared `isProjectsTopmost()` predicate.
|
|
26
|
+
- dashboard `GET /api/project/<key>?weeks=N` drill endpoint: `window_attributed_pct` now scopes to the requested window. The HTTP path reuses `snap.projects_envelope` (built by the sync thread with `weeks_back=12`), so `matching_trend["weekly_pct"]` is always a 12-element array — summing it without slicing made `1w` / `4w` / `8w` drills report the 12-week attribution total, and the answer depended on whether the helper rebuilt the envelope or reused the snapshot. The drill now trims `weekly_pct` to the trailing `weeks_back` entries before summing (same `[-take:]` slice the share-envelope adapter at `_cctally_dashboard.py:1629` already uses).
|
|
27
|
+
- dashboard Projects drill (`ProjectsDrillPanel`): stale-on-switch guard now also treats a window change for the *same* project as stale. `useProjectDetail` keeps the previously-rendered payload mounted across `(projectKey, windowWeeks)` changes (SWR pattern), but the guard only compared `data.key !== projectKey`. Switching the modal's window pill (e.g. 12w → 4w) on a heavy project therefore left the prior window's cost / models / sessions rendered under the new `{windowWeeks}w` heading for seconds while `/api/project` resolved. The guard now also checks `data.window_weeks !== windowWeeks`, so the drill always agrees with the visible window header during a re-fetch.
|
|
28
|
+
- dashboard Projects modal: persisted selection is re-bound to the leader (or cleared) when the previously-selected project drops out of the current window's trend rows. Without the re-bind, narrowing the window after selecting an older project — or cross-navigating from an older Sessions row while the modal is on a 1w/4w window — left the drill pointed at a row that no longer exists in the data (endpoint 404 on desktop, missing inline anchor on mobile). A new effect re-evaluates `selectedKey` against `env.projects.trend.projects[]` whenever the trend rows change and falls back to the current-week leader when the prior selection is absent. Cross-nav (`store.openProjectKey`) still takes priority above this branch so explicit deep-links into the modal are never overridden.
|
|
29
|
+
- dashboard Projects share path: `_build_projects_share_panel_data` now clips `period_end` to `min(cw_start + 7d, _share_now_utc())`. The rows in this panel_data are week-to-date (current_week rows aggregate through "now", and the multi-week branch's trailing slice is also week-to-date), so a mid-week export advertised `period_end = cw_start + 7d` — a future reset that the rows don't include — in the rendered period and frontmatter. The frontmatter now agrees with the live dashboard's "spent this week" KPI, which `_build_current_week_share_panel_data` symmetrically clips to `now`. Regression: `test_period_end_clipped_to_now_when_mid_week` + `test_period_end_uses_week_end_when_now_past_reset` in `tests/test_share_projects_window_clamp.py`; existing clamp tests pin `CCTALLY_AS_OF` past `cw_start + 7d` so their period_end assertions remain valid.
|
|
30
|
+
- dashboard Projects modal mobile sort cycle: `first_seen` step now defaults to `asc` (matching `PROJECTS_COLUMNS`'s `defaultDirection`). The mobile pill previously hard-coded `first_seen: desc`, which (a) produced the reverse order from the desktop column header on the `first` step and (b) left a persisted desktop `first_seen` sort unrepresentable in the mobile cycle's `findIndex` lookup — the cycle would silently fall back to cost-desc on the first tap on mobile. The cycle and the column registry now agree on direction.
|
|
31
|
+
|
|
8
32
|
## [1.9.0] - 2026-05-19
|
|
9
33
|
|
|
10
34
|
### Removed
|