Wraps /orgs in a TermsGate that polls /cp/auth/terms-status on mount
and overlays a blocking modal when the current terms version hasn't
been accepted yet. "I agree" POSTs /cp/auth/accept-terms and dismisses
the modal; the backend records IP + UA as GDPR Art. 7 proof-of-consent.
Also adds a short data residency notice under the page header:
workspaces run in AWS us-east-2 (Ohio, US). An EU region selector is
a future lift once the infra is provisioned there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the UI surface for the credit system to /orgs:
- CreditsPill next to each org row. Tone shifts from zinc → amber at
10% of plan to red at zero.
- LowCreditsBanner appears under the pill for running orgs when the
balance crosses thresholds: overage_used > 0 → "overage active",
balance <= 0 → "out of credits, upgrade", trial tail → "trial almost
out".
- Pure helpers extracted to lib/credits.ts so formatCredits, pillTone,
and bannerKind are unit-tested without jsdom.
Backend List query now returns credits_balance / plan_monthly_credits
/ overage_used_credits / overage_cap_credits so no second round-trip
is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small polish items that together close the signup-to-running-tenant
flow for real users:
1. Stripe success_url now points at /orgs?checkout=success instead of
the current page (was pricing). The old behavior left people staring
at plan cards with no indication payment went through — the new
behavior drops them right onto their org list where they can watch
the status flip.
2. /orgs shows a green "Payment confirmed, workspace spinning up"
banner when it sees ?checkout=success, then clears the query
param via replaceState so a reload doesn't show it again.
3. /orgs now polls every 5s while any org is awaiting_payment or
provisioning. Users see the Stripe webhook's effect live — no
manual refresh needed — and once every org settles the polling
stops so idle tabs don't hammer /cp/orgs.
Paired with PR #992 (the /orgs page itself) this makes the end-to-end
flow on BILLING_REQUIRED=true deployments feel right:
/pricing → Stripe → /orgs?checkout=success → banner → live poll →
"Open" button when org.status transitions to running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CP's Callback handler redirects every new WorkOS session to
APP_URL/orgs, but canvas had no such route — new users hit the canvas
Home component, which tries to call /workspaces on a tenant that
doesn't exist yet, and saw a confusing error. This PR plugs that gap
with a dedicated landing page that:
- Bounces anonymous visitors back to /cp/auth/login
- Zero-org users see a slug-picker (POST /cp/orgs, refresh)
- For each existing org, shows status + CTA:
* awaiting_payment → amber "Complete payment" → /pricing?org=…
* running → emerald "Open" → https://<slug>.moleculesai.app
* failed → "Contact support" → mailto
* provisioning → read-only "provisioning…"
- Surfaces errors inline with a Retry button
Deliberately server-light: one GET /cp/orgs, no WebSocket, no canvas
store hydration. Goal is to move the user from signup to either
Stripe Checkout or their tenant URL with one click each.
Closes the last UX gap between the BILLING_REQUIRED gate landing on
the CP and real users being able to complete a signup today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-launch audit flagged api.ts as missing a timeout on every fetch.
A slow or hung CP response would leave the UI spinning indefinitely
with no way for the user to abort — effectively a client-side DoS.
15s is long enough for real CP queries (slowest observed is Stripe
portal redirect at ~3s) and short enough that a stalled backend
surfaces as a clear error with a retry affordance.
Uses AbortSignal.timeout (widely supported since 2023) so the
abort propagates through React Query / SWR consumers cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
amber-400 on zinc-900 is 5.4:1 (AA pass). amber-300 is 6.9:1 (AA+AAA pass)
and matches the rest of the amber usage in WorkspaceNode (currentTask,
error detail, badge chip).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sets up shadcn/ui CLI so new components can be added with
`npx shadcn add <component>`. Uses new-york style, zinc base color,
no CSS variables (matches existing Tailwind-only approach).
Adds clsx + tailwind-merge for the cn() utility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
next dev --turbopack for significantly faster dev server startup
and hot module replacement. Build script unchanged (Turbopack for
next build is still experimental).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds role="button", tabIndex, aria-label="Select <name>", and keyboard
handlers (Enter/Space) to TeamMemberChip. Fixes 5 failing a11y tests
from issue #831. Updates eject button test to match existing label format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Conflicts arose because PR #892 base commits (MemoryInspectorPanel creation,
A2A overlay) had already landed on main via a different merge path, and
last-tick merges (#876, #888) had modified Toolbar, SidePanel, and test
fixtures.
Resolution strategy:
- Toolbar.tsx, SidePanel.tsx, Canvas.a11y.test.tsx, Canvas.pan-to-node.test.tsx,
MemoryInspectorPanel.test.tsx: take main (strictly newer, already contains
the branch's A2A overlay content plus subsequent a11y/UX fixes)
- MemoryInspectorPanel.tsx: take main (543 lines with semantic search) + apply
sanitizeId() helper from #904 + update bodyId prefix to mem-body-
- DetailsTab.tsx: take main (has #875 Field/useId + #878 deleteButtonRef/focus)
+ apply alertdialog structure from #905 while preserving focus management
Mechanical conflict resolution by triage-agent; no logic changes beyond the
four a11y fixes already in the branch (#902-#905).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #878 landed before this branch and added useRef + deleteButtonRef focus-
management to DetailsTab.tsx. This commit combines that import with the
useId/cloneElement import added here, and preserves the Field component
htmlFor/id wiring from this PR unchanged.
Mechanical conflict resolution by triage-agent; no logic changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
role="alert" is for passive announcements. A delete confirmation with
Confirm/Cancel action buttons requires a user response, which is the
semantics of role="alertdialog" (interactive dialog requiring response).
- Replace role="alert" with role="alertdialog" + aria-modal="true"
- Add aria-labelledby="delete-confirm-title" for an accessible name
- Add <h3 id="delete-confirm-title"> as the labelling element
("Confirm deletion") so AT announces the dialog purpose on focus
Closes#905
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Memory keys can contain characters like [ ] / : . # and spaces that make
invalid HTML id values (breaks CSS selectors and ARIA id-ref lookups).
- Add sanitizeId() helper: replaces non-alphanumeric chars with hyphens,
collapses consecutive hyphens, strips leading/trailing hyphens
- Compute bodyId = "mem-body-{sanitizeId(entry.key)}" in MemoryEntryRow
- Set id={bodyId} on the expanded body container
- Set aria-controls={bodyId} on the toggle button so AT can navigate
directly between the button and its controlled panel
Closes#904
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add aria-pressed={filter === f.id} to every filter pill button so AT
announces which filter is currently active
- Add aria-pressed={autoRefresh} to the auto-refresh toggle so AT
announces the live/paused state when the button is activated
Closes#903
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add role="alert" to the global error banner and the inline add-form
error message so screen readers announce errors immediately on render
- Add aria-label to all three add-form inputs (key / value / TTL) so
every form control has an accessible name (was flagged as unlabelled)
- Add aria-expanded={expanded === entry.key} to each entry toggle button
so AT announces collapsed/expanded state on activation
Closes#902
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The error banner div introduced in the MemoryInspectorPanel (PR #892)
was missing role="alert", regressing the a11y standard established in
PR #877 / issue #830. Screen readers now announce the error immediately
on render.
Closes#901
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per UIUX Cycle 5 spec, Dialog.Content should carry an explicit
aria-label="Conversation trace" in addition to the aria-labelledby
automatically wired by Radix Dialog via Dialog.Title. This provides
a fallback accessible name directly on the dialog container element.
All 732 tests pass, build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Corrects the source-input aria-label wording to match the UIUX Cycle 4
spec exactly. Previous commit used "Install plugin from source URL";
spec says "Install from source URL" (matches the visible "Install from
source" section heading). Updates the corresponding test assertions.
No functional change. All 736 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkspaceNode.eject.test.tsx: add draggable/selectable/deletable to
NodeProps render call (TS2739); add `as WorkspaceNodeData` cast on
makeNodeData return to silence Partial<> spread widening (TS2322)
The cherry-picked fix/canvas-test-fixture-budgetlimit commit (9e0aa61)
also lands here — it resolves latent test-fixture drift in 7 test files
that the incremental tsc cache had masked on main but that became visible
once the new WorkspaceNode.eject.test.tsx file invalidated the cache.
tsc --noEmit: 0 errors | npm test: 726 passed | npm run build: clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The budget PR (#541) added budgetLimit: number | null as a required field
on WorkspaceNodeData and budget_limit: number | null on WorkspaceData.
Seven test fixture factories were not updated, causing tsc --noEmit to
produce 34 TS2322/TS2345 errors (runtime tests still passed because
Vitest transpiles via esbuild which strips types).
Fixes:
- canvas-events.test.ts: makeNode factory +budgetLimit: null
- canvas-events-pan.test.ts: makeNode factory +budgetLimit: null
- canvas-capabilities.test.ts: makeNodeData factory +budgetLimit: null
- canvas-topology.test.ts: makeWS factory +budget_limit: null
- canvas.test.ts: makeWS factory +budget_limit: null; two inline
summarizeWorkspaceCapabilities args +budgetLimit: null; context-menu
fixture +budgetLimit: null
- ProvisioningTimeout.test.tsx: makeWS factory +budget_limit: null
Also fixes 3 TS2348 errors in AuthGate.test.tsx: newer Vitest type defs
resolve ReturnType<typeof vi.fn> to Mock<Procedure|Constructable> which
TypeScript no longer considers directly callable in a vi.mock factory.
Fix: intersect the mock variables with a plain function type so both the
call expression and the mock API (mockReturnValue etc.) type-check.
tsc --noEmit: 0 errors. npm test: 722/722.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EjectIcon now accepts React.SVGProps<SVGSVGElement> so aria-hidden can be passed
- Eject button: aria-label and title both use `Extract ${data.name} from team`
(previously title was static 'Extract from team'; aria-label was absent)
- <EjectIcon aria-hidden="true"> prevents assistive tech from double-announcing
the icon content inside the already-labelled button
- Added WorkspaceNode.eject.test.tsx (4 tests) covering aria-label, title,
label==title invariant, and aria-hidden on the SVG
Closes#854
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two WCAG violations in the Danger Zone delete flow:
1. WCAG 4.1.3 (Status Messages): the confirmation UI that appears when
the user clicks "Delete Workspace" had no ARIA live region, so screen
readers never announced the confirmation prompt. Adding role="alert"
to the confirmation container makes it an implicit assertive live
region that is announced immediately.
2. WCAG 2.4.3 (Focus Order): pressing Cancel left focus wherever the
browser placed it (often body). Keyboard users had to re-navigate to
find the Delete Workspace button. The Cancel handler now calls
deleteButtonRef.current?.focus() to return focus to the trigger
button, matching the expected modal/disclosure focus-management pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the onboarding card had no landmark role and no
live region, so screen readers had no way to know the card exists or
that the step changed.
- Add role="complementary" aria-label="Onboarding guide" to the card
container so it appears as a named landmark in assistive technology.
- Add a role="status" aria-live="polite" aria-atomic="true" sr-only div
that holds the current step label. When the step state changes React
updates the div content, which the live region broadcasts to the AT
without pulling focus away from the user's current position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NVDA and other screen readers ignore the title attribute on interactive
elements and non-interactive divs. Add aria-label alongside title on:
- Stop All button (dynamic label reflects active task count)
- Restart All button (dynamic label reflects pending workspace count)
- StatusPill component (online/offline/failed/provisioning counts)
- WsStatusPill component (connected/connecting/disconnected variants)
Inner dot and text spans get aria-hidden="true" so the screen reader
reads the single aria-label rather than individual child nodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the error div that appears after a failed workspace
deploy or blank-workspace create had no ARIA live region, so screen
readers never announced it. Adding role="alert" makes the message an
implicit aria-live="assertive" region so assistive technology surfaces
the error immediately without requiring the user to navigate to it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire WCAG 1.3.1 label associations: 6 bare <label>+control pairs in
ConfigTab (Description, Tier, Runtime, Effort, Task Budget, Backend) now
use stable useId() IDs with matching htmlFor/id. Field helper in
DetailsTab updated to generate its own fieldId via useId() and inject it
into the child element via cloneElement, so every Name/Role/Tier field in
edit mode is correctly associated without requiring call-site changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add role="separator" + aria-valuenow/min/max/orientation + tabIndex={0}
to make the resize handle focusable and discoverable by screen readers
(WAI-ARIA slider pattern). Add onKeyDown handler: ArrowLeft/Right moves
by 16px, Home/End snaps to min/max. Persist width to localStorage on
keyboard resize, matching the existing mouse behaviour.
Focus ring uses focus-visible:ring-2 to avoid showing on mouse click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously loadMessagesFromDB swallowed all errors and returned [] — a
network failure was indistinguishable from an empty history, so the user
had no way to know loading failed. Now the function returns
{ messages, error } and the MyChatPanel renders a role="alert" banner
with the error message and a Retry button when messages are empty and
a load error occurred.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace title attribute (not read by screen readers for truncated text)
with aria-label, add role="status" so live regions announce the error,
and raise text color from text-amber-300/60 (~2.1:1) to text-amber-400
(~10.6:1) to meet WCAG AA contrast (4.5:1 minimum).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add bodyId derived from entry.key, attach aria-controls={bodyId} to the
toggle button, and add id={bodyId} role="region" aria-label to the
collapsible body div. Screen readers can now announce the expand/collapse
relationship between the button and the region it controls (WCAG 4.1.2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Badge was always text-zinc-500; apply blue-500 (>=0.8), zinc-400 (0.5–0.8),
zinc-600 (<0.5) per spec. Add 3 vitest tests for each color tier (725 total).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>