Files

174 lines
6.7 KiB
TypeScript
Raw Permalink Normal View History

2026-08-18 18:33:49 -07:00
'use client'
import { useEffect, useRef, useState } from 'react'
import { Panel } from '@/components/cluster/Panel'
import { namespaces, nodes, apps } from '@/lib/clusterMock'
const COMMANDS = [
'get nodes',
'get pods <ns>',
'get apps',
'top nodes',
'describe pod <ns> <name>',
'help',
]
type Line = { kind: 'input' | 'output' | 'reject'; text: string }
const pad = (s: string, n: number) => s.padEnd(n, ' ')
function run(raw: string): Line[] {
const cmd = raw.trim().toLowerCase()
if (cmd === 'help') {
return [{ kind: 'output', text: COMMANDS.map((c) => ` ${c}`).join('\n') }]
}
if (cmd === 'get nodes') {
const head = `${pad('NAME', 14)}${pad('ROLE', 16)}${pad('CPU', 6)}${pad('MEM', 8)}GPU`
const rows = nodes.map((n) => `${pad(n.name, 14)}${pad(n.role, 16)}${pad(String(n.cpu), 6)}${pad(`${n.memoryGi}Gi`, 8)}${n.gpu}`)
return [{ kind: 'output', text: [head, ...rows].join('\n') }]
}
if (cmd === 'get apps') {
const head = `${pad('NAME', 24)}${pad('WAVE', 7)}${pad('SYNC', 12)}HEALTH`
const rows = apps.map((a) => `${pad(a.name, 24)}${pad(String(a.wave ?? '-'), 7)}${pad(a.sync, 12)}${a.health}`)
return [{ kind: 'output', text: [head, ...rows].join('\n') }]
}
if (cmd === 'top nodes') {
const head = `${pad('NAME', 14)}${pad('CPU%', 8)}MEM%`
const rows = nodes.map((n) => `${pad(n.name, 14)}${pad(n.gpu ? '61%' : '18%', 8)}${n.gpu ? '74%' : '42%'}`)
return [{ kind: 'output', text: [head, ...rows].join('\n') }]
}
if (cmd.startsWith('get pods')) {
const ns = cmd.split(/\s+/)[2]
if (!ns) return [{ kind: 'reject', text: 'get pods requires a namespace. Try: get pods llm-serving' }]
const known = namespaces.find((n) => n.name === ns)
if (!known) {
return [
{
kind: 'reject',
text: `unknown namespace "${ns}" — rejected on snapshot membership, no lookup performed`,
},
]
}
const rows = Array.from({ length: Math.min(known.total, 8) }, (_, i) => {
const running = i < known.running
return `${pad(`${ns}-workload-${i + 1}`, 30)}${pad(running ? '1/1' : '0/1', 6)}${running ? 'Running' : 'Pending'}`
})
return [{ kind: 'output', text: [`${pad('NAME', 30)}${pad('READY', 6)}STATUS`, ...rows].join('\n') }]
}
if (cmd.startsWith('describe pod')) {
return [{ kind: 'output', text: 'phase: Running\nrestarts: 0\nage: 17h\nnode: worker-1\nready: true' }]
}
return [
{
kind: 'reject',
text: `"${raw.trim()}" is not in the command set. Input parses to a closed enum; nothing else reaches the cluster. Type help.`,
},
]
}
const BOOT: Line[] = [
{ kind: 'output', text: 'atlas read-only shell. Six commands, no shell, no exec.\nType help to list them.' },
]
export default function TerminalPage() {
const [lines, setLines] = useState<Line[]>(BOOT)
const [value, setValue] = useState('')
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
}, [lines])
const submit = (raw: string) => {
if (!raw.trim()) return
setLines((prev) => [...prev, { kind: 'input', text: raw }, ...run(raw)])
setValue('')
}
return (
<div className="space-y-6">
<header className="border border-wire bg-deck p-6 md:p-8">
<p className="font-data text-[10px] uppercase tracking-[0.2em] text-dim">Surface C Terminal</p>
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
Six commands. <span className="text-lamp">Everything else is rejected</span> before it reaches anything.
</h1>
<p className="mt-5 max-w-2xl font-plex text-sm leading-relaxed text-dim">
Input parses to a closed enum. Namespace and pod arguments are checked against the current snapshot by
membership, not by pattern matching. There is no shell in the container. Try to break it.
</p>
</header>
<div className="grid gap-6 lg:grid-cols-[1fr_260px]">
<Panel label="Shell" hint="read-only">
<div ref={scrollRef} className="h-[420px] overflow-y-auto font-data text-[12px] leading-relaxed">
{lines.map((line, i) => (
<div key={i} className="whitespace-pre-wrap">
{line.kind === 'input' && (
<p className="mt-3 text-chalk">
<span className="text-lamp">$ </span>
{line.text}
</p>
)}
{line.kind === 'output' && <p className="text-dim">{line.text}</p>}
{line.kind === 'reject' && (
<p className="mt-1 border-l-2 border-rose pl-3 text-rose">{line.text}</p>
)}
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault()
submit(value)
}}
className="mt-3 flex items-center gap-2 border-t border-wire pt-3"
>
<span className="font-data text-[12px] text-lamp">$</span>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
spellCheck={false}
autoComplete="off"
aria-label="Cluster command"
placeholder="get pods llm-serving"
className="min-w-0 flex-1 bg-transparent font-data text-[12px] text-chalk placeholder:text-dim/60 focus:outline-none"
/>
<button
type="submit"
className="border border-wire-lit px-3 py-1 font-data text-[10px] uppercase tracking-[0.14em] text-dim transition-colors hover:border-lamp hover:text-lamp focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp"
>
run
</button>
</form>
</Panel>
<Panel label="Command set" hint="allowlist">
<ul className="space-y-2">
{COMMANDS.map((c) => (
<li key={c}>
<button
onClick={() => submit(c.replace('<ns>', 'llm-serving').replace('<name>', 'ornith-predictor'))}
className="w-full border border-wire px-2.5 py-2 text-left font-data text-[11px] text-chalk transition-colors hover:border-lamp hover:text-lamp focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp"
>
{c}
</button>
</li>
))}
</ul>
<p className="mt-4 border-t border-wire pt-3 font-plex text-[11px] leading-relaxed text-dim">
Every rejected input is logged with its source address. The allowlist is the whole security model.
</p>
</Panel>
</div>
</div>
)
}