+ {firstName ? `${firstName}'s Machine` : 'My Machine'} +
+
+ Your always-available dev box — a persistent home, a terminal, and a
+ file browser rooted at ~.
+
{status === 'connected' - ? 'Connected · workspace shell' + ? `Connected · ${label}` : status === 'connecting' ? 'Connecting…' : status === 'closed' diff --git a/apps/next/src/components/layout/header/index.tsx b/apps/next/src/components/layout/header/index.tsx index 609b704..f2f8bf7 100644 --- a/apps/next/src/components/layout/header/index.tsx +++ b/apps/next/src/components/layout/header/index.tsx @@ -8,6 +8,7 @@ import { GitBranch, LayoutDashboard, MessagesSquare, + Server, Settings, ShieldCheck, } from 'lucide-react'; @@ -37,6 +38,11 @@ const Header = (headerProps: ComponentProps<'header'>) => { icon: MessagesSquare, label: 'Threads', }, + { + href: '/machine', + icon: Server, + label: 'Machine', + }, { href: '/settings/profile', icon: Settings, diff --git a/apps/next/src/components/machine/box-status-card.tsx b/apps/next/src/components/machine/box-status-card.tsx new file mode 100644 index 0000000..ba393a2 --- /dev/null +++ b/apps/next/src/components/machine/box-status-card.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { Loader2, Play, RotateCw, Server, Square } from 'lucide-react'; + +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + CardTitle, +} from '@spoon/ui'; + +export type BoxStatus = { + running: boolean; + image: string | null; + startedAt: string | null; + memoryLimitBytes: number | null; + containerName: string; +}; + +export type LifecycleAction = 'start' | 'stop' | 'restart'; + +const formatUptime = (startedAt: string | null): string => { + if (!startedAt) return '—'; + const started = new Date(startedAt).getTime(); + if (Number.isNaN(started)) return '—'; + const seconds = Math.max(0, Math.floor((Date.now() - started) / 1000)); + const days = Math.floor(seconds / 86_400); + const hours = Math.floor((seconds % 86_400) / 3_600); + const minutes = Math.floor((seconds % 3_600) / 60); + if (days > 0) return `${days}d ${hours}h`; + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m`; + return `${seconds}s`; +}; + +const formatMemory = (bytes: number | null): string => { + if (bytes === null) return 'Unlimited'; + const gib = bytes / 1024 ** 3; + return `${gib.toFixed(gib < 10 ? 1 : 0)} GiB`; +}; + +const Detail = ({ label, value }: { label: string; value: string }) => ( +
+ {label} +
+{value}
+
+ Your always-available dev box — a persistent home, a terminal, and a
+ file browser rooted at ~.
+