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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ apps/.nx

# Build outputs
target
dist
apps/dist
coverage
apps/coverage
*.log

# Git
.git
33 changes: 29 additions & 4 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FROM node:24-slim AS boxlite
# build: 2026-04-28-v2
# syntax=docker/dockerfile:1

FROM node:24-slim AS base
ENV CI=true

RUN apt-get update && apt-get install -y --no-install-recommends bash curl && \
Expand All @@ -8,12 +9,18 @@ RUN npm install -g corepack && corepack enable

WORKDIR /boxlite/apps

FROM base AS deps
COPY apps/package.json apps/yarn.lock apps/.yarnrc.yml ./
RUN yarn install --immutable

FROM base AS runtime-deps
ENV NODE_ENV=production
COPY apps/package.json apps/yarn.lock apps/.yarnrc.yml ./
RUN yarn workspaces focus --all --production

FROM deps AS build
COPY apps/nx.json apps/tsconfig.base.json apps/NOTICE ./

ARG CACHE_BUST=1
ENV NX_DAEMON=false
ENV NX_SKIP_NX_CACHE=true

Expand All @@ -26,13 +33,31 @@ COPY apps/libs/analytics-api-client/ libs/analytics-api-client/
COPY apps/libs/toolbox-api-client/ libs/toolbox-api-client/
COPY apps/libs/sdk-typescript/ libs/sdk-typescript/

RUN echo "$CACHE_BUST" && yarn nx build api --configuration=production --nxBail=true && node --check dist/apps/api/main.js
RUN yarn nx build api --configuration=production --nxBail=true --output-style=stream && node --check dist/apps/api/main.js

RUN VITE_BASE_API_URL=%BOXLITE_BASE_API_URL% yarn nx build dashboard --configuration=production --nxBail=true --output-style=stream

FROM node:24-slim AS boxlite
ENV CI=true
ENV NODE_ENV=production

