Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/api/src/box/constants/curated-images.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import { BadRequestError } from '../../exceptions/bad-request.exception'
* Env overrides (set on the Api service in apps/infra/sst.config.ts) allow digest
* rotation without a code deploy; the fallbacks cover local/dev runs.
*/
const SUPPORTED_IMAGE_SOURCES: Array<{ envVar: string; fallbackRef: string }> = [
type SupportedImageSource = {
envVar: string
fallbackRef: string
}

const SUPPORTED_IMAGE_SOURCES: SupportedImageSource[] = [
{
envVar: 'BOXLITE_SYSTEM_BASE_IMAGE',
fallbackRef:
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/assets/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import boxliteIconBlack from './boxlite-icon-black.png'
import boxliteIconLight from './boxlite-icon-light.png'
import boxliteWebsiteLogoDark from './boxlite-website-logo-dark.png'
import boxliteWebsiteLogoLight from './boxlite-website-logo-light.png'
import boxliteLogoBlack from './boxlite-black.png'
import boxliteLogoLight from './boxlite-light.png'

type LogoProps = {
className?: string
Expand All @@ -34,8 +34,8 @@ export function LogoText({ className = 'h-9 w-auto' }: LogoTextProps = {}) {

return (
<span className="inline-flex items-center text-foreground">
<img src={boxliteWebsiteLogoLight} alt="BoxLite" className={`block dark:hidden ${imageClassName}`} />
<img src={boxliteWebsiteLogoDark} alt="BoxLite" className={`hidden dark:block ${imageClassName}`} />
<img src={boxliteLogoBlack} alt="BoxLite" className={`block dark:hidden ${imageClassName}`} />
<img src={boxliteLogoLight} alt="BoxLite" className={`hidden dark:block ${imageClassName}`} />
</span>
)
}
Binary file modified apps/dashboard/src/assets/boxlite-black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/dashboard/src/assets/boxlite-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 68 additions & 17 deletions apps/dashboard/src/components/Box/CreateBoxSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Sheet,
SheetContent,
Expand All @@ -26,7 +27,7 @@ import { getBoxRouteId } from '@/lib/box-identity'
import { cn } from '@/lib/utils'
import type { Box } from '@boxlite-ai/api-client'
import { useForm } from '@tanstack/react-form'
import { Cpu, HardDrive, MemoryStick, Plus, type LucideIcon } from 'lucide-react'
import { Cpu, HardDrive, MemoryStick, Package, Plus, type LucideIcon } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { NumericFormat } from 'react-number-format'
import { createSearchParams, generatePath, useNavigate } from 'react-router-dom'
Expand Down Expand Up @@ -55,6 +56,7 @@ const formSchema = z.object({
.string()
.optional()
.refine((val) => !val || NAME_REGEX.test(val), 'Only letters, digits, dots, underscores and dashes are allowed'),
image: z.string().optional(),
cpu: z.string().optional().refine(isOptionalPositiveInteger, 'Enter a whole number, 1 or greater'),
memory: z.string().optional().refine(isOptionalPositiveInteger, 'Enter a whole number, 1 or greater'),
disk: z.string().optional().refine(isOptionalPositiveInteger, 'Enter a whole number, 1 or greater'),
Expand All @@ -64,13 +66,41 @@ type FormValues = z.input<typeof formSchema>

const defaultValues: FormValues = {
name: '',
image: '',
cpu: '',
memory: '',
disk: '',
}

type ResourceFieldName = 'cpu' | 'memory' | 'disk'

const BOX_CREATE_DEFAULTS: Record<ResourceFieldName, string> = {
cpu: '1',
memory: '1',
disk: '10',
}

const SUPPORTED_BOX_IMAGES = [
{
id: 'base',
name: 'Base',
ref: 'ghcr.io/boxlite-ai/boxlite-agent-base@sha256:834dcb65465985fc2f648451d76c81d166bc7672391c9064a0a115ce6306c85f',
isDefault: true,
},
{
id: 'python',
name: 'Python',
ref: 'ghcr.io/boxlite-ai/boxlite-agent-python@sha256:80d562a57f4bc12def4e54dbdb9e7d26d3268fe0767a2955ab5ad718041145d6',
isDefault: false,
},
{
id: 'node',
name: 'Node.js',
ref: 'ghcr.io/boxlite-ai/boxlite-agent-node@sha256:fcb8b840ab68567975853666c82fb6c59a3c1d14a0cdc31d7cbf3a01e6c6d247',
isDefault: false,
},
] as const

const RESOURCE_FIELDS: Array<{
name: ResourceFieldName
label: string
Expand Down Expand Up @@ -105,6 +135,7 @@ export const CreateBoxSheet = ({
const { selectedOrganization } = useSelectedOrganization()
const { reset: resetCreateBoxMutation, ...createBoxMutation } = useCreateBoxMutation()
const formRef = useRef<HTMLFormElement>(null)
const defaultImage = SUPPORTED_BOX_IMAGES.find((image) => image.isDefault) ?? SUPPORTED_BOX_IMAGES[0]

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const form = useForm({
defaultValues,
Expand All @@ -129,19 +160,16 @@ export const CreateBoxSheet = ({
let boxId: string | undefined = undefined
try {
const resources = {
cpu: parseOptionalInteger(value.cpu),
memory: parseOptionalInteger(value.memory),
disk: parseOptionalInteger(value.disk),
cpu: parseOptionalInteger(value.cpu) ?? Number(BOX_CREATE_DEFAULTS.cpu),
memory: parseOptionalInteger(value.memory) ?? Number(BOX_CREATE_DEFAULTS.memory),
disk: parseOptionalInteger(value.disk) ?? Number(BOX_CREATE_DEFAULTS.disk),
}
const hasResourceOverrides = Object.values(resources).some((resource) => resource !== undefined)

// TODO(image-rewrite): the image/template picker was removed with the image/template
// subsystem; box creation no longer selects an image. Rebuild image selection here once
// the new model lands.
const box = await createBoxMutation.mutateAsync({
name: value.name?.trim() || undefined,
image: value.image || defaultImage.ref,
network: { mode: 'enabled' },
...(hasResourceOverrides ? { resources } : {}),
resources,
})
boxId = getBoxRouteId(box)
onCreated?.(box)
Expand Down Expand Up @@ -225,7 +253,31 @@ export const CreateBoxSheet = ({
}}
</form.Field>

{/* TODO(image-rewrite): image/template picker removed with the image/template subsystem; rebuild here. */}
<form.Field name="image">
{(field) => {
const selectedImageRef = field.state.value || defaultImage?.ref || ''
return (
<Field>
<FieldLabel htmlFor={field.name} className="flex items-center gap-1.5 text-sm font-semibold">
<Package className="size-3.5" />
Image
</FieldLabel>
<Select value={selectedImageRef} onValueChange={(value) => field.handleChange(value)}>
<SelectTrigger id={field.name} name={field.name}>
<SelectValue placeholder="Select image" />
</SelectTrigger>
<SelectContent>
{SUPPORTED_BOX_IMAGES.map((image) => (
<SelectItem key={image.id} value={image.ref}>
{image.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
</form.Field>

<Accordion
type="single"
Expand All @@ -250,25 +302,25 @@ export const CreateBoxSheet = ({
<form.Field key={name} name={name}>
{(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
const defaultValue = BOX_CREATE_DEFAULTS[name]
return (
<div className="grid min-w-0 gap-2 sm:grid-cols-[minmax(0,1fr)_11rem] sm:items-center">
<div className="grid min-w-0 gap-2 sm:grid-cols-[minmax(0,1fr)_9.5rem] sm:items-center sm:gap-4">
<div className="min-w-0">
<Label
htmlFor={field.name}
className="flex items-center gap-1 text-xs font-medium text-muted-foreground"
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground"
>
<Icon className="size-3.5" />
<Icon className="size-4" />
{label}
</Label>
<p className="mt-0.5 text-xs text-muted-foreground">Optional.</p>
</div>
<div className="relative min-w-0">
<NumericFormat
customInput={Input}
aria-invalid={isInvalid}
id={field.name}
className="h-8 w-full pr-11 text-right font-medium tabular-nums placeholder:font-normal placeholder:text-muted-foreground/45"
placeholder={focusedAdvancedField === field.name ? '' : 'Default'}
className="h-8 w-full pr-11 text-right font-medium tabular-nums placeholder:font-normal placeholder:text-muted-foreground/55"
placeholder={focusedAdvancedField === field.name ? '' : defaultValue}
decimalScale={0}
allowNegative={false}
isAllowed={(values) => values.floatValue === undefined || values.floatValue >= 1}
Expand Down Expand Up @@ -296,7 +348,6 @@ export const CreateBoxSheet = ({
))}
</div>
</div>

</div>
</AccordionContent>
</AccordionItem>
Expand Down
12 changes: 6 additions & 6 deletions apps/dashboard/src/components/OnboardingGuideDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function OnboardingGuideDialog({ open, onOpenChange, onProgressChange, pr
</div>
</div>

<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-5">
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4 sm:px-5">
<div className="grid min-w-0 gap-4">
<div className="grid gap-3 rounded-md border bg-muted/15 p-3">
<div className="flex items-center gap-2 text-sm font-semibold">
Expand Down Expand Up @@ -288,7 +288,7 @@ export function OnboardingGuideDialog({ open, onOpenChange, onProgressChange, pr
</div>
</div>

<div className="grid gap-3">
<div className="grid min-w-0 gap-3">
<Tabs
value={language}
onValueChange={(value) => setLanguage(value as OnboardingLanguage)}
Expand All @@ -308,20 +308,20 @@ export function OnboardingGuideDialog({ open, onOpenChange, onProgressChange, pr
</TabsList>
</Tabs>

<div>
<div className="min-w-0">
<div className="mb-2 text-sm font-medium">Install SDK</div>
<CodeBlock code={activeExample.install} language="bash" showCopy />
</div>
<div>
<div className="min-w-0">
<div className="mb-2 text-sm font-medium">Example</div>
<CodeBlock
code={renderedExample}
language={activeExample.codeLanguage}
showCopy
codeAreaClassName="max-h-[420px] text-xs"
codeAreaClassName="overflow-x-hidden whitespace-pre-wrap break-words text-xs"
/>
</div>
<div>
<div className="min-w-0">
<div className="mb-2 text-sm font-medium">Run</div>
<CodeBlock code={activeExample.run} language="bash" showCopy />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,7 @@ const AdminTelemetryDrawer: React.FC<AdminTelemetryDrawerProps> = ({
const requestOnlyOperations = operations.filter((operation) => operation.state === 'request_only')
const hasPartialContract = Boolean(
evidence &&
(!evidence.resource ||
!evidence.externalLinks ||
!evidence.commands ||
!evidence.operations ||
!evidence.timeline),
(!evidence.resource || !evidence.externalLinks || !evidence.commands || !evidence.operations || !evidence.timeline),
)
const displayTitle = evidence?.resource?.title ?? target.title
const displaySubtitle =
Expand Down
3 changes: 1 addition & 2 deletions apps/dashboard/src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ const sheetVariants = cva(
)

interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = 'right', className, children, ...props }, ref) => (
Expand Down
3 changes: 0 additions & 3 deletions apps/dashboard/src/hooks/queries/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ export const queryKeys = {
metrics: (boxId: string, params: object) => [...queryKeys.telemetry.all, boxId, 'metrics', params] as const,
traceSpans: (boxId: string, traceId: string) => [...queryKeys.telemetry.all, boxId, 'traces', traceId] as const,
},
box: {
all: ['box'] as const,
},
analytics: {
all: ['analytics'] as const,
aggregatedUsage: (organizationId: string, params: object) =>
Expand Down
29 changes: 14 additions & 15 deletions apps/infra-local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,18 +319,17 @@ The migration squash removed the box-template/snapshot subsystem, then
`#755`/`#758` reintroduced an **image-keyed** boot path (the box-start
`UNKNOWN` handler now calls `runnerAdapter.createBox` —
`apps/api/src/box/managers/box-actions/box-start.action.ts`) and restricted
creation to supported pinned images. What's still *not* rebuilt is image
creation to supported pinned images. The dashboard now reads that supported
image list directly from the API. What's still *not* rebuilt is broader image
**resolution** — see the `TODO(image-rewrite)` markers in
`apps/api/src/box/services/box.service.ts:136,:155` (and ~20 more across
`apps/api`/`apps/dashboard` for the removed template webhooks, metrics, and
the dashboard image/template picker — e.g. `CreateBoxSheet.tsx:138,:228`).
`apps/api/src/box/services/box.service.ts:136,:155` (and remaining markers
across `apps/api`/`apps/dashboard` for the removed template webhooks and
metrics).

Net: a box with no image fails fast (`'Box has no image to create from'`),
and the dashboard's image picker is gone, so end-to-end "Create Box" from
the UI is incomplete. We have **not** verified a successful box boot on this
stack since the rebase (it also needs a rebuilt `libboxlite.a` for the
runner). Everything else — L1 services, API, runner registration, auth,
dashboard — works.
Net: the dashboard can create with the supported pinned images, but we have
**not** verified a successful box boot on this stack since the rebase (it also
needs a rebuilt `libboxlite.a` for the runner). Everything else — L1 services,
API, runner registration, auth, dashboard — works.

---

Expand Down Expand Up @@ -518,10 +517,10 @@ preseeded accounts (see [`apps/infra-local/CONNECTIONS.md` §4](CONNECTIONS.md))
Then click **Create Box** → pick region `us` → **Create** → open
the **Terminal** tab → **Connect** → you should see `root@boxlite:~#`.

> ⚠️ Box creation is mid-rewrite upstream and unverified on this stack: image
> **resolution** isn't rebuilt yet and the dashboard's image picker was
> removed, so "Create Box" from the UI is incomplete. See
> [Known limitations](#known-limitations) #4. The rest of the stack works.
> ⚠️ Box creation is mid-rewrite upstream and unverified on this stack: broader
> image **resolution** isn't rebuilt yet, though the dashboard does expose the
> supported pinned-image picker. See [Known limitations](#known-limitations)
> #4. The rest of the stack works.

### 2. Day-to-day dashboard development loop

Expand Down Expand Up @@ -725,7 +724,7 @@ ls -lt .apps-local/boxlite-runner/boxes/<id>/logs/
| Symptom | Likely cause | Fix |
|---|---|---|
| All API calls return 401 | `SSH_GATEWAY_API_KEY` or `PROXY_API_KEY` not set | Check `apps/api/.env` — both must be non-empty |
| "Create Box" from the dashboard is incomplete / box doesn't boot | Expected: image resolution is mid-rewrite upstream and the dashboard image picker was removed (`TODO(image-rewrite)`) | See [Known limitations](#known-limitations) #4 — the rest of the stack is unaffected |
| "Create Box" from the dashboard doesn't boot | Expected: broader image resolution is mid-rewrite upstream; the dashboard only exposes the current supported pinned-image list | See [Known limitations](#known-limitations) #4 — the rest of the stack is unaffected |
| Box reaches STARTED but the terminal is blank + `Connection closed` | image is amd64 but runner runs an arm64 microVM | Already fixed (`runner/registry.go` uses `runtime.GOARCH`) — clear the old image cache and re-pull |
| "Create Box" missing in the dashboard | Expected: PostHog isn't configured locally, so flag-gated UI stays hidden (no local flag bootstrap) | Use `POST /api/box` directly, or set `POSTHOG_API_KEY`/`POSTHOG_HOST` in `apps/api/.env` with the flags enabled in PostHog |
| `POST /api/regions` → 404 "Cannot POST" | Expected: same — flag-gated admin routes stay hidden without a configured PostHog | Same as above; the seed path (`seed-init-data.sh`) doesn't need these routes |
Expand Down
Loading