Skip to content

fix(034): renumber ingestion migration to 0024 after #102 - #116

Merged
studert merged 8 commits into
claude/injection-types-distinction-Xcf9Kfrom
034-migration-renumber
Jun 10, 2026
Merged

fix(034): renumber ingestion migration to 0024 after #102#116
studert merged 8 commits into
claude/injection-types-distinction-Xcf9Kfrom
034-migration-renumber

Conversation

@studert

@studert studert commented Jun 10, 2026

Copy link
Copy Markdown
Member

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

  • Main shipped 0023_white_gauntlet (budget extensions, feat(budget): budget extensions (spec 026) #102), colliding with this branch''s 0023_perfect_runaways.
  • Worse: the original 0023_perfect_runaways was applied directly to the production DB on 2026-06-03 from this branch, and drizzle-kit only applies journal entries whose when is newer than the DB''s last applied created_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

  1. Merge main into the branch (one real conflict: budgetPeriodsRelations in schema.ts; both feature sets kept).
  2. Regenerate the ingestion migration as 0024_greedy_ken_ellis via pnpm db:generate — identical DDL, snapshot correctly chained off 0023_white_gauntlet.
  3. Bump journal when to 1781090000000 (above 0023_white_gauntlet''s 1781080218076) so databases already on main''s migrations still pick it up.
  4. Make the SQL idempotent (DO $$ … duplicate_object guards for enums, IF NOT EXISTS for 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):

  • Production-state fork: pnpm db:migrate re-applies 0024 cleanly over the already-existing DDL; journal advances to 24 rows, last_when = 1781090000000.
  • Fresh database (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 lint clean; 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

studert and others added 7 commits June 9, 2026 18:05
* 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>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ai-developer-hub Ready Ready Preview, Comment Jun 10, 2026 8:03am

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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.json when, and make the SQL idempotent for safe re-application.
  • Add “budget extensions” (schema + actions + UI) including original_amount_cents baseline 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.

Comment thread src/actions/budget-extensions.ts Outdated
Comment on lines +54 to +56
// "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).

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 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.

Comment thread src/lib/validators.ts Outdated
Comment on lines +200 to +203
amountCents: z
.number()
.int()
.refine((n) => n !== 0, { message: "Amount must be non-zero" }),

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 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>
@studert
studert merged commit 6c29dbb into claude/injection-types-distinction-Xcf9K Jun 10, 2026
7 checks passed
@studert
studert deleted the 034-migration-renumber branch June 10, 2026 08:04
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