fix(034): renumber ingestion migration to 0024 after #102 - #116
Conversation
* docs(035): add scenario calculators plan and prototype Implementation plan + the validated single-file prototype for the new Scenarios section and its first calculator (API to subscription migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(035): add Scenarios section with API to subscription calculator New admin-only Scenarios section, driven by an extensible registry (the index and section render from SCENARIOS; budget-forecast is stubbed as 'soon'). First calculator at /scenarios/api-subscription maps Anthropic API (Claude Console) users onto flat Standard/Premium seats and models the bill under four scenarios against live data. - Pure, tested calc engine (lib/scenarios/api-subscription.ts) shared by server and client; classifyMonths extracted and unit-tested. - Live Drizzle loader (lib/scenarios/queries.ts) resolves tools by vendor+name and seat prices from access_tiers by name. - Nothing-design UI; admin gate lifted to a section layout. - Adds formatUSD0 to chart-format.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot review on the calculator verdict + tooltip - Introduce a three-way savingState (saves/costs/flat): equal-cost scenarios now read neutrally instead of as a negative, with no +/- sign rendered. - Guard savingPct against a zero baseline (no complete months yet): show 'less/more than' instead of a meaningless '0%'. - Distinguish partial-month tooltips: the most recent month is 'month-to-date'; an earlier partial month is labelled as a mid-month collection start. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only MCP server (Streamable HTTP) exposing 7 AI-spend tools, shared-secret bearer auth, dormant by default until MCP_SERVER_SECRET is set. Reuses the existing read layer; no mutations or secret exposure.
Adds the Budget / Cost Forecast Simulation scenario (/scenarios/budget-forecast): a pure projection engine + Nothing-design Recharts UI that anchors on actual spend-to-date and projects the fiscal year forward under per-tool growth assumptions vs an editable ceiling. Read-only, no schema change. 31 unit tests; Copilot + Vercel Agent review addressed.
* feat(035): add API threshold to keep light keys on metered API Adds a lower "API threshold" to the API->Subscription calculator's right-sized scenario. Keys whose monthly spend falls below it stay on pay-as-you-go metered API instead of being forced onto a flat seat — mirroring the existing Premium threshold and defaulting to $25 (the Standard seat price, the break-even below which a seat can never pay off). The right-sized scenario is now three-band (API · Standard · Premium): - engine: SeatTier gains "api"; ScenarioInputs.apiThresholdCents; ScenarioResult.apiCount; mapSeat is Premium-first and an "api" key carries its own burn as seatCents (zero delta, foots the total). - client: a second slider paired with the Premium one (clamped so the floor can't exceed the ceiling), three-way readouts across the KPI, verdict, scenario card, comparison bar, table footer, and a new API SeatPill variant. - tests: new boundary + three-band anchors (47 keys -> $1,775.91/mo, 16 API/22 Std/9 Prem) plus an apiThreshold=0 superset test pinning the legacy $2,075 figures. No schema change; read-only over existing tables. Verified in-browser (default 16·22·9 -> $1,776/mo, 42% cut; clamp + reactivity confirmed). Specs: api-threshold-implementation-plan.html + implementation-notes.html under specs/035-scenario-calculators/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot review — accurate mapSeat docs + partition verdict - mapSeat JSDoc now describes the threshold rules (Premium-first; API floor; Standard otherwise) instead of "cheapest viable option", and notes that the cost-minimising reading only holds at the break-even defaults. - Verdict copy reworded to a true partition (joinParts helper, empty groups omitted) so it stays accurate for every mix — verified in-browser for the default, API-floor=0, and Standard-empty (clamp) cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot re-review — field doc + empty-population verdict - premiumThresholdCents JSDoc no longer says "otherwise Standard" (the API tier makes that wrong); points to mapSeat for the three-band split. - Verdict breakdown clause is now conditional (verdictLead), so count=0 (population=active with no active keys) reads "Right-sizing the 0 API users costs …" instead of dangling "— —". Singular "user" handled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot pass 3 — seat $/mo precision + sort comment - Per-row "Seat $/mo" now uses formatCurrency for API rows so it matches the cents-precise "API basis" cell (was whole-dollar formatUSD0, showing e.g. $24 next to a $23.82 basis with a "—" delta). Whole-dollar seat prices keep formatUSD0. Verified in-browser: the two cells now match exactly. - TIER_SORT_ORDER comment reworded — it's a tier-escalation order (API→Standard→Premium), not "cheapest→priciest" (tiers are policy-assigned). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(budget): add budget_extensions schema, migration, and types Phase 1 of spec 026 — first-class records of mid-year ceiling changes. - New `budget_extensions` table with reason, category, optional linked_tool_id, effective_date, created_by, and a CHECK constraint on amount_cents <> 0. - New `budget_extension_period_allocations` join table tracking which periods absorbed an extension's amount (powers the "+X from extension" sub-label and lets delete cleanly reverse the impact). - New `original_amount_cents` column on `annual_budgets`. Backfilled in the same migration via a three-step add-nullable / UPDATE / SET NOT NULL pattern so existing rows aren't rejected. - Drizzle relations and inferred types (`BudgetExtension`, `BudgetExtensionWithAllocations`, `PeriodWithCosts.extensionAmountCents`, `BudgetForecast.originalCeilingCents`). `originalAmountCents` is the originally approved ceiling; the existing `totalAmountCents` continues to be the live (mutable) ceiling. Read sites across the app keep working without changes; only the new "baseline + extended" tag reads the new column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(budget): server actions, validators, and integration tests for extensions Phase 2 of spec 026. - `src/actions/budget-extensions.ts` (NEW): createBudgetExtension, updateBudgetExtension, deleteBudgetExtension. Each follows the existing budget action pattern: requireAdmin → safeParse → guards → transaction → history → revalidatePath. - Allocation modes resolved server-side: unallocated, distribute_remaining, single_period, custom. distribute_remaining falls back to all periods when the effective date precedes every period end (covers backdated bumps). - Guards: archived budgets immutable, effective date within fiscal year, per-period planned amount stays >= 0, allocations stay <= ceiling, ceiling > 0. Tx orchestration mirrors createBudget's existing pattern. - getBudgetWithCosts extended to fetch extensions + allocations and inject per-period extension totals. - getBudgets augmented with extensionCount + extensionNetCents per row for the history page. - 10 integration tests against a real Neon test branch covering create (each allocation mode), delete (cascade + reversal), update, and the guards (archived budget, out-of-year date, over-allocation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(budget): UI for budget extensions across detail, dashboard, reports Phases 3, 4, and 5 of spec 026. Detail page (`/budget`, `/budget/[id]`): - `BudgetHealthHero` now shows "<baseline> + <delta> extended" / "− <delta> reduced" next to the annual-ceiling number when totalAmountCents has diverged from originalAmountCents. - New `BudgetExtensionsCard` lists each extension with category badge, optional linked-tool badge, description, who/when, and a delete affordance for admins on active budgets. - New `AddExtensionDialog` with live "Effect on FY budget" preview, radio allocation modes, and a tool dropdown. Past periods in the single_period picker are disabled with a "(closed)" hint. - `DeleteExtensionDialog` summarizes which periods will be reversed. - `PeriodAllocationsTable` renders a clickable "+€X from extension" sub-label under the planned cell; reductions render in destructive color with "from reduction" copy. Local allocation state re-syncs on budget.updatedAt so an extension's per-period bump isn't silently rolled back by a later Save Allocations click. Cross-surface (dashboard, reports, history): - `BudgetHeroSection` on the admin dashboard now shows an "extended +€X" badge whenever totalAmountCents ≠ originalAmountCents. - `ForecastCumulativeChart` adds a dashed reference line at the original baseline so the chart shows both the live ceiling and the original. - Budget history page gains an Extensions column with count + net delta per fiscal year. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(specs): add spec 026 — budget extensions Concept doc, mockup HTML, implementation plan, running implementation notes, and browser verification screenshots for the feature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(budget): address PR #102 review feedback Four issues flagged in Copilot's review: 1. add-extension-dialog: live preview parsing diverged from submit (parseFloat accepted scientific notation that the strict regex on submit rejects, and the distribute-remaining preview showed an even split while the server dumps the remainder onto the first period). Extracted parseExtensionCents and previewDistributeRemaining helpers; the dialog now uses them so the preview can never disagree with what the server will accept or write. 2. delete-extension-dialog: copy assumed positive allocations and rendered "reduced by -$X" for reductions. Now branches on extension.amountCents sign — "reduced by $X" for extensions, "increased by $X" for reductions — with magnitudes formatted as absolute values. 3. budget-detail-client: my comment overstated what bumps annual_budgets .updated_at (only extension/ceiling/archive mutations do; allocation saves and billed-cost CRUD do not). Switched the re-sync trigger to a value hash of period.plannedAmountCents so any server-side planned change triggers re-sync, regardless of which action wrote it. 4. budget-extensions: deleteBudgetExtension was using recordStatusChange("active" → "deleted"), implying a status column that doesn't exist on budget_extensions. Replaced with the deleteBilledCost pattern (changeType="deleted" + full snapshot in previousValue) and added a regression test that asserts the history row + snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(anthropic): repair cost sync and console dropdowns on the 1st of month (#103) * fix(anthropic): repair cost sync and console dropdowns on the 1st of month Two distinct first-of-month bugs, both from date math collapsing to a zero-width or invalid range on day 1. Sync 400 error: fetchAndUpsertWorkspaceCosts capped ending_at at `now`. On the 1st, starting_at (month-start midnight) and ending_at (now, same day) snap to the same 1d bucket, so the cost_report API rejects the range with "ending date must be after starting date". Round ending_at up to the next UTC midnight instead, guaranteeing one full daily bucket — matching Anthropic's documented "current date + 1 day" pattern. Console dropdowns: /claude and /claude/users select the current month but populate options only from months that already have synced rows, so on the 1st the selected value has no matching SelectItem (blank trigger, empty data). MonthPicker now always includes the selected value, and the two available-months actions inject the current month when absent — mirroring the already-safe profile path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(month-picker): sort+dedupe month options and cover with tests Follow-up hardening from PR review. Extract option assembly into a pure buildMonthOptions() helper that dedupes and re-sorts newest-first, so an injected/URL-supplied value (past or future month) lands in its correct chronological position instead of being prepended at the head. Add unit tests for the 1st-of-month, empty-list, past, future, and duplicate cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anthropic): add Opus 4.8 to model pricing table (#106) * feat(anthropic): add Opus 4.8 to model pricing table Opus 4.8 standard API pricing matches the existing Opus 4.5/4.6/4.7 tier ($5/$25 per M tokens, $0.50 cache read, $6.25 cache write), so cost calculations now resolve it explicitly instead of falling back to the Opus 4.0/4.1 rate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(anthropic): cover Opus 4.8 pricing resolution Add Vitest coverage for resolveModelPricing/computeCostCents asserting Opus 4.8 resolves to the $5/$25 tier with resolved=true, guarding against a missing prefix entry silently falling back to the higher Opus 4.0/4.1 rate. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anthropic): current-day cost estimate (spec 033) (#107) * docs(spec-033): current-day cost estimate — finalized implementation plan + UI mockup Implementation-ready plan (Tier B, approved) for surfacing a calibrated, clearly-labelled "estimated today" cost across the Claude dashboard, so month-to-date and month-end projections are accurate intraday — especially near month end for budget management. Grounded in verified facts: - cost_report returns complete UTC days only; per-user usage_metrics carries a real, hourly-fresh today cost (computed_cost_cents). - per-user and workspace cost_report totals deliberately do NOT reconcile, so the estimate is the per-user signal calibrated to recent complete days. plan.html: 3-phase build plan (backend → projection → UI) with tasks, data contracts, constants, acceptance criteria, full touch-point map, and risks. mockup.html: target UI (KPI "incl. est. today", ghost daily bar, pacing anchor, 1st-of-month before/after). Budget integration + alert-threshold movement explicitly out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(anthropic): current-day cost estimate (spec 033) The cost_report (workspace/global) source only returns COMPLETE UTC days, so month-to-date totals and month-end projections were always missing today — and empty for the whole current month until the 2nd. Derive a calibrated "estimated today" from the hourly per-user usage source and surface it as a clearly-labelled, SEPARATE figure; also fix the projection denominator so pacing stops under-counting (and no longer projects $0 on the 1st). - estimate-today.ts: pure estimateTodayCostCents — calibrate per-user vs cost_report over the last 7 complete days, clamp 0.5–2.0, fall back to x1 when thin. No I/O. - queries.ts: today-estimate query helpers (global + per-workspace via resolved_workspace_id), threaded onto the KPI / workspace-list / workspace-detail DTOs as a separate field. totalCents unchanged. - projection: spentSoFar = MTD actual + est today, daysElapsed = UTC day, at all four callers. User-detail gets only the UTC fix (it already includes today). - forecast-workspace.ts: optional today estimate fills the missing cost_report slot so the cron Teams run-rate/MTD isn't diluted; evaluator passes per-workspace estimates. Default 0 preserves prior behaviour. - UI: dashed/ghost "today (est.)" treatment (est. chip + tooltip, sub-labels, daily-chart ghost bar, cumulative-pacing projection anchored at today). Alerts (getActiveAlerts) and budget running-costs (getRunningCostsForPeriod) stay actual-only. No schema changes, no new packages. Verified on the wt/fix-sync-first-of-month Neon branch (real data, 1st-of-month): $0 actual + $112.61 est today -> $3,378.30 projected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anthropic): key projection/forecast dates in UTC (PR #107 review) Address Copilot review: the spend data and daysElapsed are UTC-keyed, but a few day/month calculations still used local-time helpers — harmless in production (Vercel runs UTC) but off-by-one at a UTC boundary in non-UTC runtimes. - utils.ts: add getUtcDaysInMonth; use it in page.tsx and workspace-budget-list PaceLabel instead of getDaysInMonth(now) (local month). - forecast-workspace.ts: do all date math in UTC (dense-series keys, daysElapsed, MTD window, crossesCapOn) via formatUtcDateOnly + Date.UTC; drop the local-time date-fns calls. Behaviour unchanged in UTC runtimes; removes the boundary off-by-one. - test: pin UTC "today" at a month boundary (23:30Z on May 31 stays in May). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anthropic): skip current-month cost sync on the 1st (real fix for #103) (#105) #103 misdiagnosed the first-of-month cost_report 400. Verified against the live Anthropic API: cost_report (bucket_width=1d) only returns COMPLETE UTC days and silently floors `ending_at` back to start-of-today — a `now` or future `ending_at` does not help. The 400 "ending date must be after starting date" fires whenever the range contains no complete day, which on the 1st is always true for the current month (month-start == today). #103's "round up to the next midnight" therefore still 400'd on the 1st (the API floors that future instant right back to start-of-today). Correct fix: cap the window at start-of-today and bail when no complete day exists yet (the 1st of the month, or a future month). Days 2..31 and past-month backfills are unaffected. Verified on the running app against a Neon branch with production data: - old #103 code reproduced the exact prod 400 (sync_event outcome=partial) - fixed code: regular sync succeeds (June skipped), backfill upserts 527 past-month rows with 0 errors. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(anthropic): guard usage sync window against zero-width daily range (#104) * fix(anthropic): guard usage sync window against zero-width daily range Follow-up to #103. computeSyncWindow feeds the usage_report API with bucket_width=1d and ends at start-of-today UTC (today is covered separately via hourly buckets). The latest stored date should always be < today, but a same-day or future-dated row (bad backfill, clock skew, timezone edge) could push startDate to/after endDate, producing the same zero-width/inverted range that the API rejects with 400 "ending date must be after starting date" — the defect that broke the cost path in #103. Clamp startDate to at most endDate − 1 day so the historical window always spans at least one complete daily bucket. Export the helper and add unit tests covering the normal, no-data, latest==today, future-dated, and month-boundary cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(anthropic): clarify computeSyncWindow guard rationale Address PR #104 review: the guard comment claimed a same-day latest row could push startDate to/after endDate, but computeSyncWindow always subtracts one day, so latest == today still yields a valid [yesterday, today) window. Reword to state that only a future-dated row triggers the clamp. Comment-only change; behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Nothing design redesign — monochrome + one-red design system (spec 028) (#108) Migrates the AI Developer Hub from the stock shadcn/green-oklch theme to one coherent Nothing design system: monochrome canvas + a single red interrupt (#d71921), Space Grotesk / Space Mono / Doto, flat bordered surfaces, segmented-bar data viz, inline status instead of toasts. P0 tokens+fonts · P1 primitives/overlays · P2 app shell · P3 shared tables+charts · P4 page migration (toasts→inline StatusText, skeletons→LoadingState, literal tints→tokens, mono numbers, confirm()→AlertDialog) · P5 QA (a11y audited, anti-pattern grep gate clean). Final consistency pass + code-review fixes (incl. a useInlineStatus memoization fixing an infinite-render loop). Presentation-layer only — no schema/server-action changes. See specs/028-nothing-design-redesign/implementation-notes.html for decisions, deviations, and tradeoffs. * fix(mobile): optimize responsive layouts across screens (#109) Fix horizontal-overflow and cramped layouts on narrow (≈375px) viewports that surfaced after the Nothing redesign. Root cause of the reported Claude user-detail bug: the `lg:grid-cols-2` card grids holding the wide `whitespace-nowrap` model-breakdown table had grid cells defaulting to `min-width:auto`, so the table's intrinsic width expanded the track (and the whole page) past the viewport instead of letting the table's own `overflow-x-auto` engage. Add `min-w-0` to those cells on the Claude user/workspace detail pages and the users list. Other fixes: - Make Settings / Copilot / Reports tab bars scroll horizontally instead of overflowing the viewport. - Stack page headers and wrap action-button groups on mobile (Users, Invoices, Assignments, User detail, Request detail, profile cost card). - Collapse fixed two-column definition grids and the dense grid-cols-5 sync summary to single/fewer columns on mobile. - MonthPicker / workspace select go full-width below sm; license-template rows stack; budget edit controls wrap. - Harden dashboard chart cells with `min-w-0` to prevent mid-width overflow. https://claude.ai/code/session_01Cdqk8njUrmV66qC9GGF6Ad Co-authored-by: Claude <noreply@anthropic.com> * fix(charts): align legend swatches with series colors (#110) * fix(charts): align legend swatches with series colors Legend color indicators could drift from the bars/lines they label: - ChartLegendContent drew swatches at full opacity, so series rendered with fillOpacity (plan-vs-actual "running"/"forecast", the daily/global "Today (est.)" ghost bar) showed a legend dot in a visibly different shade — especially obvious on the greyscale chart palette. The swatch now mirrors the series' fill/stroke opacity and falls back to the configured --color-{key} token when Recharts omits a payload color. - daily-by-user and global-metrics used the raw Recharts <Legend>, which styles swatches/labels differently from every other chart. They now use the shared ChartLegend + ChartLegendContent for consistent, config-driven swatches. Note: chart.tsx was previously not conforming to the repo Prettier config (no semicolons); the format-on-edit hook normalized the whole file. * fix(reports): explain over-budget red bars in plan-vs-actual legend The "Billed" bar turns red (var(--destructive)) on months where actual spend exceeds the plan, via per-<Cell> fills. Recharts derives each legend entry's color from its <Bar>'s fill and ignores Cell overrides, so the red never appeared in the legend — leaving viewers with an unexplained red bar. Wrap ChartLegendContent so an "Over budget" swatch is appended, but only when at least one month actually breaches its plan. --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(035): Scenarios section + API→Subscription calculator (#113) * docs(035): add scenario calculators plan and prototype Implementation plan + the validated single-file prototype for the new Scenarios section and its first calculator (API to subscription migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(035): add Scenarios section with API to subscription calculator New admin-only Scenarios section, driven by an extensible registry (the index and section render from SCENARIOS; budget-forecast is stubbed as 'soon'). First calculator at /scenarios/api-subscription maps Anthropic API (Claude Console) users onto flat Standard/Premium seats and models the bill under four scenarios against live data. - Pure, tested calc engine (lib/scenarios/api-subscription.ts) shared by server and client; classifyMonths extracted and unit-tested. - Live Drizzle loader (lib/scenarios/queries.ts) resolves tools by vendor+name and seat prices from access_tiers by name. - Nothing-design UI; admin gate lifted to a section layout. - Adds formatUSD0 to chart-format.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot review on the calculator verdict + tooltip - Introduce a three-way savingState (saves/costs/flat): equal-cost scenarios now read neutrally instead of as a negative, with no +/- sign rendered. - Guard savingPct against a zero baseline (no complete months yet): show 'less/more than' instead of a meaningless '0%'. - Distinguish partial-month tooltips: the most recent month is 'month-to-date'; an earlier partial month is labelled as a mid-month collection start. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: add read-only MCP server for AI-spend data (#112) Read-only MCP server (Streamable HTTP) exposing 7 AI-spend tools, shared-secret bearer auth, dormant by default until MCP_SERVER_SECRET is set. Reuses the existing read layer; no mutations or secret exposure. * feat(036): Budget / Cost Forecast Simulation scenario (#114) Adds the Budget / Cost Forecast Simulation scenario (/scenarios/budget-forecast): a pure projection engine + Nothing-design Recharts UI that anchors on actual spend-to-date and projects the fiscal year forward under per-tool growth assumptions vs an editable ceiling. Read-only, no schema change. 31 unit tests; Copilot + Vercel Agent review addressed. * feat(035): API threshold — keep light keys on metered API (#115) * feat(035): add API threshold to keep light keys on metered API Adds a lower "API threshold" to the API->Subscription calculator's right-sized scenario. Keys whose monthly spend falls below it stay on pay-as-you-go metered API instead of being forced onto a flat seat — mirroring the existing Premium threshold and defaulting to $25 (the Standard seat price, the break-even below which a seat can never pay off). The right-sized scenario is now three-band (API · Standard · Premium): - engine: SeatTier gains "api"; ScenarioInputs.apiThresholdCents; ScenarioResult.apiCount; mapSeat is Premium-first and an "api" key carries its own burn as seatCents (zero delta, foots the total). - client: a second slider paired with the Premium one (clamped so the floor can't exceed the ceiling), three-way readouts across the KPI, verdict, scenario card, comparison bar, table footer, and a new API SeatPill variant. - tests: new boundary + three-band anchors (47 keys -> $1,775.91/mo, 16 API/22 Std/9 Prem) plus an apiThreshold=0 superset test pinning the legacy $2,075 figures. No schema change; read-only over existing tables. Verified in-browser (default 16·22·9 -> $1,776/mo, 42% cut; clamp + reactivity confirmed). Specs: api-threshold-implementation-plan.html + implementation-notes.html under specs/035-scenario-calculators/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot review — accurate mapSeat docs + partition verdict - mapSeat JSDoc now describes the threshold rules (Premium-first; API floor; Standard otherwise) instead of "cheapest viable option", and notes that the cost-minimising reading only holds at the break-even defaults. - Verdict copy reworded to a true partition (joinParts helper, empty groups omitted) so it stays accurate for every mix — verified in-browser for the default, API-floor=0, and Standard-empty (clamp) cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot re-review — field doc + empty-population verdict - premiumThresholdCents JSDoc no longer says "otherwise Standard" (the API tier makes that wrong); points to mapSeat for the three-band split. - Verdict breakdown clause is now conditional (verdictLead), so count=0 (population=active with no active keys) reads "Right-sizing the 0 API users costs …" instead of dangling "— —". Singular "user" handled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(035): address Copilot pass 3 — seat $/mo precision + sort comment - Per-row "Seat $/mo" now uses formatCurrency for API rows so it matches the cents-precise "API basis" cell (was whole-dollar formatUSD0, showing e.g. $24 next to a $23.82 basis with a "—" delta). Whole-dollar seat prices keep formatUSD0. Verified in-browser: the two cells now match exactly. - TIER_SORT_ORDER comment reworded — it's a tier-escalation order (API→Standard→Premium), not "cheapest→priciest" (tiers are policy-assigned). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(026): upgrade budget extensions to latest main + Nothing design Post-merge upgrade pass for PR #102 against main at 2502462 (which now includes the Nothing design redesign #108 and the forecast scenario #114). UI migration to the Nothing design system (spec 028): - budget-detail-client: replace the removed sonner toasts with the StatusText/useInlineStatus idiom. Errors render inside the open dialog footer (a page-level status would sit behind the modal overlay); success renders in the extensions card header after close. - budget-extensions-card: adopt CardHeader/CardTitle/CardDescription structure, mono uppercase micro-labels, ink/destructive value colors, and a statusSlot for inline feedback. - add-extension-dialog: footer StatusText, border-based (never filled) preview panel and radio cards per the tags-are-border-only rule. - delete-extension-dialog: footer StatusText. - budget-health-hero: the "extended/reduced" tag is now a real Badge (border-only pill) instead of a filled bg-accent link. - budget-table: positive net extension uses text-ink (monochrome), red reserved for reductions. Conceptual fixes: - Remove the dead updateBudgetTotal action + schema. It had no UI or test callers left and was the one remaining way to silently break the total = original + extensions-sum invariant. Ceiling changes now go exclusively through extensions. - getBudgetWithCosts: stop leaking the joined linkedTool/creator objects into the RSC payload (explicit destructure). - schema: add inverse many(budgetExtensions) relations on users/aiTools. Migration timestamp fix (merge-blocking): - Bump 0023_white_gauntlet journal `when` to 1781080218076. The unmerged 034 branch applied its own 0023_perfect_runaways to the production DB on 2026-06-03 with a NEWER journal timestamp; drizzle-kit only applies entries newer than the DB last created_at, so our migration was silently skipped (verified on a fresh wt/budget-026 Neon branch). With the bump it applies cleanly everywhere. Test infra fix: - vitest.config.integration.mts now rewrites DATABASE_URL to the unpooled endpoint. The session-scoped advisory lock in syncInvoices leaks on the pooled endpoint (lock/unlock can hit different pooler backends), which made invoice-sync tests fail flakily and persistently. Verified: typecheck, lint, 484 unit tests, 25 integration tests (x2 runs) against Neon branch wt/budget-026 with migrations 0000-0023 applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(026): browser verification, labeled upgrade notes, dialog nit - Replace the pre-redesign verify screenshots with a fresh set captured against the Nothing UI on a wt/budget-026 Neon branch: dark + light, full create -> delete round-trip, dashboard badge, history column, forecast baseline reference line. - implementation-notes.html: five labeled entries ("Upgrade pass · 2026-06-10 · Claude (Fable 5)") covering the migration timestamp bump, updateBudgetTotal removal, the Nothing design migration decisions, the unpooled-endpoint test fix, and which review follow-ups are now closed. - add-extension-dialog: widen the sign select (w-20 truncated "+ add"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main shipped 0023_white_gauntlet (budget extensions, #102) while this branch carried its own 0023_perfect_runaways. Resolution drops this branch's 0023 SQL + snapshot + journal entry entirely — the ingestion schema delta will be regenerated as 0024 against main's 0023 state in the follow-up commit. schema.ts keeps both feature sets (ingestion discrimination + budget extensions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerated the ingestion-discrimination migration against main''s 0023 state via pnpm db:generate (identical DDL, new snapshot chained off 0023_white_gauntlet), with two deliberate edits: 1. Journal `when` bumped to 1781090000000 — above 0023_white_gauntlet''s 1781080218076. drizzle-kit only applies entries newer than the DB''s last applied created_at, so anything lower would be silently skipped on every database that already ran main''s migrations (the exact trap that bit #102). 2. The SQL is idempotent (DO $$ duplicate_object guards for the enums, IF NOT EXISTS for columns/index). The original 0023_perfect_runaways was applied to production on 2026-06-03 under its old hash, so the migrator will re-run this renamed file there — every statement must tolerate the objects already existing. Verified on Neon branch wt/034-migration-renumber: - production-state fork: 0024 re-applies cleanly over the existing DDL, journal advances to 24 rows - fresh database: full chain 0000-0024 applies in order; both the budget-extensions and ingestion schemas land - typecheck, lint, 494 unit tests, 25 integration tests all pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR resolves a Drizzle migration numbering collision on the spec-034 branch by renumbering the ingestion migration to 0024_* (and bumping the journal timestamp) so it will not be silently skipped on databases that already applied main’s 0023_* migration. It also brings the branch up to date with main by merging in budget-extension support, scenarios modeling infrastructure, and a read-only MCP server surface.
Changes:
- Renumber ingestion migration to
0024_greedy_ken_ellis, bump_journal.jsonwhen, and make the SQL idempotent for safe re-application. - Add “budget extensions” (schema + actions + UI) including
original_amount_centsbaseline tracking and baseline/extension visualization. - Introduce new admin “Scenarios” pages + data loaders, and a read-only MCP server endpoint with shared-secret auth and unit tests.
Reviewed changes
Copilot reviewed 74 out of 85 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.integration.mts | Forces integration tests onto the unpooled DB URL to avoid session-scoped advisory-lock flakiness. |
| tests/unit/scenarios/api-subscription.test.ts | Adds regression/unit coverage for the API→subscription scenario engine. |
| tests/unit/mcp/tools.test.ts | Unit tests for MCP tool registration and handler routing/error shaping. |
| tests/unit/mcp/format.test.ts | Unit tests for MCP formatting helpers (USD conversion + result envelopes). |
| tests/unit/mcp/auth.test.ts | Unit tests for MCP shared-secret auth helpers. |
| tests/integration/invoice-sync.test.ts | Updates integration fixtures for annual_budgets.original_amount_cents being required. |
| src/types/index.ts | Adds budget-extension types and originalCeilingCents to forecast type contracts. |
| src/middleware.ts | Excludes /api/mcp from NextAuth middleware redirect behavior. |
| src/lib/validators.ts | Adds budget-extension validator schemas (category, allocation, CRUD inputs). |
| src/lib/utils.ts | Adds centsToUsd() and routes formatCurrency() through it; formatting cleanups. |
| src/lib/scenarios/types.ts | Introduces framework-free scenario dataset types. |
| src/lib/scenarios/registry.ts | Adds a central scenario registry for the /scenarios index and routing metadata. |
| src/lib/scenarios/queries.ts | Adds server-side loader for API→subscription scenario dataset. |
| src/lib/scenarios/budget-forecast.ts | Adds pure budget forecast projection engine + presets. |
| src/lib/scenarios/budget-forecast-queries.ts | Adds server-only dataset assembly for the budget forecast scenario. |
| src/lib/scenarios/api-subscription.ts | Adds pure API→subscription scenario engine (basis/threshold mapping + totals). |
| src/lib/mcp/tools.ts | Registers the Hub’s read-only MCP tools with Zod-validated inputs. |
| src/lib/mcp/format.ts | Adds MCP result/format utilities (jsonResult/errorResult/safeJsonResult + usd helpers). |
| src/lib/mcp/data.ts | Implements MCP data assembly functions by delegating to existing read layers. |
| src/lib/mcp/auth.ts | Implements shared-secret bearer auth for MCP with constant-time comparison. |
| src/lib/forecast.ts | Extends budget forecasting to surface originalCeilingCents and exports OLS helper. |
| src/lib/env.ts | Adds optional MCP_SERVER_SECRET env var with minimum-length validation. |
| src/lib/db/schema.ts | Adds budget extension tables/enums/relations and annual_budgets.original_amount_cents. |
| src/lib/db/migrations/meta/_journal.json | Renumbers/journals migrations so 0024_* is applied after main’s 0023_*. |
| src/lib/db/migrations/0024_greedy_ken_ellis.sql | New idempotent ingestion migration (renumbered from the old 0023_*). |
| src/lib/db/migrations/0023_white_gauntlet.sql | Adds budget-extension DB objects (enum/tables/column/indexes). |
| src/lib/db/migrations/0023_perfect_runaways.sql | Removes the colliding old ingestion migration file. |
| src/lib/chart-format.ts | Adds whole-dollar and axis-tick USD helpers for charts. |
| src/lib/agent-auth.ts | Adds /api/mcp to the built-in deny list (defense-in-depth). |
| src/components/reports/budget/forecast-cumulative-chart.tsx | Renders an “original baseline” reference line when the ceiling was extended. |
| src/components/dashboard/admin/budget-hero-section.tsx | Shows an “extended ±$X” tag using original vs live ceiling. |
| src/components/dashboard/admin/admin-dashboard.tsx | Threads original ceiling through to the hero section. |
| src/components/app-sidebar.tsx | Adds “Scenarios” to the admin sidebar navigation. |
| src/app/scenarios/page.tsx | Adds Scenarios landing page/cards. |
| src/app/scenarios/layout.tsx | Admin-gates the /scenarios subtree. |
| src/app/scenarios/budget-forecast/page.tsx | Adds server entrypoint for the budget forecast scenario page. |
| src/app/scenarios/api-subscription/page.tsx | Adds server entrypoint for the API→subscription scenario page. |
| src/app/budget/page.tsx | Loads tool list for the budget extension dialog and passes it to the client page. |
| src/app/budget/components/period-allocations-table.tsx | Shows per-period “±$X from extension/reduction” sub-labels with anchor link. |
| src/app/budget/components/dialogs/index.ts | Exports new budget extension dialogs. |
| src/app/budget/components/dialogs/extension-form.ts | Adds client-side extension form model + parsing + allocation preview helpers. |
| src/app/budget/components/dialogs/delete-extension-dialog.tsx | Adds delete-confirmation dialog for extensions with inline status messaging. |
| src/app/budget/components/dialogs/add-extension-dialog.tsx | Adds add-extension dialog with allocation-mode UI and live preview. |
| src/app/budget/components/budget-health-hero.tsx | Shows baseline vs extended budget in the budget hero. |
| src/app/budget/components/budget-extensions-card.tsx | Adds extensions list card with add/delete actions and summary. |
| src/app/budget/components/budget-detail-client.tsx | Wires extension create/delete flows and keeps allocation inputs in sync. |
| src/app/budget/budget-table.tsx | Adds extensions count/net column to the budget history table. |
| src/app/budget/[id]/page.tsx | Loads tool list for per-budget extension dialogs (budget detail route). |
| src/app/api/mcp/[transport]/route.ts | Mounts MCP server endpoint using mcp-handler + shared-secret auth wrapper. |
| src/actions/scenarios.ts | Adds cached server actions to load scenario datasets (admin-only). |
| src/actions/dashboard.ts | Adds budgetOriginalCeilingCents to the admin dashboard data contract. |
| src/actions/budget.ts | Adds baseline ceiling tracking, extension summaries, and extensions join in budget reads. |
| src/actions/budget-extensions.ts | Adds server actions to create/update/delete budget extensions with allocation writes. |
| specs/036-budget-forecast-simulation/implementation-notes.html | Adds implementation notes for spec 036 (budget forecast simulation). |
| specs/034-mcp-server/implementation-plan.html | Adds MCP server implementation plan/spec documentation. |
| specs/026-budget-extensions/concept.md | Adds budget extensions concept/spec documentation. |
| package.json | Adds MCP dependencies (mcp-handler, @modelcontextprotocol/sdk). |
| docs/mcp-server.md | Adds operator/client documentation for the MCP server endpoint and tools. |
| CLAUDE.md | Notes the addition of the budget forecast scenario in recent changes. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // "Remaining" = periods whose endDate >= effectiveDate. Falls back to | ||
| // all periods if effectiveDate is before every period's end (i.e. | ||
| // backdated extensions covering the full year). |
There was a problem hiding this comment.
Fixed in e29ef12 — comment corrected: the fallback triggers when the effective date is after every period's end; backdated dates already match every period via the filter.
| amountCents: z | ||
| .number() | ||
| .int() | ||
| .refine((n) => n !== 0, { message: "Amount must be non-zero" }), |
There was a problem hiding this comment.
Fixed in e29ef12 — MAX_EXTENSION_CENTS now lives in validators.ts and is enforced in createBudgetExtensionSchema (abs ≤ $20M); extension-form.ts imports the shared constant so client and server can't diverge.
Both findings are in code inherited from main via the merge commit: 1. resolveAllocations comment had the fallback condition inverted — `remaining` is empty only when effectiveDate is AFTER every period''s endDate, not before (backdated dates match every period). 2. MAX_EXTENSION_CENTS was enforced only client-side; a crafted request with a huge amountCents would overflow the Postgres INTEGER column and surface as a raw DB error. The cap now lives in validators.ts on createBudgetExtensionSchema and the dialog form imports the shared constant, so client and server can never disagree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6c29dbb
into
claude/injection-types-distinction-Xcf9K
Summary
Fixes the migration-numbering collision between the 034 branch and main, discovered during the PR #102 upgrade pass.
Base is
claude/injection-types-distinction-Xcf9K(the 034 feature branch), not main — merging this makes PR #111 conflict-free and up to date with main without shipping the feature itself.The problem
0023_white_gauntlet(budget extensions, feat(budget): budget extensions (spec 026) #102), colliding with this branch''s0023_perfect_runaways.0023_perfect_runawayswas applied directly to the production DB on 2026-06-03 from this branch, and drizzle-kit only applies journal entries whosewhenis newer than the DB''s last appliedcreated_at. Without this fix, the 034 migration would be silently skipped on any database that already ran main''s migrations ("migrations applied successfully", no DDL executed) — the exact failure mode that bit feat(budget): budget extensions (spec 026) #102.The fix
budgetPeriodsRelationsin schema.ts; both feature sets kept).0024_greedy_ken_ellisviapnpm db:generate— identical DDL, snapshot correctly chained off0023_white_gauntlet.whento 1781090000000 (above0023_white_gauntlet''s 1781080218076) so databases already on main''s migrations still pick it up.DO $$ … duplicate_objectguards for enums,IF NOT EXISTSfor columns/index) — production already has this DDL under the old hash, so the migrator will re-run the renamed file there and every statement must tolerate existing objects.Verification
On Neon branch
wt/034-migration-renumber(fork of production):pnpm db:migratere-applies 0024 cleanly over the already-existing DDL; journal advances to 24 rows,last_when = 1781090000000.neondb_fresh): full chain 0000–0024 applies in order from empty; both the budget-extensions and ingestion schemas land (verified columns/tables/index).pnpm typecheck/pnpm lintclean; 494 unit tests and 25 integration tests pass on the merged branch.Risk
No production action needed at merge time — production already has the ingestion DDL; this only fixes the bookkeeping so the eventual #111 merge migrates correctly everywhere. Nothing changes on main until #111 itself merges.
🤖 Generated with Claude Code