- Adds optional Budget limit (USD) numeric field to CreateWorkspaceDialog;
blank = null (unlimited), populated = parsed float sent as budget_limit in
POST /workspaces body
- Adds budget_limit field to DetailsTab edit form; saves via
PATCH /workspaces/:id; pre-fills from current WorkspaceNodeData
- Shows 'Budget limit exceeded' warning badge when budgetUsed > budgetLimit
(forward-compatible — badge hidden when budgetUsed is absent)
- Extends WorkspaceData, WorkspaceNodeData, and buildNodesAndEdges to carry
budgetLimit / budgetUsed fields ready for backend hydration (issue #541 BE PR)
- Ships 22 new tests across CreateWorkspaceDialog and BudgetLimit.DetailsTab
suites (575 total, all passing); npm run build clean; 'use client' grep empty
API shape confirmed from workspace.go and CreateWorkspacePayload struct:
field name: budget_limit | type: number | null | units: USD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- #554 CRITICAL: Add hydrationError state to Zustand store; catch handler now
calls setHydrationError instead of silent console.error; page renders a
full-screen zinc-950 error banner with a Retry button that reloads the page
- #556 MEDIUM: Add roving tabIndex + ArrowDown/Up/Left/Right keyboard handler
to the tier radio group in CreateWorkspaceDialog (WCAG 2.1 compliant)
- #557 MEDIUM: Add "Zoom to Team" menu item to ContextMenu (visible only when
node has children); dispatches molecule:zoom-to-team for keyboard accessibility
- Bonus: add missing 'use client' directive to RevealToggle.tsx
Co-authored-by: Molecule AI Frontend Engineer <frontend-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Settings panel: wire TokensTab into "API Tokens" tab (was imported
but not rendered). Rename "API Keys" → "Secrets", add "API Tokens"
tab. Fix docs link → doc.moleculesai.app/docs/tokens.
2. Referer match hardening: require exact host match or trailing slash
to prevent evil.com subdomain bypass. Cache CANVAS_PROXY_URL at
init time instead of per-request os.Getenv.
3. Extract shared deriveWsBaseUrl() to lib/ws-url.ts — eliminates
duplicate 12-line derivation in socket.ts and TerminalTab.tsx.
4. Token list pagination: add ?limit= and ?offset= params (default
50, max 200) to GET /workspaces/:id/tokens.
507/507 canvas tests pass, Go build + vet clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs on the combined tenant image (canvas + API same-origin):
1. WebSocket URL: NEXT_PUBLIC_WS_URL="" (empty string for same-origin)
was preserved by ?? operator, producing an invalid WS URL. Now derives
from window.location when both env vars are empty. Same fix applied
to TerminalTab.
2. AdminAuth blocking canvas: same-origin requests have no Origin header,
so neither AdminAuth nor CanvasOrBearer could authenticate the canvas.
Added isSameOriginCanvas() that checks Referer against request Host,
gated behind CANVAS_PROXY_URL (only active on tenant image). This
lets the canvas create/list workspaces, view events, etc. without a
bearer token when served from the same Go process.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace setCenter(x, y, {zoom:1}) with fitView({nodes:[{id}]}) in the
molecule:pan-to-node handler (Canvas.tsx). The old implementation forced
zoom=1 regardless of the user's current zoom level, which was jarring when
panned/zoomed away. fitView adapts to whatever zoom the user had and
gracefully fits the new node in view.
Tests:
- Canvas.pan-to-node.test.tsx: fitView called with correct nodeId after
100ms debounce; debounce coalesces rapid successive events.
- canvas-events-pan.test.ts: molecule:pan-to-node dispatched for new
provisions only, NOT on restart of an existing node.
Fixes#426.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of position collision after node deletion:
handleCanvasEvent(WORKSPACE_PROVISIONING) used nodes.length as the
grid placement index. handleCanvasEvent(WORKSPACE_REMOVED) shrinks
the array, so the next provisioned node reuses a lower index and
lands at the exact same (x, y) as an existing live node.
Example (4-col grid, COL_SPACING=320):
Provision A → idx 0 → (100, 100)
Provision B → idx 1 → (420, 100)
Provision C → idx 2 → (740, 100)
Remove A → nodes.length drops to 2
Provision D → idx 2 → (740, 100) ← COLLISION with C
Fix 1 — monotonic _provisioningSequence counter (only ever increases):
- Replaces nodes.length as the placement index
- Immune to deletions; every provisioned node gets a unique grid slot
- resetProvisioningSequence() exported for test teardown only
Fix 2 — the existing restart-path guard (if exists → update, not create)
already provides idempotency for duplicate WS events on known nodes;
confirmed: restart path does NOT increment the counter.
Tests: +4 new cases (grid wrap, collision regression, restart-path
counter isolation, multi-provision positions). 485/485 pass.
Build: next build ✓ clean.
Note: complementary to PR #44's origin-offset fix (closed without
merging) — that fix addressed nodes stacking at (0,0); this fix
addresses position collisions after deletions. Both should land.
Co-authored-by: Canvas Agent <agent@canvas.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
New nodes were placed at (0,0) or close to it, causing them to spawn
behind the toolbar/palette chrome and require manual panning to find.
Add GRID_ORIGIN_X/Y = 100 offset so the first node lands in clear canvas
space, and update the position assertion in the unit test accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gap 1 — WS_URL now derives from NEXT_PUBLIC_PLATFORM_URL when
NEXT_PUBLIC_WS_URL is not set (http→ws, appends /ws; https→wss).
Operators need only one env var. NEXT_PUBLIC_WS_URL remains an explicit
override escape hatch.
Gap 2 — Add canvas/.env.example documenting NEXT_PUBLIC_PLATFORM_URL
(required) and NEXT_PUBLIC_WS_URL (optional override, commented out).
Gap 3 — Toolbar fires showToast("Live updates restored", "success")
when wsStatus transitions connecting→connected. mountedRef (set after
2 s) suppresses the toast on the very first page-load connection so
only genuine reconnects notify the user.
Gap 4 — New canvas/src/store/__tests__/socket.url.test.ts (6 tests):
· fallback to ws://localhost:8080/ws when no env set
· http→ws derivation from NEXT_PUBLIC_PLATFORM_URL
· https→wss derivation
· NEXT_PUBLIC_WS_URL override takes precedence
· api.ts PLATFORM_URL fallback
· api.ts reads NEXT_PUBLIC_PLATFORM_URL
375/375 tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a live/reconnecting/offline pill to the Toolbar so users can see
at a glance whether the canvas is receiving real-time updates.
Changes:
- canvas/src/store/canvas.ts: add wsStatus ('connected'|'connecting'|
'disconnected') field + setWsStatus action to CanvasState (initial:
'connecting')
- canvas/src/store/socket.ts: wire setWsStatus into ReconnectingSocket —
'connecting' on connect() call, 'connected' in onopen, 'connecting'
in onclose (will reconnect), 'disconnected' in disconnect()
- canvas/src/components/Toolbar.tsx: subscribe to wsStatus; render
WsStatusPill (green "Live" / amber pulsing "Reconnecting" / red
"Offline") after the workspace count section
- canvas/src/store/__tests__/socket.test.ts: add setWsStatus: vi.fn()
to the canvas store mock (global factory, beforeEach reset, and the
mid-test override in the onmessage test)
369/369 canvas tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- computeAutoLayout() BFS tree layout seeds from anchored nodes; assigns
distinct x/y to workspaces returned at 0,0 by the API and persists via PATCH
- buildNodesAndEdges() accepts layoutOverrides map so hydration uses computed
positions instead of raw 0,0 coordinates
- canvas-events WORKSPACE_PROVISIONING grid layout replaces offset===offset
assignment that caused position:{x:t,y:t} in the minified bundle
- 8 new vitest tests cover computeAutoLayout and override behaviour (365 pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>