Skip to content

feat(dashboard): dashboard restyle + self-contained mock target#820

Open
law-chain-hot wants to merge 37 commits into
mainfrom
feat/dashboard-floating-restyle
Open

feat(dashboard): dashboard restyle + self-contained mock target#820
law-chain-hot wants to merge 37 commits into
mainfrom
feat/dashboard-floating-restyle

Conversation

@law-chain-hot

@law-chain-hot law-chain-hot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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

  • Floating cards across the box list, detail panels, and tabs:
    centralized elevation tokens, rounder corners, shadow-over-border,
    softened nav/table-header rules, content inset from the rounded edges.
  • Box list: minimal default, tighter density, wider viewport, skeleton
    rows, status pill, region column dropped, full image refs in onboarding,
    search input removed (command palette now finds boxes), page title dropped.
  • Box detail: centered single column, compressed header, deduped Box ID,
    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.
  • Create Box: now a centered dialog (was a right sheet) with a single
    CPU/Memory/Disk row.
  • Shell: org + billing in the profile menu; Guide opens a dialog from any
    page; theme toggle padding/flicker fixes.
  • Typography: self-hosted Space Grotesk (SIL OFL) for titles and
    labels; body stays Inter, code stays monospace; base size 14 -> 16px.
  • Region prefetch failures fail silently instead of toasting every load.

Self-contained mock (npm run start:mock)

Previously only billing was mocked and /config was 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 errors
  • Verified floating cards, fonts, whole-page scroll, and the centered Create
    Box dialog in-app

Summary by CodeRabbit

Release Notes

  • New Features
    • Added dialog-based box creation with validation and navigation to the box terminal.
    • Added box search to the command palette.
    • Introduced an app-wide onboarding guide with progress tracking.
  • Bug Fixes
    • Improved dashboard /api proxy target selection via environment configuration.
  • Refactor
    • Removed screen-recordings actions and streamlined box details/tabs (including simplified overview behavior).
    • Updated box state display options, SSH-related actions, and table column behavior/defaults.
  • Style
    • Refreshed typography and card/shadow styling; improved loading skeleton layouts.
  • Documentation
    • Updated dashboard start instructions for running commands from the workspace root.

… 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
…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.
@law-chain-hot law-chain-hot requested a review from a team as a code owner June 17, 2026 03:53
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR overhauls the Boxlite dashboard across design system, box table, box detail page, onboarding, dev tooling, and mock infrastructure. It replaces CreateBoxSheet with CreateBoxDialog, removes region/screen-recordings from the box table while adding SSH actions, refactors BoxDetails to a single-column layout, introduces Space Grotesk font with card shadow tokens, lifts onboarding into a global host component, adds typed MSW fixtures, and centralizes API proxy target configuration via DASHBOARD_API_PROXY_TARGET.

Changes

Dashboard UI Overhaul

