148 lines
6.3 KiB
TypeScript
148 lines
6.3 KiB
TypeScript
'use client'
|
||||
|
|
|
|||
|
|
import { useState } from 'react'
|
|||
|
|
import { Panel } from '@/components/cluster/Panel'
|
|||
|
|
import { SlotMeter } from '@/components/cluster/SlotMeter'
|
|||
|
|
import { models, namespaces, nodes, type Node } from '@/lib/clusterMock'
|
|||
|
|
|
|||
|
|
function Cells({ filled, total, tone = 'flux' }: { filled: number; total: number; tone?: 'flux' | 'rose' }) {
|
|||
|
|
return (
|
|||
|
|
<div className="flex gap-[2px]" aria-label={`${filled} of ${total} running`}>
|
|||
|
|
{Array.from({ length: total }, (_, i) => (
|
|||
|
|
<span
|
|||
|
|
key={i}
|
|||
|
|
className={`h-2.5 w-[5px] rounded-[1px] ${i < filled ? (tone === 'rose' ? 'bg-rose' : 'bg-flux') : 'bg-wire'}`}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function NodeCard({ node, active, onSelect }: { node: Node; active: boolean; onSelect: () => void }) {
|
|||
|
|
const isGpu = node.gpu > 0
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
onClick={onSelect}
|
|||
|
|
aria-pressed={active}
|
|||
|
|
className={[
|
|||
|
|
'group relative border p-4 text-left transition-colors',
|
|||
|
|
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp',
|
|||
|
|
active ? 'border-lamp bg-riser' : 'border-wire bg-deck hover:border-wire-lit',
|
|||
|
|
].join(' ')}
|
|||
|
|
>
|
|||
|
|
{isGpu && <span className="absolute right-0 top-0 h-6 w-px bg-lamp" />}
|
|||
|
|
<div className="flex items-baseline justify-between">
|
|||
|
|
<h3 className="font-signage text-xl font-semibold tracking-signage">{node.name}</h3>
|
|||
|
|
<span className={`h-1.5 w-1.5 rounded-full ${node.health === 'healthy' ? 'bg-flux' : 'bg-rose'}`} />
|
|||
|
|
</div>
|
|||
|
|
<p className={`mt-1 font-data text-[10px] uppercase tracking-[0.14em] ${isGpu ? 'text-lamp' : 'text-dim'}`}>
|
|||
|
|
{node.role}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<dl className="mt-4 space-y-1.5 font-data text-[11px]">
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<dt className="text-dim">cpu</dt>
|
|||
|
|
<dd className="tabular-nums">{node.cpu}</dd>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<dt className="text-dim">mem</dt>
|
|||
|
|
<dd className="tabular-nums">{node.memoryGi} Gi</dd>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<dt className="text-dim">gpu</dt>
|
|||
|
|
<dd className={`tabular-nums ${isGpu ? 'text-lamp' : 'text-dim'}`}>{node.gpu}</dd>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<dt className="text-dim">pods</dt>
|
|||
|
|
<dd className="tabular-nums">{node.pods}</dd>
|
|||
|
|
</div>
|
|||
|
|
</dl>
|
|||
|
|
</button>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function TopologyPage() {
|
|||
|
|
const [selected, setSelected] = useState('worker-1')
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* Thesis: the constraint that shaped everything downstream. */}
|
|||
|
|
<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 B — Topology</p>
|
|||
|
|
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
|
|||
|
|
Four nodes. One GPU. <span className="text-lamp">Eight sequence slots</span> for everything that thinks.
|
|||
|
|
</h1>
|
|||
|
|
<div className="mt-6 flex flex-wrap items-center gap-4">
|
|||
|
|
<SlotMeter used={3} size="lg" showCap />
|
|||
|
|
</div>
|
|||
|
|
<p className="mt-6 max-w-2xl font-plex text-sm leading-relaxed text-dim">
|
|||
|
|
Every workload below runs on hardware sitting in one room. The scarcest thing in it is inference
|
|||
|
|
capacity, so that is the number this page keeps in front of you.
|
|||
|
|
</p>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|||
|
|
{nodes.map((n) => (
|
|||
|
|
<NodeCard key={n.name} node={n} active={selected === n.name} onSelect={() => setSelected(n.name)} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid gap-6 lg:grid-cols-[1.4fr_1fr]">
|
|||
|
|
<Panel label="Namespaces" hint="pods running / scheduled">
|
|||
|
|
<ul className="space-y-1.5">
|
|||
|
|
{namespaces.map((ns) => {
|
|||
|
|
const short = ns.running < ns.total
|
|||
|
|
return (
|
|||
|
|
<li key={ns.name} className="flex items-center gap-4 py-0.5">
|
|||
|
|
<span className="w-40 shrink-0 truncate font-data text-[11px] text-chalk">{ns.name}</span>
|
|||
|
|
<Cells filled={ns.running} total={ns.total} tone={ns.running === 0 ? 'rose' : 'flux'} />
|
|||
|
|
<span
|
|||
|
|
className={`ml-auto shrink-0 font-data text-[11px] tabular-nums ${short ? 'text-lamp' : 'text-dim'}`}
|
|||
|
|
>
|
|||
|
|
{ns.running}/{ns.total}
|
|||
|
|
</span>
|
|||
|
|
</li>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
</Panel>
|
|||
|
|
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<Panel label="Inference" hint="KServe · worker-1">
|
|||
|
|
<ul className="space-y-3">
|
|||
|
|
{models.map((m) => (
|
|||
|
|
<li key={m.name} className="border-b border-wire pb-3 last:border-0 last:pb-0">
|
|||
|
|
<div className="flex items-baseline justify-between">
|
|||
|
|
<span className="font-signage text-base font-semibold tracking-signage">{m.name}</span>
|
|||
|
|
<span className={`font-data text-[10px] uppercase tracking-[0.14em] ${m.ready ? 'text-flux' : 'text-rose'}`}>
|
|||
|
|
{m.ready ? 'ready' : 'down'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="mt-1 font-data text-[10px] text-dim">
|
|||
|
|
{m.replicas}× replica · {m.seqPerReplica} seq · {m.contextTokens.toLocaleString()} ctx
|
|||
|
|
</p>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</Panel>
|
|||
|
|
|
|||
|
|
<Panel label="Withheld" hint="redaction allowlist">
|
|||
|
|
<ul className="space-y-1.5 font-data text-[10px] text-dim">
|
|||
|
|
{['node and pod addresses', 'container arguments', 'image tags and digests', 'source repository URLs', 'Secret names', 'condition messages'].map((x) => (
|
|||
|
|
<li key={x} className="flex items-center gap-2">
|
|||
|
|
<span className="h-px w-3 bg-wire-lit" />
|
|||
|
|
{x}
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
<p className="mt-4 font-plex text-[11px] leading-relaxed text-dim">
|
|||
|
|
Fields are built by explicit construction, so anything not listed as public never enters the
|
|||
|
|
response.
|
|||
|
|
</p>
|
|||
|
|
</Panel>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|