RUN apt-get update && apt-get install -y --no-install-recommends bash curl && \
rm -rf /var/lib/apt/lists/*

WORKDIR /boxlite/apps

COPY --from=runtime-deps --chown=node:node /boxlite/apps/package.json ./package.json
COPY --from=runtime-deps --chown=node:node /boxlite/apps/node_modules ./node_modules
COPY --from=build --chown=node:node /boxlite/apps/NOTICE ./NOTICE
COPY --from=build --chown=node:node /boxlite/apps/dist/apps/api ./dist/apps/api
COPY --from=build --chown=node:node /boxlite/apps/dist/apps/dashboard ./dist/apps/dashboard

ARG VERSION=0.0.1
ENV VERSION=${VERSION}

EXPOSE 3000
HEALTHCHECK CMD [ "curl", "-f", "http://localhost:3000/api/config" ]

USER node

ENTRYPOINT ["node", "dist/apps/api/main.js"]
25 changes: 10 additions & 15 deletions apps/api/project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "api",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/api/src",
"sourceRoot": "api/src",
"projectType": "application",
"tags": [],
"targets": {
Expand All @@ -10,24 +10,23 @@
"options": {
"outputPath": "dist/apps/api",
"deleteOutputPath": false,
"main": "apps/api/src/main.ts",
"tsConfig": "apps/api/tsconfig.app.json",
"generatePackageJson": true,
"main": "api/src/main.ts",
"tsConfig": "api/tsconfig.app.json",
"generatePackageJson": false,
"target": "node",
"compiler": "tsc",
"sourceMap": true,
"webpackConfig": "apps/api/webpack.config.js",
"webpackConfig": "api/webpack.config.js",
"assets": [
{
"input": "apps/api/src/assets",
"input": "api/src/assets",
"glob": "**/*",
"output": "./assets/"
}
]
},
"configurations": {
"development": {
"generatePackageJson": false,
"skipTypeChecking": true
},
"production": {
Expand Down Expand Up @@ -95,14 +94,10 @@
"check-version-env": {},
"docker": {
"options": {
"target": "boxlite"
},
"dependsOn": [
{
"target": "build-amd64",
"projects": "runner"
}
]
"target": "boxlite",
"context": "..",
"file": "apps/api/Dockerfile"
}
},
"push-manifest": {},
"lint": {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"references": [
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "daemon",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/daemon",
"sourceRoot": "daemon",
"tags": [],
"targets": {
"prepare": {
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dashboard",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/dashboard/src",
"sourceRoot": "dashboard/src",
"projectType": "application",
"tags": [],
"targets": {
Expand Down
11 changes: 8 additions & 3 deletions apps/dashboard/src/components/Box/CreateBoxSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export const CreateBoxSheet = ({

let boxId: string | undefined = undefined
try {
const resources = {
cpu: parseOptionalInteger(value.cpu),
memory: parseOptionalInteger(value.memory),
disk: parseOptionalInteger(value.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.
Expand All @@ -157,9 +164,7 @@ export const CreateBoxSheet = ({
networkBlockAll: false,
autoStopInterval: parseOptionalInteger(value.autoStopInterval),
autoDeleteInterval: parseOptionalInteger(value.autoDeleteInterval),
cpu: parseOptionalInteger(value.cpu),
memory: parseOptionalInteger(value.memory),
disk: parseOptionalInteger(value.disk),
...(hasResourceOverrides ? { resources } : {}),
})
boxId = getBoxRouteId(box)
onCreated?.(box)
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/BoxTable/BoxState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function BoxState({ state, errorReason, recoverable, className }: BoxStat
const stateIcon = recoverable ? STATE_ICONS['RECOVERY'] : STATE_ICONS[state] || STATE_ICONS[BoxStateType.UNKNOWN]
const label = getStateLabel(state)

if (state === BoxStateType.ERROR || state === BoxStateType.BUILD_FAILED) {
if (state === BoxStateType.ERROR) {
const errorColor = recoverable ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400'

const errorContent = (
Expand Down
5 changes: 1 addition & 4 deletions apps/dashboard/src/components/BoxTable/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ const STATE_LABEL_MAPPING: Record<BoxState, string> = {
[BoxState.STARTED]: 'Running',
[BoxState.STOPPED]: 'Stopped',
[BoxState.ERROR]: 'Error',
[BoxState.BUILD_FAILED]: 'Build Failed',
[BoxState.BUILDING_ARTIFACT]: 'Preparing runtime',
[BoxState.PENDING_BUILD]: 'Pending Build',
[BoxState.RESTORING]: 'Restoring',
[BoxState.ARCHIVED]: 'Archived',
[BoxState.CREATING]: 'Creating',
Expand All @@ -23,6 +20,7 @@ const STATE_LABEL_MAPPING: Record<BoxState, string> = {
[BoxState.DESTROYING]: 'Deleting',
[BoxState.DESTROYED]: 'Deleted',
[BoxState.UNKNOWN]: 'Unknown',
[BoxState.UNKNOWN_DEFAULT_OPEN_API]: 'Unknown',
[BoxState.ARCHIVING]: 'Archiving',
[BoxState.RESIZING]: 'Resizing',
}
Expand All @@ -35,7 +33,6 @@ export const STATUSES: FacetedFilterOption[] = [
},
{ label: getStateLabel(BoxState.STOPPED), value: BoxState.STOPPED, icon: Circle },
{ label: getStateLabel(BoxState.ERROR), value: BoxState.ERROR, icon: AlertTriangle },
{ label: getStateLabel(BoxState.BUILD_FAILED), value: BoxState.BUILD_FAILED, icon: AlertTriangle },
{ label: getStateLabel(BoxState.STARTING), value: BoxState.STARTING, icon: Timer },
{ label: getStateLabel(BoxState.STOPPING), value: BoxState.STOPPING, icon: Timer },
{ label: getStateLabel(BoxState.DESTROYING), value: BoxState.DESTROYING, icon: Timer },
Expand Down
4 changes: 1 addition & 3 deletions apps/dashboard/src/components/BoxTable/state-icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ export const STATE_ICONS: Record<BoxState | 'RECOVERY', React.ReactNode> = {
[BoxState.DESTROYING]: <Loader2 className="w-3 h-3 animate-spin" />,
[BoxState.DESTROYED]: <Square color="bg-muted-foreground/20" />,
[BoxState.ERROR]: <Square color="bg-destructive" />,
[BoxState.BUILD_FAILED]: <Square color="bg-destructive" />,
[BoxState.BUILDING_ARTIFACT]: <Loader2 className="w-3 h-3 animate-spin" />,
[BoxState.PENDING_BUILD]: <Square color="bg-muted-foreground/20" />,
[BoxState.ARCHIVED]: <Square color="bg-muted-foreground/20" />,
[BoxState.ARCHIVING]: <Loader2 className="w-3 h-3 animate-spin" />,
[BoxState.RESTORING]: <Loader2 className="w-3 h-3 animate-spin" />,
[BoxState.RESIZING]: <Loader2 className="w-3 h-3 animate-spin" />,
[BoxState.UNKNOWN_DEFAULT_OPEN_API]: <Square color="bg-muted-foreground/20" />,
RECOVERY: <Square color="bg-yellow-600" />,
}
4 changes: 0 additions & 4 deletions apps/dashboard/src/components/RunnerDetailsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,6 @@ const RunnerDetailsSheet: React.FC<RunnerDetailsSheetProps> = ({
<h4 className="text-muted-foreground text-xs">Active Boxes</h4>
<p className="mt-1 text-2xl font-semibold">{runner.currentStartedBoxes ?? 0}</p>
</div>
<div>
<h4 className="text-muted-foreground text-xs">Images</h4>
<p className="mt-1 text-2xl font-semibold">{runner.currentArtifactCount ?? 0}</p>
</div>
</div>
</div>

Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/hooks/mutations/useDeleteBoxMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import { useSelectedOrganization } from '@/hooks/useSelectedOrganization'
import { queryKeys } from '@/hooks/queries/queryKeys'
import { useMutation, useQueryClient } from '@tanstack/react-query'

interface StartBoxVariables {
interface DeleteBoxVariables {
boxId: string
detailRef?: string
}

export const useStartBoxMutation = () => {
export const useDeleteBoxMutation = () => {
const { boxApi } = useApi()
const { selectedOrganization } = useSelectedOrganization()
const queryClient = useQueryClient()

return useMutation({
mutationFn: async ({ boxId }: StartBoxVariables) => {
await boxApi.startBox(boxId, selectedOrganization?.id)
mutationFn: async ({ boxId }: DeleteBoxVariables) => {
await boxApi.deleteBox(boxId, selectedOrganization?.id)
},
onSuccess: (_, { boxId, detailRef }) => {
queryClient.invalidateQueries({
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/hooks/mutations/useRecoverBoxMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import { useSelectedOrganization } from '@/hooks/useSelectedOrganization'
import { queryKeys } from '@/hooks/queries/queryKeys'
import { useMutation, useQueryClient } from '@tanstack/react-query'

interface DeleteBoxVariables {
interface RecoverBoxVariables {
boxId: string
detailRef?: string
}

export const useDeleteBoxMutation = () => {
export const useRecoverBoxMutation = () => {
const { boxApi } = useApi()
const { selectedOrganization } = useSelectedOrganization()
const queryClient = useQueryClient()

return useMutation({
mutationFn: async ({ boxId }: DeleteBoxVariables) => {
await boxApi.deleteBox(boxId, selectedOrganization?.id)
mutationFn: async ({ boxId }: RecoverBoxVariables) => {
await boxApi.recoverBox(boxId, selectedOrganization?.id)
},
onSuccess: (_, { boxId, detailRef }) => {
queryClient.invalidateQueries({
Expand Down
36 changes: 36 additions & 0 deletions apps/dashboard/src/hooks/mutations/useStartBoxMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Daytona Platforms Inc.
* SPDX-License-Identifier: AGPL-3.0
*/

import { useApi } from '@/hooks/useApi'
import { useSelectedOrganization } from '@/hooks/useSelectedOrganization'
import { queryKeys } from '@/hooks/queries/queryKeys'
import { useMutation, useQueryClient } from '@tanstack/react-query'

interface StartBoxVariables {
boxId: string
detailRef?: string
}

export const useStartBoxMutation = () => {
const { boxApi } = useApi()
const { selectedOrganization } = useSelectedOrganization()
const queryClient = useQueryClient()

return useMutation({
mutationFn: async ({ boxId }: StartBoxVariables) => {
await boxApi.startBox(boxId, selectedOrganization?.id)
},
onSuccess: (_, { boxId, detailRef }) => {
queryClient.invalidateQueries({
queryKey: queryKeys.boxes.detail(selectedOrganization?.id ?? '', boxId),
})
if (detailRef && detailRef !== boxId) {
queryClient.invalidateQueries({
queryKey: queryKeys.boxes.detail(selectedOrganization?.id ?? '', detailRef),
})
}
},
})
}
8 changes: 7 additions & 1 deletion apps/dashboard/src/hooks/mutations/useStopBoxMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'

interface StopBoxVariables {
boxId: string
detailRef?: string
}

export const useStopBoxMutation = () => {
Expand All @@ -21,10 +22,15 @@ export const useStopBoxMutation = () => {
mutationFn: async ({ boxId }: StopBoxVariables) => {
await boxApi.stopBox(boxId, selectedOrganization?.id)
},
onSuccess: (_, { boxId }) => {
onSuccess: (_, { boxId, detailRef }) => {
queryClient.invalidateQueries({
queryKey: queryKeys.boxes.detail(selectedOrganization?.id ?? '', boxId),
})
if (detailRef && detailRef !== boxId) {
queryClient.invalidateQueries({
queryKey: queryKeys.boxes.detail(selectedOrganization?.id ?? '', detailRef),
})
}
},
})
}
9 changes: 3 additions & 6 deletions apps/dashboard/src/hooks/useBoxWsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,8 @@ export function useBoxWsSync({ boxId, refetchOnCreate = false }: UseBoxWsSyncOpt

let updatedState = data.newState

// error/build_failed with desiredState=DESTROYED should display as destroyed
if (
data.box.desiredState === BoxDesiredState.DESTROYED &&
(data.newState === BoxState.ERROR || data.newState === BoxState.BUILD_FAILED)
) {
// error with desiredState=DESTROYED should display as destroyed
if (data.box.desiredState === BoxDesiredState.DESTROYED && data.newState === BoxState.ERROR) {
updatedState = BoxState.DESTROYED
}

Expand All @@ -110,7 +107,7 @@ export function useBoxWsSync({ boxId, refetchOnCreate = false }: UseBoxWsSyncOpt
if (!matchesActiveBox(data.box)) return

if (data.newDesiredState !== BoxDesiredState.DESTROYED) return
if (data.box.state !== BoxState.ERROR && data.box.state !== BoxState.BUILD_FAILED) return
if (data.box.state !== BoxState.ERROR) return

optimisticUpdate(data.box, BoxState.DESTROYED)
invalidate()
Expand Down
3 changes: 1 addition & 2 deletions apps/dashboard/src/lib/utils/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export function isTransitioning(box: Box): boolean {
box.state === BoxState.STARTING ||
box.state === BoxState.STOPPING ||
box.state === BoxState.DESTROYING ||
box.state === BoxState.RESTORING ||
box.state === BoxState.BUILDING_ARTIFACT
box.state === BoxState.RESTORING
)
}

Expand Down
Loading
Loading