Layer / File(s) Summary
Design system: Space Grotesk font, card shadows, and base CSS tokens
apps/dashboard/src/assets/fonts/Space-Grotesk-OFL.txt, apps/dashboard/src/index.css, apps/dashboard/tailwind.config.js, apps/dashboard/src/components/ui/card.tsx, apps/dashboard/src/components/ui/dialog.tsx, apps/dashboard/src/components/ui/empty.tsx, apps/dashboard/src/components/ui/label.tsx, apps/dashboard/src/components/ui/sheet.tsx, apps/dashboard/src/components/ApiKeyTable.tsx
Adds Space Grotesk @font-face and OFL license, defines --shadow-card/--shadow-card-hover CSS tokens and a fontFamily.display Tailwind stack, bumps base font-size to 16px, and applies font-display/shadow-card across Card, Dialog, Empty, Sheet, and ApiKeyTable.
BoxState rendering modes and PageSize content option
apps/dashboard/src/components/BoxTable/BoxState.tsx, apps/dashboard/src/components/PageLayout.tsx, apps/dashboard/src/hooks/use-mobile.tsx, apps/dashboard/src/enums/LocalStorageKey.ts
Adds iconOnly and pill props to BoxState with a getStateTint helper, expands PageSize to include 'content' (1040px), lowers COMPACT_BREAKPOINT from 1200 to 1024, and bumps the column-visibility localStorage key to V2.
BoxTable types, columns, and useBoxTable: remove region/screen-recordings, add SSH/recover
apps/dashboard/src/components/BoxTable/types.ts, apps/dashboard/src/components/BoxTable/columns.tsx, apps/dashboard/src/components/BoxTable/useBoxTable.ts
Removes getRegionName, handleScreenRecordings, refresh props, and BoxTableHeaderProps. Adds handleCreateSshAccess, handleRevokeSshAccess, handleRecover. Renders BoxState icon-only in the Name column, drops the Region column, enables hiding for several columns, and updates default column-visibility state.
BoxTableActions and BoxTable index: SSH button, secondary variant, dropdown refactor, skeleton loading
apps/dashboard/src/components/BoxTable/BoxTableActions.tsx, apps/dashboard/src/components/BoxTable/index.tsx
Adds isSshAccessible-gated inline SSH and revoke options, changes primary button variant to secondary, refactors the mobile dropdown to Terminal/SSH only (removes start/stop/recover), removes BoxTableHeader, updates skeleton loading UI to multi-row Skeleton cards, and scopes click-propagation stop to the select column only.
Replace CreateBoxSheet with CreateBoxDialog and update Boxes page
apps/dashboard/src/components/Box/CreateBoxDialog.tsx, apps/dashboard/src/components/Box/CreateBoxSheet.tsx, apps/dashboard/src/pages/Boxes.tsx
Deletes CreateBoxSheet (377 lines). Adds CreateBoxDialog with Zod validation, optional resource fields, and useCreateBoxMutation wiring that navigates to the created box's terminal tab on success. Boxes page renders a permission-gated CreateBoxDialog above BoxTable, removes manual refresh mechanism, region handler, and screen-recordings support.
BoxDetails, BoxHeader, BoxInfoPanel, BoxContentTabs, and BoxTerminalTab: single-column layout with SSH and terminal start
apps/dashboard/src/components/boxes/BoxDetails.tsx, apps/dashboard/src/components/boxes/BoxHeader.tsx, apps/dashboard/src/components/boxes/BoxInfoPanel.tsx, apps/dashboard/src/components/boxes/BoxContentTabs.tsx, apps/dashboard/src/components/boxes/BoxTerminalTab.tsx, apps/dashboard/src/components/boxes/index.ts
Removes resizable desktop overview panel, handleScreenRecordings, and SDK guide nudge. Redirects overview tab to logs or terminal. BoxHeader shows pill BoxState and drops Box ID section. BoxInfoPanel adds getRegionName prop and a MetaCell-based meta-grid showing image, box ID, region, resources, and timestamps. BoxContentTabs adds TAB_SHELL and a no-experiments early-return. BoxTerminalTab adds a permission-gated Start button via useStartBoxMutation. Removes InfoSection/InfoRow re-exports.
Onboarding: global OnboardingDialogHost, UI polish, code example image update
apps/dashboard/src/components/OnboardingDialogHost.tsx, apps/dashboard/src/pages/Dashboard.tsx, apps/dashboard/src/components/OnboardingGuideDialog.tsx, apps/dashboard/src/lib/onboarding-code-examples.ts
Adds OnboardingDialogHost managing progress init from userId, event listeners for ONBOARDING_OPEN_EVENT and ONBOARDING_PROGRESS_EVENT, localStorage skip on close, and delayed highlight dispatch. Mounts it in Dashboard.tsx. Updates API key display to inline code with conditional copy button. Updates all four language examples to the new ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3 base image.
Sidebar: BoxSearchCommands, org command palette, billing relocation
apps/dashboard/src/components/BoxSearchCommands.tsx, apps/dashboard/src/components/Sidebar.tsx, apps/dashboard/src/components/Organizations/OrganizationPicker.tsx
Adds renderless BoxSearchCommands that debounces palette input, fetches paginated box results as commands with keywords for client-side filtering. Sidebar adds org Copy Organization ID command, moves Billing from primary nav to profile dropdown, adds conditional Organization settings link, removes OrganizationPicker header block.
LoadingFallback, ThemeContext, and RegionsProvider cleanup
apps/dashboard/src/components/LoadingFallback.tsx, apps/dashboard/src/contexts/ThemeContext.tsx, apps/dashboard/src/providers/RegionsProvider.tsx
Reworks LoadingFallback to use LogoText and Skeleton at 1040px, simplifies long-loading UI without animation. Removes runWithoutAnimation from ThemeProvider. Silences RegionsProvider fetch errors to console.warn with empty array fallback.
MSW mock fixtures, MockAuthProvider, expanded handlers, and ConfigProvider mock mode
apps/dashboard/src/mocks/fixtures.ts, apps/dashboard/src/mocks/MockAuthProvider.tsx, apps/dashboard/src/mocks/handlers.ts, apps/dashboard/src/providers/ConfigProvider.tsx
Adds typed mock fixtures (user, org, boxes, config factory). Adds MockAuthProvider with hardcoded authenticated state. Expands MSW handlers to cover /organizations, /box/paginated, /box/:boxIdOrName, /regions, admin probe (403), and API key POST. ConfigProvider conditionally mounts MockAuthProvider when VITE_ENABLE_MOCKING is set.
Startup script, Vite proxy, npm scripts, and CLAUDE.md docs
apps/scripts/start-dashboard.mjs, apps/dashboard/vite.config.mts, apps/package.json, apps/CLAUDE.md
Rewrites start-dashboard.mjs with API_TARGETS map, per-target proxy/env/prod fields, --yes-prod production guard, and DASHBOARD_API_PROXY_TARGET injection. Vite reads that env var instead of hard-coded localhost. Adds start:prod npm script. Updates CLAUDE.md for apps/ workspace root and /api proxy behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • boxlite-ai/boxlite#799: Implements the same-origin /api Vite proxy model (DASHBOARD_API_PROXY_TARGET, API_TARGETS) that this PR builds on top of in start-dashboard.mjs and vite.config.mts.
  • boxlite-ai/boxlite#803: Removes the Region column and getRegionName plumbing from the BoxTable stack, directly overlapping with this PR's changes to types.ts, columns.tsx, useBoxTable.ts, and Boxes.tsx.
  • boxlite-ai/boxlite#760: Removes VNC tab plumbing from BoxContentTabs/BoxDetails at the same props and tab-routing level that this PR further refactors.

