Skip to content

feat(budget): workflow + detail redesign (#031)#98

Merged
studert merged 5 commits into
mainfrom
feat/budget-workflow-and-redesign
May 21, 2026
Merged

feat(budget): workflow + detail redesign (#031)#98
studert merged 5 commits into
mainfrom
feat/budget-workflow-and-redesign

Conversation

@studert
Copy link
Copy Markdown
Member

@studert studert commented May 21, 2026

Summary

Spec 031 — Budget Workflow & Detail Redesign. Two changes land together:

  1. Workflow. Sidebar Budget opens the active fiscal year's detail view directly. No more summary card with a "View Details" button. The all-budgets list moved to /budget/history, reachable via the header "All budgets →" link and the detail-view breadcrumb.
  2. Visual redesign of the detail page. Five flat KPI tiles replaced by a budget-health hero (status pill + narrative + multi-marker progress bar + 4 stat tiles), plus a past-month spotlight for the most recent closed period. Period table picks up a current-month accent + a "Current" pill, distinct over/under/future row treatments, and a dialed-down "live" Anthropic-API row.

Functionality is unchanged — every server action (createBilledCost, updateBilledCost, deleteBilledCost, updateBudgetAllocations, archiveBudget, getActiveBudget, etc.) keeps its current signature. No schema changes.

Notable refactors

  • BudgetDetailClient ~813 → ~245 lines, composed from BudgetDetailHeader, BudgetHealthHero, PastMonthSpotlight, PeriodAllocationsTable, BilledCostDialog (mode="add"|"edit"), DeleteBilledCostDialog.
  • Shared StatTile deduped between hero + spotlight.
  • getPerToolSpend deleted from src/actions/budget.ts — it was dead code (Reports has its own per-tool query).
  • The per-tool spending list on the detail page is dropped; Reports → Budget already covers that view.

Spec artifacts

  • specs/031-budget-workflow-and-redesign/README.md — workflow proposal + comparison table.
  • specs/031-budget-workflow-and-redesign/detail-mock.html — visual reference.
  • specs/031-budget-workflow-and-redesign/plan.html — implementation plan + reuse audit.
  • specs/031-budget-workflow-and-redesign/implementation-notes.html — running log of decisions, deviations, tradeoffs, and open questions captured while implementing (including the post-/simplify review fixes).

Open questions

  • Status-pill thresholds (under <95%, on-track ≤105%, at-risk ≤115%, over >115%) are the plan's defaults. If product wants different cutoffs, edit STATUS_THRESHOLDS at the top of budget-health-hero.tsx.
  • Year-end projection uses simple average-of-completed-periods, not the Reports linear forecast. Easy to swap if alignment is preferred.

Test plan

  • pnpm typecheck clean
  • pnpm lint clean (zero warnings)
  • pnpm test — 291/291 unit tests pass
  • Manual smoke against a Neon branch: /budget (200), /budget/history (200, all 5 fiscal years), /budget/5 (200, archived → read-only) — hero, spotlight, period table, breadcrumb, allocations editor, dialogs all render and submit
  • Playwright e2e suite — assertions in tests/e2e/budget-period-running-costs.spec.ts updated for the new layout; run in CI

🤖 Generated with Claude Code

- /budget lands directly on the active fiscal year's detail view; the
  all-budgets list moves to /budget/history (reachable from the header
  "All budgets" link + breadcrumb).
- Replace the 5 flat KPI tiles with a budget-health hero: multi-marker
  progress bar (planned/expected/actual against the annual ceiling),
  status pill (under / on-track / at-risk / over / no-data), narrative,
  and 4 stat tiles (Billed YTD / Actual YTD / Projected year-end /
  Variance YTD).
- Add a past-month spotlight card for the most recent completed period
  with billed or running spend.
- Period table: current-month accent + "Current" pill, distinct over /
  under / future treatments, dialed-down "live" sky-tinted Anthropic
  API row.
- Drop the per-tool spending breakdown from the detail page (Reports
  owns that view).
- Refactor BudgetDetailClient (~813 → ~245 lines) into composable
  sections: BudgetDetailHeader, BudgetHealthHero, PastMonthSpotlight,
  PeriodAllocationsTable, BilledCostDialog (mode="add"|"edit"),
  DeleteBilledCostDialog. All server actions unchanged.
- Delete orphaned getPerToolSpend (no remaining callers; Reports has
  its own per-tool query).

Spec, mockup, plan, and running implementation notes under
specs/031-budget-workflow-and-redesign/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 21, 2026 14:40
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-developer-hub Ready Ready Preview, Comment May 21, 2026 6:01pm

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Spec 031’s budget workflow + detail-page redesign by making /budget render the active fiscal year’s detail view directly, moving the all-budgets list to /budget/history, and refactoring the budget detail UI into smaller components (hero, spotlight, period table, dialogs). It also removes dead per-tool spend code and updates the Playwright e2e spec to match the new layout.

Changes:

  • Update /budget to fetch the active budget and render the shared detail client (with an empty state when no active budget exists).
  • Add /budget/history to host the full budgets table and update navigation (header link + optional breadcrumb).
  • Refactor/detail redesign: new BudgetHealthHero, PastMonthSpotlight, PeriodAllocationsTable, dialog components, shared StatTile; remove dead getPerToolSpend; update e2e assertions.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/e2e/budget-period-running-costs.spec.ts Updates Playwright checks to match the new /budget landing + hero content.
src/app/budget/page.tsx Makes /budget render active budget detail directly; adds empty state CTAs and history link.
src/app/budget/history/page.tsx New history page showing all budgets in a table with navigation back to active budget.
src/app/budget/components/stat-tile.tsx New shared stat-tile component used by hero/spotlight.
src/app/budget/components/period-allocations-table.tsx New extracted period table with styling for current/future/over/under and expandable running-cost rows.
src/app/budget/components/past-month-spotlight.tsx New “most recent closed period” spotlight card.
src/app/budget/components/dialogs/* Extracted/combined billed-cost add/edit and delete dialogs; shared form state module.
src/app/budget/components/budget-health-hero.tsx New budget-health hero (status + narrative + multi-marker bar + stat tiles).
src/app/budget/components/budget-detail-header.tsx New header with optional breadcrumb and “All budgets”/“New Budget” actions.
src/app/budget/components/budget-detail-client.tsx New composed client detail view; wires hero/spotlight/table/dialogs together.
src/app/budget/[id]/page.tsx Simplifies budget detail route; removes per-tool spend usage; reuses shared client.
src/actions/budget.ts Removes dead getPerToolSpend action.
specs/031-budget-workflow-and-redesign/* Adds spec artifacts (README + mock/plan/notes HTML).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +122 to +126
const start = new Date(period.startDate);
const end = new Date(period.endDate);
const isCurrent = start <= today && today <= end;
const isFuture = start > today;
const isClosed = end < today;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in a357c83. The hero, spotlight, and table now all share the existing classifyPeriod(period, today) helper from src/lib/reports/period-helpers.ts, which treats endDate as inclusive end-of-day. One source of truth across all three views.

Comment on lines +81 to +92
const totals = periods.reduce(
(a, p) => {
const running = runningCosts[p.id]?.runningCostCents ?? 0;
a.allocated += allocations[p.id] ?? 0;
a.expected += p.expectedSpendCents;
a.actual += p.billedTotalCents + running;
return a;
},
{ allocated: 0, expected: 0, actual: 0 }
);
const totalVariance = totals.actual - totals.expected;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a357c83. The footer totals now exclude future periods (using classifyPeriod), and the internal fields were renamed expectedToDate / actualToDate so the intent is obvious to the next reader. Kept the "YTD Total" label — the meaning now matches.

Comment on lines +66 to +71
(a, p) => {
const start = new Date(p.startDate);
const end = new Date(p.endDate);
const isYtd = start <= today;
const isClosed = end < today;
const billed = p.billedTotalCents;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a357c83 — the hero now calls classifyPeriod(p, today) for the phase decision instead of the homegrown end < today comparison. Same shared helper the period table and spotlight use.

Comment on lines +18 to +27
export function PastMonthSpotlight({ budget, runningCosts }: Props) {
const today = new Date();
let latest: { period: BudgetWithCosts["periods"][number]; endTime: number; actual: number } | null = null;
for (const p of budget.periods) {
const end = new Date(p.endDate);
if (end >= today) continue;
const running = runningCosts[p.id]?.runningCostCents ?? 0;
const actual = p.billedTotalCents + running;
if (actual <= 0) continue;
const endTime = end.getTime();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a357c83 — the spotlight uses classifyPeriod(p, today) === "past" now, matching the hero and the table.

Comment on lines +195 to +219
<BudgetHealthHero budget={budget} runningCosts={runningCosts} />

<PastMonthSpotlight budget={budget} runningCosts={runningCosts} />

<Card>
<CardHeader>
<CardTitle>Period allocations &amp; billed costs</CardTitle>
</CardHeader>
<CardContent>
<PeriodAllocationsTable
budget={budget}
runningCosts={runningCosts}
allocations={allocations}
onAllocationChange={handleAllocationChange}
isAdmin={isAdmin}
isArchived={isArchived}
saving={saving}
onSaveAllocations={handleSave}
onAddBilledCost={openAddDialog}
onEditBilledCost={openEditDialog}
onDeleteBilledCost={openDeleteDialog}
/>
</CardContent>
</Card>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for the hero in a357c83BudgetHealthHero now accepts an optional allocations prop, and the parent client passes its live map, so the "unallocated" tile reflects unsaved edits and stops disagreeing with the period-table warning when the user pushes allocations over the ceiling.

I intentionally kept the spotlight reading persisted values: it's showing a closed-period snapshot, and editing a past period's planned amount while the spotlight is on it would be an unusual flow — showing the saved value there feels more correct than reflecting an unsaved edit. Happy to revisit if you'd rather have both stay perfectly in sync.

Comment on lines +8 to +12
export function makeEmptyBilledCostForm(): BilledCostFormState {
return {
amountDollars: "",
invoiceDate: new Date().toISOString().slice(0, 10),
description: "",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a357c83 — now uses formatDateOnly(new Date()) from @/lib/utils. Thanks for catching this; the previous toISOString().slice(0, 10) would have prefilled the wrong date for non-UTC users around midnight.

Comment on lines +60 to +66
## Out of scope

- Data model: no schema changes. All redesign uses existing `annual_budgets`, `budget_periods`, `billed_costs`, `anthropic_workspace_costs`.
- Server actions: `getActiveBudget`, `getBudgets`, `getBudgetWithCosts`, `getPerToolSpend`, `getRunningCostsForPeriod`, `updateBudgetAllocations`, `createBilledCost`, `updateBilledCost`, `deleteBilledCost` — all unchanged.
- Validation, the new-budget creation flow (`/budget/new`), and the budget edit/archive actions in `BudgetListActions`.
- The Reports → Budget tab — that's spec 028 and lives at `/reports`.
- Per-tool spending breakdown — removed from the detail view entirely. It's already covered by the Reports → Budget page (spec 028); duplicating it here was noise.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — updated in a357c83. The "Out of scope" list now states explicitly that getPerToolSpend was deleted, with the reason: the implementation audit found no remaining callers (Reports has its own fetchPerToolByPeriod in src/actions/reports.ts).

Vercel deploy on d4e6076 failed in 1.4s before build start (empty logs) — appears infra-only; CI build job passed. Retriggering.
- Period phase: hero, spotlight, and table now share `classifyPeriod`
  from `src/lib/reports/period-helpers.ts` instead of comparing
  `new Date(endDate) < today`. `endDate` is a SQL DATE; the old
  comparison flipped periods to "past" at the start of their last day
  (with extra timezone skew). The shared helper treats `endDate` as
  inclusive end-of-day.
- Period table footer: "YTD Total" was summing `expectedSpendCents`
  across all periods, so the variance line compared YTD actual to
  full-year expected. Future periods are now excluded from the
  expected/actual totals; internal fields renamed to `expectedToDate`
  / `actualToDate` to keep the intent legible.
- Hero now receives the parent's in-progress `allocations` map, so
  "unallocated" and YTD-planned reflect unsaved edits and stop
  disagreeing with the period-table warning when the user pushes the
  allocations over the ceiling. Falls back to `plannedAmountCents`
  when the prop is absent.
- `makeEmptyBilledCostForm()` uses `formatDateOnly(new Date())` for
  the default invoice date instead of `new Date().toISOString().slice(0, 10)`,
  which prefilled yesterday/tomorrow for non-UTC users near midnight.
- Spec README "Out of scope" list updated to reflect the
  `getPerToolSpend` deletion (no callers; Reports has its own
  per-tool query).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Projection: the old formula was `avgPerClosed × periods.length`, which
re-projected the past and ignored the current month's pace. New:
`closedActual + currentContribution + remainingCount × avgPerClosed`,
where `currentContribution = max(currentRunning / elapsedFraction,
avgPerClosed)` once at least 20% of the current period has elapsed,
otherwise the historical average. Closed months are taken at their
actual; the in-progress month surfaces its own pace but can't drop
the projection below the historical average; future months still
average. The 20% threshold avoids divide-by-very-small at the start
of a period.

Variance: the "Variance YTD" tile included the current period's full
`expectedSpendCents` against its partial actual, so the number drifted
through the month and snapped at month-end. Replaced with a
closed-only tile labeled "Variance through {lastClosedLabel}". The
status pill, the bullet under it, and the blue tick on the multi-
marker bar all follow the same closed window, so they read
consistently.

Narrative pairs the closed-window sentence with a current-period
burn-rate signal — "{currentLabel} is pacing 53% above the historical
average ($X projected vs $Y)" once the pace is trustworthy; falls
back to "{currentLabel} is 12% through ($X so far)" before then.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@studert
Copy link
Copy Markdown
Member Author

studert commented May 21, 2026

Follow-up to the discussion thread: 405f4ed reworks the projection + variance model in BudgetHealthHero.

Projection — was avgPerClosed × periods.length (re-projected the past, deaf to the current month). Now:

closedActual
  + currentContribution           // max(currentRunning / elapsedFraction, avgPerClosed)
                                  // once ≥20% elapsed; else avgPerClosed
  + remainingCount × avgPerClosed

Closed months are taken at actual. The current month surfaces its own burn rate but won't drag the projection below the historical average (a slow first week shouldn't optimistically pull it down). Future months extrapolate from history. The 20% threshold avoids divide-by-very-small at the start of a period.

Variance — "Variance YTD" included the current month's full expected against its partial actual, so the tile drifted less-negative through the month and snapped at month-end. Replaced with a closed-only tile labeled "Variance through {lastClosedLabel}" (e.g. "Variance through Apr"). The status pill, the small bullet under it, and the blue tick on the multi-marker bar all follow the same closed window — they read consistently now.

Narrative pairs the closed-window sentence with a current-period burn-rate signal — e.g. "May 2026 is pacing 53% above the historical average ($58k projected vs $38k)" — or, before we trust the pace, "May is 12% through ($4.5k so far)".

Verified locally: pnpm typecheck / pnpm lint / pnpm test (291/291) all green; rendered against the worktree's Neon branch with FY 2026 data showing the new tile/pill/narrative behaving as expected.

See specs/031-budget-workflow-and-redesign/implementation-notes.html for the full rationale + a "considered and rejected" note on linear-time prorated variance.

- Per-row variance and % diff cells now render "—" for current and
  future periods. The in-progress month was showing partial actual vs
  full-month expected (always negative, always misleading); future
  months were showing 0 vs 0 (always useless). Only closed periods
  display a real variance + % diff, with the over/under badges that
  signal something happened.

- "YTD Total" footer became "Total through {lastClosedLabel}" (e.g.
  "Total through Apr 2026") and sums expected/actual/variance over
  the closed window only — matching the hero's "Variance through
  Apr 2026" tile so the two reads agree. Falls back to "Total" with
  em-dashes when no period has closed yet. Allocated stays summed
  across all periods (it answers a different question: "what have I
  committed", independent of phase).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@studert
Copy link
Copy Markdown
Member Author

studert commented May 21, 2026

@copilot review

@studert studert merged commit 7f273f1 into main May 21, 2026
8 checks passed
@studert studert deleted the feat/budget-workflow-and-redesign branch May 21, 2026 18:15
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.

2 participants