Skip to content

feat(035): API threshold — keep light keys on metered API - #115

Merged
studert merged 4 commits into
mainfrom
worktree-api-subscription-calc
Jun 9, 2026
Merged

feat(035): API threshold — keep light keys on metered API#115
studert merged 4 commits into
mainfrom
worktree-api-subscription-calc

Conversation

@studert

@studert studert commented Jun 9, 2026

Copy link
Copy Markdown
Member

What & why

The API→Subscription calculator's right-sized scenario used to force every key onto a flat seat — even a key burning $3/mo got a $25 Standard seat, which costs more than leaving it on the API. This adds a lower API threshold: below it, a key stays on pay-as-you-go metered API.

It mirrors the existing Premium threshold and defaults to $25 — the Standard seat price, i.e. the break-even below which a seat can never pay off. The result is a genuine three-band right-sizing (API · Standard · Premium) and a right-sized total that is now provably ≤ the metered baseline at default thresholds.

Changes

  • Engine (src/lib/scenarios/api-subscription.ts): SeatTier gains "api"; ScenarioInputs.apiThresholdCents; ScenarioResult.apiCount. mapSeat is Premium-first; an "api" key carries its own spend as seatCents (zero delta, still foots the right-sized total). standardCount = count − premiumCount − apiCount.
  • Client (api-subscription-client.tsx): a second slider paired with the Premium one (clamped so the floor can't exceed the ceiling); three-way readouts across the threshold cell, KPI, verdict (which omits the "keeping N keys" clause when none are metered), scenario card, comparison bar, and table footer; a new API SeatPill variant. /simplify pass extracted a ThresholdSlider component and SEAT_PILL / TIER_SORT_ORDER lookups.
  • Tests: boundary cases + three-band anchors, plus an apiThreshold = 0 superset test pinning the legacy figures.

No schema change — read-only over existing tables. No new dependency.

Regression anchors (recomputed on the 2026-06-09 fixture)

Population API · Std · Prem Right-sized /mo vs baseline
All 47 16 · 22 · 9 $1,775.91 (was $2,075) −42% (was 32%)
Active 43 16 · 20 · 7 $1,475.91 −39%
apiThreshold=0 (superset) 0 · 38 · 9 $2,075 legacy, pinned in tests

Note: the default headline figures intentionally change because the floor defaults to $25 — the two shipped anchors were updated and a superset test preserves the old numbers.

Verification

  • pnpm test (22 passed) · pnpm typecheck clean · pnpm lint clean (--max-warnings 0).
  • 4-way adversarial review (engine math / test coverage / UI completeness / quality+a11y+tokens) → all pass.
  • In-browser (Playwright, agent session): default renders 16 API · 22 Standard · 9 Premium → $1,776/mo, 42% cut; dragging the floor to $0 reverts to legacy 38-Standard; clamp confirmed (Premium → $20 drags the floor down); 16 API pills in the table.

Docs & open questions

Plan + running notes under specs/035-scenario-calculators/ (api-threshold-implementation-plan.html, implementation-notes.html). Open questions for review: the "Keep on API <" label wording; whether to refresh the standalone prototype.html to the three-band split.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings June 9, 2026 18:25
@vercel

vercel Bot commented Jun 9, 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 9, 2026 6:53pm

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

Adds a lower “API threshold” to the API→Subscription scenario calculator so low-spend keys can remain on metered API (three-band right-sizing: API · Standard · Premium), aligning the “right-sized” scenario with break-even logic and updating regression anchors accordingly.

Changes:

  • Engine: introduce SeatTier: "api", add apiThresholdCents input and apiCount output, and extend mapSeat/computeScenarios for three-band assignment.
  • Client: add an API-threshold slider (clamped to ≤ Premium threshold), update all mix readouts to three-way, add an API SeatPill variant, and factor shared slider/tier presentation logic.
  • Tests + specs: add boundary/superset tests (including apiThreshold=0 legacy pin) and add HTML implementation plan/notes.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/scenarios/api-subscription.test.ts Updates scenario anchors and adds boundary + legacy-superset coverage for the new API floor behavior.
src/lib/scenarios/api-subscription.ts Extends the pure calculation engine to support the new API tier and tallies apiCount.
src/app/scenarios/api-subscription/api-subscription-client.tsx Adds the API threshold control and propagates the three-tier model through sorting, pills, mix readouts, verdict text, and table footer.
specs/035-scenario-calculators/implementation-notes.html Adds implementation notes capturing decisions, deviations, and verification outcomes for the increment.
specs/035-scenario-calculators/api-threshold-implementation-plan.html Adds the implementation plan/spec documenting intended behavior, anchors, and acceptance checklist.

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

Comment thread src/lib/scenarios/api-subscription.ts
Comment thread src/app/scenarios/api-subscription/api-subscription-client.tsx Outdated
…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>

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/lib/scenarios/api-subscription.ts Outdated
Comment thread src/app/scenarios/api-subscription/api-subscription-client.tsx Outdated
…rdict

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

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/lib/scenarios/api-subscription.ts
Comment thread src/app/scenarios/api-subscription/api-subscription-client.tsx
- 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>
@studert
studert merged commit 2502462 into main Jun 9, 2026
7 checks passed
@studert
studert deleted the worktree-api-subscription-calc branch June 9, 2026 19:06
studert added a commit that referenced this pull request Jun 10, 2026
* 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>
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