(chore) init commit and add tasks
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user