Suggested reviewers

  • DorianZheng

Poem

🐇 A dialog popped up where a sheet once lay,
The terminal tab shines in bg-black today.
Space Grotesk graces each title and card,
SSH buttons appear — no more regions scarred.
Onboarding now lives in one global host,
The rabbit ships features it loves the most! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main changes: a comprehensive dashboard restyle and addition of a self-contained mock environment.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-floating-restyle

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +67 to +68
{writePermitted && (
<Button onClick={handleStart} disabled={startMutation.isPending}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
apps/dashboard/src/lib/onboarding-code-examples.ts (1)

27-27: ⚡ Quick win

Deduplicate 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa7f0c4 and b8e9717.

⛔ Files ignored due to path filters (1)
  • apps/dashboard/src/assets/fonts/space-grotesk-latin-variable.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (46)
  • apps/CLAUDE.md
  • apps/dashboard/src/assets/fonts/Space-Grotesk-OFL.txt
  • apps/dashboard/src/components/ApiKeyTable.tsx
  • apps/dashboard/src/components/Box/CreateBoxDialog.tsx
  • apps/dashboard/src/components/Box/CreateBoxSheet.tsx
  • apps/dashboard/src/components/BoxSearchCommands.tsx
  • apps/dashboard/src/components/BoxTable/BoxState.tsx
  • apps/dashboard/src/components/BoxTable/BoxTableActions.tsx
  • apps/dashboard/src/components/BoxTable/BoxTableHeader.tsx
  • apps/dashboard/src/components/BoxTable/columns.tsx
  • apps/dashboard/src/components/BoxTable/index.tsx
  • apps/dashboard/src/components/BoxTable/types.ts
  • apps/dashboard/src/components/BoxTable/useBoxTable.ts
  • apps/dashboard/src/components/LoadingFallback.tsx
  • apps/dashboard/src/components/OnboardingDialogHost.tsx
  • apps/dashboard/src/components/OnboardingGuideDialog.tsx
  • apps/dashboard/src/components/Organizations/OrganizationPicker.tsx
  • apps/dashboard/src/components/PageLayout.tsx
  • apps/dashboard/src/components/Sidebar.tsx
  • apps/dashboard/src/components/boxes/BoxContentTabs.tsx
  • apps/dashboard/src/components/boxes/BoxDetails.tsx
  • apps/dashboard/src/components/boxes/BoxHeader.tsx
  • apps/dashboard/src/components/boxes/BoxInfoPanel.tsx
  • apps/dashboard/src/components/boxes/BoxTerminalTab.tsx
  • apps/dashboard/src/components/boxes/index.ts
  • apps/dashboard/src/components/ui/card.tsx
  • apps/dashboard/src/components/ui/dialog.tsx
  • apps/dashboard/src/components/ui/empty.tsx
  • apps/dashboard/src/components/ui/label.tsx
  • apps/dashboard/src/components/ui/sheet.tsx
  • apps/dashboard/src/contexts/ThemeContext.tsx
  • apps/dashboard/src/enums/LocalStorageKey.ts
  • apps/dashboard/src/hooks/use-mobile.tsx
  • apps/dashboard/src/index.css
  • apps/dashboard/src/lib/onboarding-code-examples.ts
  • apps/dashboard/src/mocks/MockAuthProvider.tsx
  • apps/dashboard/src/mocks/fixtures.ts
  • apps/dashboard/src/mocks/handlers.ts
  • apps/dashboard/src/pages/Boxes.tsx
  • apps/dashboard/src/pages/Dashboard.tsx
  • apps/dashboard/src/providers/ConfigProvider.tsx
  • apps/dashboard/src/providers/RegionsProvider.tsx
  • apps/dashboard/tailwind.config.js
  • apps/dashboard/vite.config.mts
  • apps/package.json
  • apps/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

Comment thread apps/CLAUDE.md
Comment on lines +22 to +23
- 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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

Comment on lines +31 to +38
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')
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +152 to +156
const copyOrgId = useCallback(() => {
if (!selectedOrganization) return
copyToClipboard(selectedOrganization.id)
toast.success('Organization ID copied to clipboard')
}, [copyToClipboard, selectedOrganization])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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


🏁 Script executed:

fd -e tsx -e ts "Sidebar" apps/dashboard/src/components/ | head -5

Repository: boxlite-ai/boxlite

Length of output: 104


🏁 Script executed:

wc -l apps/dashboard/src/components/Sidebar.tsx

Repository: boxlite-ai/boxlite

Length of output: 108


🏁 Script executed:

sed -n '145,170p' apps/dashboard/src/components/Sidebar.tsx

Repository: boxlite-ai/boxlite

Length of output: 1260


🏁 Script executed:

sed -n '160,165p' apps/dashboard/src/components/Sidebar.tsx

Repository: boxlite-ai/boxlite

Length of output: 430


🏁 Script executed:

rg -n "copyToClipboard|toast\.(success|error)" apps/dashboard/src/components/Sidebar.tsx

Repository: boxlite-ai/boxlite

Length of output: 277


🏁 Script executed:

rg -B 2 -A 2 "onSelect.*copyOrgId|copyOrgId.*onSelect" apps/dashboard/src/components/Sidebar.tsx

Repository: boxlite-ai/boxlite

Length of output: 274


🏁 Script executed:

rg -n "onSelect" apps/dashboard/src/ -A 1 | head -20

Repository: boxlite-ai/boxlite

Length of output: 1249


🏁 Script executed:

rg -n "CommandConfig|onSelect" apps/dashboard/src/vendor/pylon/ | head -30

Repository: 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.tsx

Repository: 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.

Suggested change
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.

Comment on lines +97 to +114
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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.

Comment on lines +29 to +32
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 })
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +33 to 36
// 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([])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Apply isSshAccessible gating 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

📥 Commits

Reviewing files that changed from the base of the PR and between b8e9717 and 241682f.

📒 Files selected for processing (5)
  • apps/dashboard/src/components/Box/CreateBoxDialog.tsx
  • apps/dashboard/src/components/BoxTable/BoxTableActions.tsx
  • apps/dashboard/src/components/BoxTable/index.tsx
  • apps/dashboard/src/components/Sidebar.tsx
  • apps/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

@law-chain-hot law-chain-hot changed the title feat(dashboard): dashboard restyle + self-contained mock target feat(dashboard): dashboard restyle + self-contained mock target POL-41 Jun 19, 2026
@law-chain-hot law-chain-hot changed the title feat(dashboard): dashboard restyle + self-contained mock target POL-41 feat(dashboard): dashboard restyle + self-contained mock target Jun 19, 2026
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.

1 participant