Sidebar trial meter with Upgrade
## Original requirement (verbatim, epic 8925)
> When a new customer is on trial, they signed up for some agents, but they are not paying for them for 14 days. Let's give them unlimited agents and keep a tally of what was the max until now compared to what they signed up for. Something like 10/3.
>
> This needs to be web researched and analyzed and graphically represented in the sidebar with an unobtrusive Upgrade button that allows to increase the number of agents workin.
Settled inputs: the BA PRD (Task 8923, decisions D1–D7, assumptions AS1–AS6, criteria 1–10) and the Architecture (Task 8924, §3 components, §4 decomposition). This child is one row of that §4 table. The contracts quoted below are pinned; do not re-design them.
## Settled decisions this child relies on (verbatim from the PRD)
- **D1. The "max until now" number** is peak **demand** since the trial started: the maximum over time of **agents running + ready tasks the dispatcher held back only because of the org's agent cap** (the trial ceiling). Tasks waiting only for node or worktree capacity are not counted. The operator asked for "the maximum agents that were willing to run, but were limited by plan".
- **D2. "Unlimited" during the trial** means the runner-slot cap is lifted to the plan's **Max agents**. When the plan has no Max agents, a new operator-set per-plan **trial agent ceiling** applies. A blank ceiling falls back to a fixed default of 10. Node and worktree capacity still bound the count.
- **D3. Upgrade during a trial** raises the agent count without charging now. The new count is billed when the trial converts. The count field is prefilled with the peak. This lifts the `IN_TRIAL` refusal for the agent count only. Plan, interval and hosting changes stay refused during a trial. (Architecture amendment Q1: the trial agent change moves the count **both ways**, within the plan's min/max.)
- **D4. The meter shows only while the subscription is `trialing`.** It is hidden after conversion, cancellation or expiry.
- **D5. Audience.** Every member of the org sees the meter. Only Owners and Admins (`isOrgAdminSeat`) see the Upgrade button.
- **D6. Trial end.** At conversion the cap returns to the paid agent count. Running work is never killed; only new dispatch is limited. In the last 3 days of the trial, if peak is greater than signed-up, the meter turns warning-coloured and states the gap, e.g. "Trial ends in 2d — used 10, paying for 3".
- **D7. Scope** covers **per-agent trial plans only**. Flat plans have no customer-chosen "signed up for" number and are unchanged.
- **AS1.** An org override (`orgs.limit_runner_slots`) still wins over the trial ceiling. **AS2.** The `active_worktrees` cap must not hold trial dispatch below the trial ceiling. **AS3.** A staging org shares its parent's ledger; its demand counts toward the parent's meter and the meter shows on both. **AS5.** Peak is a high-water mark persisted per org and per trial, reset when a new trial starts.
## This child: C4 — Sidebar trial meter + Upgrade (and the integrated dev pass)
Research (A3, settled in the PRD): reverse-trial pattern; usage meters warn before the limit and offer in-place capacity add; contextual, non-interruptive upgrade prompts (GitLab puts trial status in the sidebar). Hence a compact block and a text-style button, no nags.
Approach (Architecture §3.7, verbatim):
* New `apps/web/src/components/TrialAgentsMeter.tsx`. `useQuery([TRIAL_AGENTS_QUERY], getTrialAgents, {refetchInterval: (q) => q.state.data?.trialing ? 60_000 : false, refetchOnWindowFocus: true, enabled: !!admin && admin.tenant !== false})`. Renders nothing unless `trialing`; any non-2xx (404 included) = hidden.
* Expanded rail (`apps/web/src/components/Sidebar.tsx`): compact block directly above `sidebar-footer` (the footer line is not touched): `Agents` · `10 / 3` (tabular numerals, never ellipsised — min-width from content) · a 4 px bar whose filled share is `signed_up / peak` (overshoot visible as the unfilled remainder) · `Trial · 9d left` · text-variant `Upgrade` button for `isOrgAdminSeat(admin)` only (D5).
* Collapsed rail: `10/3` only, native `title` with the full sentence (Sidebar's existing tooltip idiom).
* Phone (≤480 px): the same block at the bottom of the More sheet, above its footer (Q3); bottom bar unchanged.
* Warning (D6): one pure helper `trialMeterState(peak, signedUp, trialEnd, now)` → `{daysLeft, warn, text}`; `warn = daysLeft <= 3 && peak > signedUp`; text `Trial ends in 2d — used 10, paying for 3`; warning palette token, both themes. `daysLeft = ceil((trialEnd − now)/1d)`, min 0.
* Upgrade opens `ChangeAgentsDialog` (C3's trial mode) with `agents={count: signed_up, min: min_agents, max: max_agents}`, `initialCount = clamp(peak, min, max)`, and `publishableKey` from `getOrgBilling()` (`apps/web/src/lib/billing.ts`, `[ORG_BILLING_QUERY]`), fetched only when an admin clicks Upgrade — never on the 60 s poll. On success invalidate `TRIAL_AGENTS_QUERY` and `ORG_PLAN_QUERY`.
* Staging org (AS3): the meter shows the parent's figures; Upgrade is **not** rendered there; the title reads `Billed with {billed_with}`.
* Copy per `docs/standards/ui-copy-standards.md`; layout per `docs/standards/ui-style-guide.md`; no modal, toast or nag.
Tests: Vitest for the bar fill (`8 / 3` fills 37.5%; peak ≤ signed-up fills 100%), `trialMeterState` (days > 3 / ≤ 3 × peak > N / ≤ N), render hidden for `trialing:false` and for a 404, Upgrade only for admin seat, staging shows no Upgrade, collapsed shows `10/3` with title; Playwright at the supported phone width for the More sheet.
Integrated pass: this child merges last and runs criteria 1–9 end-to-end on dev with no toggle (criterion 10), using C1's fixtures.
Credentials & reach: (verbatim rows from the Architecture §7; the dev console host is `app.dev.jaah.ai` — the PRD's "dev.jaah.ai" means this host; operate dev only through the console UI)
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| Dev dispatch, sidebar, Upgrade, plan editor, trial-end move (criteria 1–6, 8–10) | `jaah/debug/main/operator-login` (operator; switches into tenants; Owner role in a test tenant; plain member created by that Owner in the console), dev CP `https://app.dev.jaah.ai` | verified — `credential_get` ok; `GET /` 200, `GET /api/auth/me` 401 (reached, unauthenticated) |
| Unit/API/Vitest/Playwright tests | worktree `./dc` stack only | verified — nothing beyond the worktree |
| Dev fixtures for criteria 1–3, 6, 8 (a per-agent plan with trial days, a trialing test-tenant subscription on dev's Stripe test account, a node with ≥ 8 free slots for that tenant) | same operator login, console only | deferred — fixture state, not access: the C1 builder creates it through the console (Products → plan; Customers → org + `set_trial_end`) before its dev check; scaled down if fewer slots exist (e.g. ceiling 3 vs N=1) |
Out of scope:
- Do NOT change trial length logic (`billing_plans.trial_days`, the `trial_days()` eligibility rules) or trial eligibility and reuse rules (`self_serve_rules.trial_used`).
- Do NOT lift `IN_TRIAL` for plan, interval or hosting changes. Only the agent-count change is opened.
- Do NOT auto-bill the peak, and do NOT change the agent count without an admin's explicit confirmation.
- Do NOT stop, pause or evict running tasks or sessions at trial end or on a cap change.
- Do NOT change caps or behaviour for flat (non-per-agent) plans, non-trialing subscriptions, or the other caps except as AS2 requires.
- Do NOT add a post-trial usage meter or a usage analytics page.
- Do NOT restructure the sidebar footer line (badge, name, version, Help). The meter is an addition above it.
- Do NOT add nag modals, toasts or emails for upgrade prompting.
- Deferred, do NOT build: post-trial used-vs-bought meter; operator Customers view of trial peak; `customer.subscription.trial_will_end` notices.
- Do NOT change the API contract `TrialAgentsOut` (C1) or the billing routes (C3); consume them.
- Do NOT change the phone bottom bar; the meter goes in the More sheet only.
Done means:
- **4. Sidebar meter renders (A4, D4, AS4).** For a trial org on a per-agent plan, every rendered sidebar surface shows "Agents", `peak / signed-up`, the bar and "Trial · Nd left". These surfaces are the expanded rail on desktop, the collapsed rail with a tooltip, and the More sheet at the supported phone width (`docs/standards/product-testing-requirements.md`).
→ Verify on dev in a browser: sign in as a member of a trial org, open any console page, observe the block at all three surfaces. Attach screenshots of each, light and dark.
→ Quality bar: the figures are fully legible and never truncated or ellipsised; the block does not crowd or overlap the footer line; it stays readable in light and dark theme. The Upgrade control reads as a quiet text button, not a filled primary button. The bar's filled share is `signed_up / peak` (Vitest: `8 / 3` → 37.5%), not peak/cap.
- **3 (UI). Peak shows in the sidebar.** → Verify on dev: the scenario in criterion 1 with ceiling 5 shows `8 / 3` in the sidebar.
- **5. Meter hidden when not applicable (all states).** The meter is not rendered for: a converted (`active`) subscription; a cancelled or ended subscription; a free or unpaid org; a flat-plan trial; operator (cross-tenant) views with no tenant org.
→ Verify: a web unit test covers each state. On dev, a converted org shows no block.
- **6. Upgrade button (A5, D3, D5).** Owners and Admins see an "Upgrade" control on the meter. Other members see the meter with no button. Clicking it opens the Change agents flow, prefilled with the peak. Confirming during the trial succeeds without a charge. The Subscription page then shows the new agent count, with billing at trial end. The trial does not end.
→ Verify on dev: as Owner of a trial org with N=3, peak 8, click Upgrade, observe 8 prefilled, confirm. Observe no invoice or charge, the trial-ends date unchanged, and the Subscription page shows 8 agents. As a plain member, observe no Upgrade control.
- **8. Trial-end warning (D6).** In the last 3 days of the trial with peak > signed-up, the meter turns warning-coloured and reads like "Trial ends in 2d — used 10, paying for 3". With peak ≤ signed-up, or more than 3 days left, it stays neutral.
→ Web unit test over (days left > 3 or ≤ 3) × (peak > N or ≤ N). Verify on dev by moving the trial end to 2 days out; the warning text is fully legible at all three surfaces.
- **10. On by default, real deployment.** No feature flag gates criteria 1–9. If one is introduced, it defaults ON and is wired into the dev deployment.
→ Verify: criteria 1–9 pass on `app.dev.jaah.ai` with no manual toggle (the integrated pass; criterion 7 is cited from C3's test-clock run).
Outcome
Sidebar trial meter (rail, collapsed, More sheet) with Owner/Admin Upgrade into Change agents; merged #6618 (af62316), quick gate + Vitest + 448px Playwright green, dev deploy ok. Visual check done: clean light/dark locally and on dev (0 / 3, 46d). Dev dispatch/peak legs not run: fixture has no node. Memory: none applied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)