feat(dashboard): dashboard restyle + self-contained mock target#820
feat(dashboard): dashboard restyle + self-contained mock target#820law-chain-hot wants to merge 37 commits into
Conversation
… env Local dashboard dev calls a same-origin /api path; Vite proxies it to the selected backend, so the browser never makes a cross-origin (CORS-gated) request and no backend has to allow-list localhost. Replaces the prior VITE_BASE_API_URL=dev direct-call approach, which the browser blocked with CORS. - start-dashboard.mjs: API_TARGETS map (local/dev + prod placeholder), --api=<url> for any env, --yes-prod guard for prod - vite.config.mts: /api proxy target read from DASHBOARD_API_PROXY_TARGET - package.json: start:prod (replaces throwaway start:dev-proxy) - apps/CLAUDE.md: fix 'from repository root' -> apps/, document the proxy model
…om/boxlite-ai/boxlite into claude/loving-carson-e4a7cf
…n onboarding - BoxTable: remove the Region column from the desktop table and the Region entry from the compact mobile meta. The box table no longer surfaces region. - Remove the now-unused getRegionName plumbing from the box-table path (Boxes page, BoxTable, useBoxTable, columns, types) and the stale 'region' entry from persisted column visibility. useRegions/getRegionName stay in place for the other consumers (Runners, Org Settings, Box details). - onboarding-code-examples: use the fully-qualified image reference ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3 across all four SDK snippets (JS/TS, Python, Go, Rust) instead of the short 'boxlite/base'.
- BoxTable: drop the stale min-w-[1360px] (sized for removed columns) to min-w-[1120px] so the table fills the viewport without horizontal scroll, and tighten row vertical padding (py-2 -> py-1, scoped to this table) so rows are ~41px instead of ~46px. - use-mobile: lower COMPACT_BREAKPOINT 1200 -> 1024 so the dashboard keeps the full table (not the compact card layout) down to 1024px.
Adopt exe.dev's calm/minimal/progressive-disclosure feel on the box list. List minimalism + progressive disclosure: - Default visible columns reduced to Name + Last Event + actions. Box ID, State, Resources, and Created At are hidden by default and toggleable via the View menu (TanStack columnVisibility + localStorage, bumped to a V2 key so the old forced 'id:true' default does not stick). - Status now leads the Name cell as a compact status dot (BoxState gains an iconOnly mode with a tooltip) instead of a dedicated State column. Centered-narrow layout: - New PageLayout 'content' size (~1040px); Boxes list switches off 'full' (1440px) onto it, so the list centers with side whitespace instead of filling the viewport. Table min-width lowered 1120 -> 680 for the trimmed row. Visual calm (global tokens): - Root font-size 15px -> 14px; --border lightened to a hairline; Card drops shadow-sm to rely on the border. Warm-cream brand palette kept. Verified in Chrome against the dev API: list centers, shows Name+dot / Last Event / actions, View menu toggles the four hidden columns back on.
…ass 2) Restructure the box detail page to exe.dev's calm, centered, progressive layout (approach B: observability moves into the column, not a full-bleed split). - BoxDetails: drop the full-bleed resizable Group/Panel split and the desktop/mobile duality. Detail now flows in one centered max-w-[1040px] scrolling column: metadata grid -> content tabs. Removes isDesktop / react-resizable-panels / ScrollArea usage. - BoxHeader: compress the two-line header (name over Box ID) into a single row (name + copy + inline mono Box ID + copy). - SDK 'Connect with the SDK' nudge demoted from a full-width banner owning the top to a compact inline card inside the column. - BoxInfoPanel: reflow the vertical sidebar sections into a 2-column metadata grid and surface fields that were in the Box model but never rendered — Image (full ghcr ref) and Region (getRegionName(target)), plus Labels. - BoxContentTabs: drop the now-redundant Overview tab (metadata is always shown above) and give the tab shell a bounded height (60vh) so the terminal and observability tabs render correctly inside the scrolling column. - Remove the now-dead InfoSection/InfoRow exports. Verified in Chrome: detail centers in ~1040px, header is one line, metadata grid shows Image + Region, terminal tab renders bounded.
Both the global top nav and the box-detail header spanned wider than the new centered ~1040px content column, so they didn't line up with it. - Sidebar (global top nav): constrain inner content max-w 1440 -> 1040 so the logo/tabs/search align with the page content column. - BoxHeader: center its content in a max-w-[1040px] inner wrapper (full-width border-b kept as the divider) so back/name align left and actions align right with the content below. Verified in Chrome: nav, box header, and detail content share one centered 1040px column on both the list and detail pages.
…inent - Org switcher/display moves out of the top nav into the right-side profile dropdown as an Organization section (org name -> settings, Copy organization ID). The 'Copy Organization ID' command-palette entry is preserved (re- registered in Sidebar). Removes the now-unused OrganizationPicker component. - Make the primary state action prominent: Start / Stop / Recover use the filled default button variant instead of muted outline, in both the box-table row (desktop + mobile) and the detail header, so the main action stands out from the secondary (terminal / SSH / overflow) buttons. Verified in Chrome: nav no longer shows the org pill; profile menu shows the Organization section; Start/Stop render as filled buttons in list and detail.
…ge row - BoxInfoPanel: remove the Auto-stop and Auto-delete metadata rows. The auto-stop/auto-delete feature is not supported (pipeline removed), so the dashboard should not surface it. (Note: box.autoStopInterval/autoDeleteInterval still exist on the api-client Box model — a backend/model remnant to purge separately.) - BoxDetails: remove the 'Connect with the SDK' nudge row from the detail page (misaligned and redundant — the guide is still reachable from the nav). Drop the now-orphaned onboardingCoreProgress/showOnboardingNudge and unused imports (getOnboardingCoreProgress, Code2, ListChecks, formatDuration).
- BoxTableHeader: remove the Refresh, View (column visibility), and Filter (state/last-event) controls. The toolbar is now just the box Search + the Create Box action. Removes the active-filter indicator row. - Drop the now-orphaned refresh plumbing end to end: onRefresh/isRefreshing from BoxTableHeaderProps and BoxTableProps, the passthrough in BoxTable, and handleRefresh + boxDataIsRefreshing + the refetch handle in Boxes.tsx. - Profile menu Organization section: keep just the org name (-> Settings) and drop the standalone 'Copy organization ID' item (still available via the command palette). Verified in Chrome: list toolbar shows only Search + Create Box; profile menu Organization shows only the org name.
…pler menus - Start/Stop/Recover: use the lighter 'secondary' button variant instead of the solid-black 'default' (still prominent vs the outline secondary buttons, but not heavy black). Applies to the box-table row (desktop + mobile) and detail header. - Typography consistency: drop the ad-hoc monospace from Box ID (header + detail) and Image so box detail reads in one consistent font. - Detail metadata grid: normal-case muted labels (no uppercase/letter-spacing), tighter row rhythm, baseline-aligned label/value pairs. - Profile menu: theme toggle on top (no 'Appearance' label); Organization is a single item (-> Settings) parallel to Docs/Discord (no section label, no org name); dropped the standalone org-name display. Verified in Chrome (light + dark): detail metadata uniform + compact; Start/Stop render as soft buttons; profile menu shows Organization/Docs/Discord parallel.
…boxes) Add BoxSearchCommands: while the command palette (nav search / ⌘K) is open, the typed query is debounced and run against listBoxesPaginated(idOrName); matching boxes are registered as a 'Boxes' result group, each selecting to the box detail. Mounted in Sidebar (inside the command-palette + api providers). This makes the global search find boxes by name/ID (industry-standard command palette entity search), complementing the in-list box filter. Verified in Chrome: typing a box name/ID in ⌘K shows it under 'Boxes' and navigates to the detail on select.
Billing is no longer a top-nav tab; it sits in the profile dropdown alongside Organization / Docs / Discord (-> /dashboard/billing). The top nav now carries just Boxes (plus Admin when permitted), keeping the bar minimal. Verified in Chrome: nav shows only Boxes; profile menu lists Organization, Billing, Docs, Discord.
…edup id, terminal start, status pill, metadata grid Top-designer walkthrough of the box detail + list. - Remove the unsupported Screen Recordings feature end-to-end (BoxHeader and BoxTableActions menus + the handler/prop chain through Boxes, BoxTable, columns, useBoxTable, types, BoxDetails; the port-33333 preview caller). - Dedup the Box ID: the header now shows just the name + one copy; the Box ID with its copy lives only in the metadata grid (was duplicated in both). - Terminal 'Box is not running' empty state now offers an inline 'Start box' button (gated by write permission) so users don't have to find the top-right Start (BoxTerminalTab uses useStartBoxMutation directly). - Status is now a colored pill (green running / muted stopped / red error / amber recoverable) via a new BoxState 'pill' variant — far more prominent. - Metadata grid redesigned into stacked label-over-value cells: Image spans the full row, the rest in a 3-col grid — left-aligned, no ragged right edge, consistent rhythm. Verified in Chrome (light+dark): screen recordings gone; single id copy; status pill; stopped box shows inline Start box in the terminal; metadata grid aligned.
…se, loading state - BoxTable row: the whole row is clickable again. The cell-level click handler only swallows the checkbox cell now; the actions cell lets clicks in the empty space around the (already self-stopping) buttons fall through to open the row. - BoxHeader: move the status pill next to the box name (left) instead of the crowded top-right action cluster, grouping identity + status. - BoxInfoPanel: uppercase the Region value (e.g. 'us' -> 'US'). - LoadingFallback: drop the duplicate centered spinner that overlapped the skeleton table, align width to the new 1040 content column, and show the 'taking longer' note (no competing spinner) only after 5s. One clean skeleton. Verified in Chrome: clicking near the row actions opens detail; status pill sits by the name; Region shows 'US'; reload shows a single clean skeleton.
…parse
Anchor the top of the box list with a title + one-line description ('Boxes' /
'Run code in isolated, on-demand environments.') and move Create Box up next to
it (title left, primary action right). Gives the page top matter — like a
breadcrumb/context region — instead of a thin table floating on a tall empty
viewport. The toolbar row is now just search.
…loading rows - Give the box table an iOS-like floating feel: weaker hairline border (border/40-50), softer/larger drop shadow, rounded-xl. Row + header dividers weakened to border/50-60 so separation reads from elevation, not heavy lines. Applied to both the desktop table card and the compact card. - Replace the in-table 'Loading...' text (which looked off in the framed table) with skeleton rows (5 desktop / 4 compact) so the loading state fills the card like real rows forming.
…mand palette) The in-list 'Search by name or Box ID' field is removed — box lookup is now served by the global command palette (⌘K), which searches boxes by name/ID. With the field and the (already-relocated) Create Box action gone, BoxTableHeader had nothing left to render, so it and its now-unused headerAction prop are deleted.
…low menu, surface SSH The row's '...' menu repeated actions that already have inline buttons. Now: - Remove Start/Stop/Recover and (desktop) Terminal from the overflow menu — they already exist as the inline primary button + terminal button. - Surface SSH as its own inline button (KeyRound) on desktop when the box is ssh-accessible, instead of burying 'Create SSH Access' in the menu. - Overflow menu is now just: View Details, Revoke SSH Access, Delete (plus, on compact/mobile where there are no inline terminal/ssh buttons, Terminal + Create SSH so mobile keeps access). Inline (desktop): primary state toggle · Terminal · SSH (when accessible) · more.
…der corners Establish a cohesive 'floating surface' visual system, centralized for easy rollback: - index.css: bump --radius 0.375rem -> 0.625rem (rounder cards/buttons/inputs proportionally) and add theme-aware --shadow-card / --shadow-card-hover (soft layered light shadow; deeper dark shadow so cards still lift off the bg). - tailwind.config: register shadow-card / shadow-card-hover utilities from those vars. - Card component: rounded-xl + hairline border (border/60) + shadow-card — this alone restyles the ~14 pages that use the shared Card (Billing, Settings, Wallet, Spending, Webhooks, Admin, …). - Apply the same shadow-card token to the surfaces that use ad-hoc containers: box table (replacing the earlier inline shadow), BoxInfoPanel, BoxContentTabs, and the API keys table. Verified in Chrome (dark): box list table floats with rounded-xl + soft shadow; buttons rounder but not pill-y. Other Card pages inherit via the central change.
Remove the border-b on the BoxHeader so there's no horizontal rule between the title row and the metadata/terminal content below it.
…ens a dialog anywhere (no redirect)
- Profile menu: give the theme toggle top padding (px-2 pb-2 -> px-2 py-2) so it
no longer hugs the dropdown's top edge.
- Theme switch flicker: drop the runWithoutAnimation hack that injected a global
'* { transition:none !important }' style and forced two synchronous reflows on
every element (which flashed the open dropdown). Just toggle the html class.
- Guide no longer jumps to the homepage. Add a single app-global OnboardingDialogHost
(rendered in Dashboard) that listens for ONBOARDING_OPEN_EVENT and opens the
guide as a dialog from any page; it preventDefaults the event so the Sidebar's
navigate(/dashboard/boxes?onboarding=1) fallback never fires. Removed the
per-page ONBOARDING_OPEN_EVENT listeners from Boxes/BoxDetails so the Guide
button no longer triggers their copies (first-visit auto-open + ?onboarding=1
stay page-local).
…vanced accordion) Surface CPU/Memory/Disk directly in the Create Box sheet instead of hiding them behind an 'Advanced options' accordion, so the sheet reads less empty. Image stays a dropdown; no live-summary card.
… cells The all-rounded floating restyle left hard hairlines and edge-hugging content that clashed with it: - top nav border-b: border-border -> border-border/50 - table header border-b: border-border/60 -> border-border/40 - first/last table cells: add pl-5 / pr-5 so content clears the card's rounded corners instead of sitting flush against them
… login) Previously the MSW mock only covered billing and even fetched /config from the real dev API, so the dashboard could not render its core surfaces offline. Make the local mock chain stand on its own: - fixtures.ts: typed Organization, member (owner), config and box fixtures - MockAuthProvider: swaps react-oidc-context's AuthProvider for a fixed authenticated session (no OIDC server, no login round-trip) - handlers.ts: serve a static config plus organizations, org members, paginated boxes, regions, and a 403 admin probe - ConfigProvider: use MockAuthProvider when VITE_ENABLE_MOCKING is set
…heet Few fields no longer warrant a full-height drawer; mirror the API-key dialog — centered modal, resources laid out in a single CPU/Memory/Disk row.
Replace the plain textarea + icon button with the tinted key card and a prominent labelled Copy button that confirms in place.
…ndlers Lets the box detail page and the onboarding key flow render offline.
… 16px Self-host Space Grotesk (SIL OFL) and apply it to titles and labels (cards, dialogs, sheets, the Label primitive, box name, detail meta labels — now bolder); body stays Inter and code stays monospace. Raise the root font-size from 14px to 16px so the whole UI scales up via rem.
… panel Drop the inner scroll region so the whole detail page scrolls. With observability tabs off, render a plain Terminal header instead of a single-tab selector, and remove the terminal's inner rounded frame so it fills the card edge to edge under the header.
📝 WalkthroughWalkthroughThis PR overhauls the Boxlite dashboard across design system, box table, box detail page, onboarding, dev tooling, and mock infrastructure. It replaces ChangesDashboard UI Overhaul
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8e9717144
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { data } = useQuery({ | ||
| queryKey: ['command-palette-boxes', selectedOrganization?.id, debounced], | ||
| queryFn: async () => | ||
| (await boxApi.listBoxesPaginated(selectedOrganization!.id, 1, 6, undefined, debounced)).data, |
There was a problem hiding this comment.
Preserve Box ID search in the palette
When users type a Box ID into the new command-palette search, this call sends the query as the generated client's name argument (listBoxesPaginated(xOrg, page, limit, id, name, ...)), leaving the id argument undefined. Since the table search was removed in this change, ID searches now return nothing unless a box happens to have the same name; pass the ID filter or add the same getBox fallback used by the boxes list for exact ID searches.
Useful? React with 👍 / 👎.
| {writePermitted && ( | ||
| <Button onClick={handleStart} disabled={startMutation.isPending}> |
There was a problem hiding this comment.
Gate terminal start action to startable boxes
This button is rendered for every non-running state because the branch is only !running, so boxes in STARTING, STOPPING, DESTROYED, or recoverable ERROR states get a "Start box" action that calls the start mutation even though only STOPPED boxes are startable. In those states users can now trigger invalid lifecycle requests from the terminal empty state; render it only for isStartable(box) or show the appropriate wait/recover state instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
apps/dashboard/src/lib/onboarding-code-examples.ts (1)
27-27: ⚡ Quick winDeduplicate the onboarding image tag into a single constant.
The same image reference appears in four snippets; centralizing it will prevent accidental drift on the next image bump.
Proposed refactor
+const ONBOARDING_IMAGE = 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3' + const codeExamples: Record<OnboardingLanguage, OnboardingCodeExample> = { typescript: { @@ -const box = await rt.create({ image: 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3' }, 'sdk-quickstart') +const box = await rt.create({ image: '${ONBOARDING_IMAGE}' }, 'sdk-quickstart') @@ - box = await rt.create(BoxOptions(image="ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3"), name="sdk-quickstart") + box = await rt.create(BoxOptions(image="${ONBOARDING_IMAGE}"), name="sdk-quickstart") @@ - box, err := rt.Create(ctx, "ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3", boxlite.WithName("sdk-quickstart")) + box, err := rt.Create(ctx, "${ONBOARDING_IMAGE}", boxlite.WithName("sdk-quickstart")) @@ - rootfs: RootfsSpec::Image("ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3".into()), + rootfs: RootfsSpec::Image("${ONBOARDING_IMAGE}".into()),Also applies to: 57-57, 108-108, 146-146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/lib/onboarding-code-examples.ts` at line 27, The image tag 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3' is hardcoded in four different locations within the file (at lines 27, 57, 108, and 146), which creates maintenance risk if the image needs to be updated. Extract this image reference into a single constant defined at the top of the file (outside any functions), then replace all four occurrences of the hardcoded image string with references to this new constant. This ensures a single source of truth for the image tag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/CLAUDE.md`:
- Around line 22-23: In the CLAUDE.md file, update the documented Dex test
account username from `admin@boxlite.dev` to `adminboxlite.dev` (removing the @
symbol) to match the actual local Dex test account credentials. This ensures
that the documentation accurately reflects the correct username required for
local testing to prevent login failures during E2E testing.
In `@apps/dashboard/src/components/boxes/BoxTerminalTab.tsx`:
- Around line 31-38: The handleStart function and its associated UI button need
to be gated with the isStartable(box) check, as relying on !running alone is
insufficient to prevent invalid start requests during transient states. Add an
isStartable(box) guard at the beginning of the handleStart function to prevent
the mutation from being called when the box is not in a startable state, and
also ensure the Start button in the UI is disabled when !isStartable(box) is
true. Apply the same pattern to other similar mutation handlers mentioned in the
related ranges.
In `@apps/dashboard/src/components/Sidebar.tsx`:
- Around line 152-156: In the copyOrgId function, chain .then().catch() to the
copyToClipboard() call instead of immediately showing the success toast. Move
the toast.success() call inside the .then() block so it only displays when the
clipboard operation succeeds, and add a .catch() block to handle clipboard
failures (such as displaying an error toast). This maintains the () => void
return type signature required by CommandConfig.onSelect while ensuring users
only see success messages when the clipboard operation actually succeeds.
In `@apps/dashboard/src/mocks/fixtures.ts`:
- Around line 97-114: The `createdAt` and `updatedAt` timestamp fields in the
mock fixtures are assigned `Date` objects through the `now` variable, but the
API contract expects these fields to be ISO 8601 strings. Convert the timestamp
fields to ISO 8601 string format in all three builder functions: `buildBox()`,
`buildUser()`, and `buildOrganization()`. Replace each instance of `createdAt:
now` and `updatedAt: now` with their ISO 8601 string representations to ensure
type safety and compliance with the API contract.
In `@apps/dashboard/src/mocks/handlers.ts`:
- Around line 29-32: Remove the fallback operator (`?? MOCK_BOXES[0]`) from the
box lookup in the http.get handler for the `/box/:boxIdOrName` endpoint. The
current implementation masks not-found behavior by always returning the first
mock box when an unknown ID is requested. Instead, allow the `find()` method to
return undefined when no matching box is found, so the handler can properly
return a 404 response via the existing conditional that checks if box exists.
In `@apps/dashboard/src/providers/RegionsProvider.tsx`:
- Around line 33-36: The RegionsProvider is silently collapsing fetch failures
into empty arrays (in the error handlers around setSharedRegions([]) at both the
initial fetch failure and retry attempt locations), making it impossible for
consumers to distinguish between "no regions exist" and "the API failed". Add
error state tracking to the provider (such as an error or errorState variable)
and update the error handlers to set this error state instead of just setting an
empty array, then expose this error state through the provider's context so
consumers can properly handle and display fetch failures separately from empty
results.
---
Nitpick comments:
In `@apps/dashboard/src/lib/onboarding-code-examples.ts`:
- Line 27: The image tag 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3'
is hardcoded in four different locations within the file (at lines 27, 57, 108,
and 146), which creates maintenance risk if the image needs to be updated.
Extract this image reference into a single constant defined at the top of the
file (outside any functions), then replace all four occurrences of the hardcoded
image string with references to this new constant. This ensures a single source
of truth for the image tag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ba48bc43-eb31-45b7-a175-4a1739f5f6df
⛔ Files ignored due to path filters (1)
apps/dashboard/src/assets/fonts/space-grotesk-latin-variable.woff2is excluded by!**/*.woff2
📒 Files selected for processing (46)
apps/CLAUDE.mdapps/dashboard/src/assets/fonts/Space-Grotesk-OFL.txtapps/dashboard/src/components/ApiKeyTable.tsxapps/dashboard/src/components/Box/CreateBoxDialog.tsxapps/dashboard/src/components/Box/CreateBoxSheet.tsxapps/dashboard/src/components/BoxSearchCommands.tsxapps/dashboard/src/components/BoxTable/BoxState.tsxapps/dashboard/src/components/BoxTable/BoxTableActions.tsxapps/dashboard/src/components/BoxTable/BoxTableHeader.tsxapps/dashboard/src/components/BoxTable/columns.tsxapps/dashboard/src/components/BoxTable/index.tsxapps/dashboard/src/components/BoxTable/types.tsapps/dashboard/src/components/BoxTable/useBoxTable.tsapps/dashboard/src/components/LoadingFallback.tsxapps/dashboard/src/components/OnboardingDialogHost.tsxapps/dashboard/src/components/OnboardingGuideDialog.tsxapps/dashboard/src/components/Organizations/OrganizationPicker.tsxapps/dashboard/src/components/PageLayout.tsxapps/dashboard/src/components/Sidebar.tsxapps/dashboard/src/components/boxes/BoxContentTabs.tsxapps/dashboard/src/components/boxes/BoxDetails.tsxapps/dashboard/src/components/boxes/BoxHeader.tsxapps/dashboard/src/components/boxes/BoxInfoPanel.tsxapps/dashboard/src/components/boxes/BoxTerminalTab.tsxapps/dashboard/src/components/boxes/index.tsapps/dashboard/src/components/ui/card.tsxapps/dashboard/src/components/ui/dialog.tsxapps/dashboard/src/components/ui/empty.tsxapps/dashboard/src/components/ui/label.tsxapps/dashboard/src/components/ui/sheet.tsxapps/dashboard/src/contexts/ThemeContext.tsxapps/dashboard/src/enums/LocalStorageKey.tsapps/dashboard/src/hooks/use-mobile.tsxapps/dashboard/src/index.cssapps/dashboard/src/lib/onboarding-code-examples.tsapps/dashboard/src/mocks/MockAuthProvider.tsxapps/dashboard/src/mocks/fixtures.tsapps/dashboard/src/mocks/handlers.tsapps/dashboard/src/pages/Boxes.tsxapps/dashboard/src/pages/Dashboard.tsxapps/dashboard/src/providers/ConfigProvider.tsxapps/dashboard/src/providers/RegionsProvider.tsxapps/dashboard/tailwind.config.jsapps/dashboard/vite.config.mtsapps/package.jsonapps/scripts/start-dashboard.mjs
💤 Files with no reviewable changes (3)
- apps/dashboard/src/components/Organizations/OrganizationPicker.tsx
- apps/dashboard/src/components/Box/CreateBoxSheet.tsx
- apps/dashboard/src/components/BoxTable/BoxTableHeader.tsx
| - The local Dex test account is `admin@boxlite.dev` / `password`. Browser E2E should | ||
| log in through Dex when redirected and should not depend on cached cookies. |
There was a problem hiding this comment.
Fix the documented Dex username to match the known local test account.
The credential string here differs from the expected local Dex test username and can cause failed logins during local testing.
Based on learnings: Use the local Dex test account credentials adminboxlite.dev / password for testing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/CLAUDE.md` around lines 22 - 23, In the CLAUDE.md file, update the
documented Dex test account username from `admin@boxlite.dev` to
`adminboxlite.dev` (removing the @ symbol) to match the actual local Dex test
account credentials. This ensures that the documentation accurately reflects the
correct username required for local testing to prevent login failures during E2E
testing.
Source: Learnings
| const handleStart = async () => { | ||
| try { | ||
| await startMutation.mutateAsync({ boxId: box.id, detailRef: getBoxRouteId(box) }) | ||
| toast.success('Box started') | ||
| } catch (error) { | ||
| handleApiError(error, 'Failed to start box') | ||
| } | ||
| } |
There was a problem hiding this comment.
Gate the Start action with isStartable(box) before firing mutation.
!running is broader than “startable”, so this branch can expose Start box during transient/non-startable states and trigger invalid start requests. Add isStartable(box) in UI gating and in handleStart as a hard guard.
Suggested fix
-import { isStoppable } from '`@/lib/utils/box`'
+import { isStartable, isStoppable } from '`@/lib/utils/box`'
const handleStart = async () => {
+ if (!isStartable(box)) return
try {
await startMutation.mutateAsync({ boxId: box.id, detailRef: getBoxRouteId(box) })
toast.success('Box started')
} catch (error) {
handleApiError(error, 'Failed to start box')
}
}
- {writePermitted && (
+ {writePermitted && isStartable(box) && (
<Button onClick={handleStart} disabled={startMutation.isPending}>
{startMutation.isPending ? <Spinner className="size-4" /> : <Play className="size-4" />}
Start box
</Button>
)}Also applies to: 49-72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/components/boxes/BoxTerminalTab.tsx` around lines 31 - 38,
The handleStart function and its associated UI button need to be gated with the
isStartable(box) check, as relying on !running alone is insufficient to prevent
invalid start requests during transient states. Add an isStartable(box) guard at
the beginning of the handleStart function to prevent the mutation from being
called when the box is not in a startable state, and also ensure the Start
button in the UI is disabled when !isStartable(box) is true. Apply the same
pattern to other similar mutation handlers mentioned in the related ranges.
| const copyOrgId = useCallback(() => { | ||
| if (!selectedOrganization) return | ||
| copyToClipboard(selectedOrganization.id) | ||
| toast.success('Organization ID copied to clipboard') | ||
| }, [copyToClipboard, selectedOrganization]) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In usehooks-ts v3.1.1, what are the return type and failure semantics of the function returned by useCopyToClipboard?
💡 Result:
In usehooks-ts v3.1.1, the useCopyToClipboard hook returns a tuple containing the copied text (or null) and a function to copy text to the clipboard, typed as [CopiedValue, CopyFn] [1]. The return type of the copy function (CopyFn) is a Promise [1]. Regarding failure semantics, the copy function does not throw errors [1][2]. Instead, it handles potential failures—such as the clipboard API being unsupported or an exception occurring during the write operation—by logging a warning to the console and returning false [1]. Specifically: 1. If navigator.clipboard is unavailable, it logs 'Clipboard not supported' and returns false [1]. 2. If the clipboard operation fails (e.g., due to a rejected promise or browser security restrictions), it catches the error, logs 'Copy failed' with the error object, sets the copied text state to null, and returns false [1]. Success is indicated by a return value of true [1].
Citations:
- 1: https://usehooks-ts.com/react-hook/use-copy-to-clipboard
- 2: docs: improve
useCopyToClipboarddemo juliencrn/usehooks-ts#560
🏁 Script executed:
fd -e tsx -e ts "Sidebar" apps/dashboard/src/components/ | head -5Repository: boxlite-ai/boxlite
Length of output: 104
🏁 Script executed:
wc -l apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 108
🏁 Script executed:
sed -n '145,170p' apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 1260
🏁 Script executed:
sed -n '160,165p' apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 430
🏁 Script executed:
rg -n "copyToClipboard|toast\.(success|error)" apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 277
🏁 Script executed:
rg -B 2 -A 2 "onSelect.*copyOrgId|copyOrgId.*onSelect" apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 274
🏁 Script executed:
rg -n "onSelect" apps/dashboard/src/ -A 1 | head -20Repository: boxlite-ai/boxlite
Length of output: 1249
🏁 Script executed:
rg -n "CommandConfig|onSelect" apps/dashboard/src/vendor/pylon/ | head -30Repository: boxlite-ai/boxlite
Length of output: 144
🏁 Script executed:
find apps/dashboard/src -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -l "CommandConfig" {} \;Repository: boxlite-ai/boxlite
Length of output: 417
🏁 Script executed:
rg -B 5 -A 10 "type CommandConfig|interface CommandConfig" apps/dashboard/src/Repository: boxlite-ai/boxlite
Length of output: 4240
🏁 Script executed:
rg -n "copyToClipboard\(" apps/dashboard/src/components/Sidebar.tsxRepository: boxlite-ai/boxlite
Length of output: 111
Handle clipboard failures before showing a success toast.
toast.success is always shown even if clipboard write fails, which can mislead users. However, making the callback async will violate the CommandConfig.onSelect type signature which expects () => void. Use .then().catch() instead to handle the Promise<boolean> result while maintaining type safety:
Proposed fix
const copyOrgId = useCallback(() => {
if (!selectedOrganization) return
- copyToClipboard(selectedOrganization.id)
- toast.success('Organization ID copied to clipboard')
+ copyToClipboard(selectedOrganization.id).then(ok => {
+ if (ok) {
+ toast.success('Organization ID copied to clipboard')
+ } else {
+ toast.error('Failed to copy Organization ID')
+ }
+ }).catch(() => {
+ toast.error('Failed to copy Organization ID')
+ })
}, [copyToClipboard, selectedOrganization])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const copyOrgId = useCallback(() => { | |
| if (!selectedOrganization) return | |
| copyToClipboard(selectedOrganization.id) | |
| toast.success('Organization ID copied to clipboard') | |
| }, [copyToClipboard, selectedOrganization]) | |
| const copyOrgId = useCallback(() => { | |
| if (!selectedOrganization) return | |
| copyToClipboard(selectedOrganization.id).then(ok => { | |
| if (ok) { | |
| toast.success('Organization ID copied to clipboard') | |
| } else { | |
| toast.error('Failed to copy Organization ID') | |
| } | |
| }).catch(() => { | |
| toast.error('Failed to copy Organization ID') | |
| }) | |
| }, [copyToClipboard, selectedOrganization]) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/components/Sidebar.tsx` around lines 152 - 156, In the
copyOrgId function, chain .then().catch() to the copyToClipboard() call instead
of immediately showing the success toast. Move the toast.success() call inside
the .then() block so it only displays when the clipboard operation succeeds, and
add a .catch() block to handle clipboard failures (such as displaying an error
toast). This maintains the () => void return type signature required by
CommandConfig.onSelect while ensuring users only see success messages when the
clipboard operation actually succeeds.
| function buildBox(overrides: Partial<Box> & Pick<Box, 'id' | 'name' | 'state'>): Box { | ||
| return { | ||
| organizationId: MOCK_ORGANIZATION_ID, | ||
| user: MOCK_USER.email, | ||
| env: {}, | ||
| labels: {}, | ||
| public: false, | ||
| networkBlockAll: false, | ||
| target: 'mock', | ||
| image: 'ghcr.io/boxlite-ai/boxlite-agent-base:mock', | ||
| cpu: 1, | ||
| gpu: 0, | ||
| memory: 1, | ||
| disk: 10, | ||
| desiredState: BoxDesiredState.STARTED, | ||
| createdAt: now, | ||
| updatedAt: now, | ||
| class: BoxClassEnum.SMALL, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify Box contract fields in generated API client:
fd -a 'box.ts' apps/libs/api-client/src/models --exec sed -n '1,220p' {}
# Compare fixture assignments:
rg -n 'createdAt|updatedAt' apps/dashboard/src/mocks/fixtures.tsRepository: boxlite-ai/boxlite
Length of output: 4205
Replace Date objects with ISO 8601 strings for timestamp fields in mock fixtures.
Box.createdAt and Box.updatedAt are typed as strings in the generated API contract, but fixtures assign Date objects. This violates the API contract and breaks type safety for consumers. The issue also exists in buildUser() (lines 63-64) and buildOrganization() (lines 93-94).
Suggested fix
-const now = new Date()
+const now = new Date()
+const nowIso = now.toISOString()
@@
function buildBox(overrides: Partial<Box> & Pick<Box, 'id' | 'name' | 'state'>): Box {
return {
@@
- createdAt: now,
- updatedAt: now,
+ createdAt: nowIso,
+ updatedAt: nowIso,
@@
}
}Apply the same fix to buildUser() and buildOrganization().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/mocks/fixtures.ts` around lines 97 - 114, The `createdAt`
and `updatedAt` timestamp fields in the mock fixtures are assigned `Date`
objects through the `now` variable, but the API contract expects these fields to
be ISO 8601 strings. Convert the timestamp fields to ISO 8601 string format in
all three builder functions: `buildBox()`, `buildUser()`, and
`buildOrganization()`. Replace each instance of `createdAt: now` and `updatedAt:
now` with their ISO 8601 string representations to ensure type safety and
compliance with the API contract.
| http.get(`${API_URL}/box/:boxIdOrName`, ({ params }) => { | ||
| const box = MOCK_BOXES.find((b) => b.id === params.boxIdOrName) ?? MOCK_BOXES[0] | ||
| return box ? HttpResponse.json(box) : new HttpResponse(null, { status: 404 }) | ||
| }), |
There was a problem hiding this comment.
Return 404 for unknown box IDs instead of substituting the first fixture.
The ?? MOCK_BOXES[0] fallback makes unknown IDs look valid, so not-found behavior is never exercised.
💡 Suggested fix
- http.get(`${API_URL}/box/:boxIdOrName`, ({ params }) => {
- const box = MOCK_BOXES.find((b) => b.id === params.boxIdOrName) ?? MOCK_BOXES[0]
- return box ? HttpResponse.json(box) : new HttpResponse(null, { status: 404 })
- }),
+ http.get(`${API_URL}/box/:boxIdOrName`, ({ params }) => {
+ const box = MOCK_BOXES.find((b) => b.id === params.boxIdOrName)
+ return box ? HttpResponse.json(box) : new HttpResponse(null, { status: 404 })
+ }),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| http.get(`${API_URL}/box/:boxIdOrName`, ({ params }) => { | |
| const box = MOCK_BOXES.find((b) => b.id === params.boxIdOrName) ?? MOCK_BOXES[0] | |
| return box ? HttpResponse.json(box) : new HttpResponse(null, { status: 404 }) | |
| }), | |
| http.get(`${API_URL}/box/:boxIdOrName`, ({ params }) => { | |
| const box = MOCK_BOXES.find((b) => b.id === params.boxIdOrName) | |
| return box ? HttpResponse.json(box) : new HttpResponse(null, { status: 404 }) | |
| }), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/mocks/handlers.ts` around lines 29 - 32, Remove the
fallback operator (`?? MOCK_BOXES[0]`) from the box lookup in the http.get
handler for the `/box/:boxIdOrName` endpoint. The current implementation masks
not-found behavior by always returning the first mock box when an unknown ID is
requested. Instead, allow the `find()` method to return undefined when no
matching box is found, so the handler can properly return a 404 response via the
existing conditional that checks if box exists.
| // Optimistic prefetch at app start — fail silently like the admin probe. | ||
| // Pages that actually need regions surface their own empty/error state. | ||
| console.warn('Failed to fetch shared regions', error) | ||
| setSharedRegions([]) |
There was a problem hiding this comment.
Expose fetch-failure state instead of collapsing errors into empty lists.
At Line 33 and Line 53, failures are converted to [] with no error signal, so consumers cannot distinguish “no regions exist” from “regions API failed.” That hides outages and blocks proper retry/error UX.
Suggested direction
type IRegionsContext = {
sharedRegions: Region[]
loadingSharedRegions: boolean
+ sharedRegionsError: Error | null
availableRegions: Region[]
loadingAvailableRegions: boolean
+ availableRegionsError: Error | null
...
}
+const [sharedRegionsError, setSharedRegionsError] = useState<Error | null>(null)
+const [availableRegionsError, setAvailableRegionsError] = useState<Error | null>(null)
try {
+ setSharedRegionsError(null)
const regions = (await regionsApi.listSharedRegions()).data
setSharedRegions(regions)
} catch (error) {
console.warn('Failed to fetch shared regions', error)
setSharedRegions([])
+ setSharedRegionsError(error as Error)
}
try {
+ setAvailableRegionsError(null)
const regions = (await organizationsApi.listAvailableRegions(selectedOrganization.id)).data
setAvailableRegions(regions)
} catch (error) {
console.warn('Failed to fetch available regions', error)
setAvailableRegions([])
+ setAvailableRegionsError(error as Error)
}Also applies to: 53-57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/providers/RegionsProvider.tsx` around lines 33 - 36, The
RegionsProvider is silently collapsing fetch failures into empty arrays (in the
error handlers around setSharedRegions([]) at both the initial fetch failure and
retry attempt locations), making it impossible for consumers to distinguish
between "no regions exist" and "the API failed". Add error state tracking to the
provider (such as an error or errorState variable) and update the error handlers
to set this error state instead of just setting an empty array, then expose this
error state through the provider's context so consumers can properly handle and
display fetch failures separately from empty results.
…d section Now that Create Box is a centered dialog, the CPU/Memory/Disk fields tuck into an Advanced accordion (collapsed by default), stacked vertically.
… empty Drop the inline SSH button (its key icon clashed with API Keys and only showed on some rows); SSH now lives in the overflow menu on every layout, so rows are uniform. Skip the column header when the list is empty so the empty state isn't stranded under a half-width header.
…pulse Light tokens now form a clear ladder: a recessed page background with a brighter shared surface for cards and the top nav (nav switches from the background tone to the card tone) so chrome reads as one elevated layer. Also remove the onboarding Guide entry's pulse animation (and its dead keyframe / state / listener) so it's a plain button.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/components/BoxTable/BoxTableActions.tsx (1)
96-109:⚠️ Potential issue | 🟠 Major | ⚡ Quick winApply
isSshAccessiblegating consistently to SSH revoke action.Line 96 gates Create SSH Access, but Line 104-109 always shows Revoke SSH Access. That diverges from the stated Create/Revoke SSH gating behavior and exposes a revoke flow for non-SSH-accessible box states.
💡 Suggested fix
- if (isSshAccessible(box)) { + if (isSshAccessible(box)) { items.push({ key: 'create-ssh', label: 'Create SSH Access', onClick: () => onCreateSshAccess(box.id), disabled: isLoading, }) - } - items.push({ - key: 'revoke-ssh', - label: 'Revoke SSH Access', - onClick: () => onRevokeSshAccess(box.id), - disabled: isLoading, - }) + items.push({ + key: 'revoke-ssh', + label: 'Revoke SSH Access', + onClick: () => onRevokeSshAccess(box.id), + disabled: isLoading, + }) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/BoxTable/BoxTableActions.tsx` around lines 96 - 109, The Create SSH Access action is conditionally shown only when isSshAccessible(box) is true, but the Revoke SSH Access action is always added to the items array regardless of the isSshAccessible check. Apply the same isSshAccessible gating to the Revoke SSH Access action by moving its items.push call inside the existing isSshAccessible(box) conditional block, so both SSH actions are only displayed when SSH is actually accessible for the box.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apps/dashboard/src/components/BoxTable/BoxTableActions.tsx`:
- Around line 96-109: The Create SSH Access action is conditionally shown only
when isSshAccessible(box) is true, but the Revoke SSH Access action is always
added to the items array regardless of the isSshAccessible check. Apply the same
isSshAccessible gating to the Revoke SSH Access action by moving its items.push
call inside the existing isSshAccessible(box) conditional block, so both SSH
actions are only displayed when SSH is actually accessible for the box.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 803316db-898a-468a-b0bc-240c0dd9b75e
📒 Files selected for processing (5)
apps/dashboard/src/components/Box/CreateBoxDialog.tsxapps/dashboard/src/components/BoxTable/BoxTableActions.tsxapps/dashboard/src/components/BoxTable/index.tsxapps/dashboard/src/components/Sidebar.tsxapps/dashboard/src/index.css
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/dashboard/src/components/Sidebar.tsx
- apps/dashboard/src/components/Box/CreateBoxDialog.tsx
- apps/dashboard/src/components/BoxTable/index.tsx
What
A calm, consistent restyle of the dashboard plus a self-contained local
mock so this kind of UI work no longer needs a reachable backend or login.
Restyle
centralized elevation tokens, rounder corners, shadow-over-border,
softened nav/table-header rules, content inset from the rounded edges.
rows, status pill, region column dropped, full image refs in onboarding,
search input removed (command palette now finds boxes), page title dropped.
inline Start in the terminal empty state, removed Screen Recordings /
Auto-stop / Auto-delete / SDK nudge. Now scrolls as one page (no inner
scroll region). With observability tabs off it shows a plain Terminal
header instead of a one-tab selector, and the terminal fills the card
(inner frame removed). Meta labels are bolder.
CPU/Memory/Disk row.
page; theme toggle padding/flicker fixes.
labels; body stays Inter, code stays monospace; base size 14 -> 16px.
Self-contained mock (
npm run start:mock)Previously only billing was mocked and
/configwas fetched from real dev.Now
fixtures.ts+MockAuthProvider+ handlers serve config, organizations,members, boxes (list + by id), regions, api keys, and a fake authenticated
session — the dashboard renders with no backend and no login.
Test plan
npm run start:mock-> list + detail render with fixtures, no console errorsBox dialog in-app
Summary by CodeRabbit
Release Notes
/apiproxy target selection via environment configuration.