(chore) init commit and add tasks
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { Panel } from '@/components/cluster/Panel'
|
||||
import { SlotMeter } from '@/components/cluster/SlotMeter'
|
||||
|
||||
const DAILY_BUDGET = 12
|
||||
|
||||
type Turn = {
|
||||
role: 'user' | 'model'
|
||||
reasoning?: string
|
||||
content: string
|
||||
streaming?: boolean
|
||||
}
|
||||
|
||||
const SEED: Turn[] = [
|
||||
{ role: 'user', content: 'Why is the sms application degraded?' },
|
||||
{
|
||||
role: 'model',
|
||||
reasoning:
|
||||
'The snapshot lists sms with health "degraded" and sync "synced". Sync being clean means the manifests applied fine, so this is a runtime problem rather than a delivery one. The sms namespace shows 0 of 1 pods running. A single pod that never reaches Running, with Argo reporting the desired state as applied, usually means the pod cannot be placed at all.',
|
||||
content:
|
||||
'Delivery is fine — sms is Synced, so Argo applied everything it was asked to. The failure is at runtime: the namespace has one pod and zero running. Argo marks the app degraded because the workload never became ready, not because the manifests are wrong.',
|
||||
},
|
||||
]
|
||||
|
||||
const REPLY =
|
||||
'Wave 4 is empty. Waves are sort keys, not a sequence — Argo orders by value and skips gaps, so 3 is followed directly by 5. Nothing is missing.'
|
||||
|
||||
const REPLY_REASONING =
|
||||
'The user is asking about a gap in the wave column display. Argo CD sync waves are integers used purely for ordering; there is no requirement that they be contiguous. The cluster uses 0,1,2,3,5,6,7,8. This is normal and usually happens when a wave is retired or intentionally reserved.'
|
||||
|
||||
export default function ChatPage() {
|
||||
const [turns, setTurns] = useState<Turn[]>(SEED)
|
||||
const [value, setValue] = useState('')
|
||||
const [queue, setQueue] = useState<number | null>(null)
|
||||
const [slots, setSlots] = useState(3)
|
||||
const [used, setUsed] = useState(2)
|
||||
const [openReasoning, setOpenReasoning] = useState<number | null>(1)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}, [turns, queue])
|
||||
|
||||
const send = () => {
|
||||
if (!value.trim() || queue !== null || used >= DAILY_BUDGET) return
|
||||
setTurns((t) => [...t, { role: 'user', content: value }])
|
||||
setValue('')
|
||||
setQueue(3)
|
||||
}
|
||||
|
||||
// Placeholder for the queue → stream transition. Real version is SSE from atlas.
|
||||
useEffect(() => {
|
||||
if (queue === null) return
|
||||
if (queue > 0) {
|
||||
const t = setTimeout(() => setQueue((q) => (q === null ? null : q - 1)), 700)
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
setQueue(null)
|
||||
setSlots((s) => Math.min(s + 1, 6))
|
||||
setUsed((u) => u + 1)
|
||||
setTurns((t) => [...t, { role: 'model', reasoning: REPLY_REASONING, content: '', streaming: true }])
|
||||
|
||||
let i = 0
|
||||
const stream = setInterval(() => {
|
||||
i += 3
|
||||
setTurns((t) => {
|
||||
const next = [...t]
|
||||
const last = next[next.length - 1]
|
||||
if (last?.role !== 'model') return t
|
||||
next[next.length - 1] = { ...last, content: REPLY.slice(0, i), streaming: i < REPLY.length }
|
||||
return next
|
||||
})
|
||||
if (i >= REPLY.length) {
|
||||
clearInterval(stream)
|
||||
setSlots((s) => Math.max(s - 1, 1))
|
||||
}
|
||||
}, 40)
|
||||
return () => clearInterval(stream)
|
||||
}, [queue])
|
||||
|
||||
const exhausted = used >= DAILY_BUDGET
|
||||
|
||||
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 D — Chat</p>
|
||||
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
|
||||
A 32B model, running on one card, <span className="text-lamp">two rooms from here.</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-2xl font-plex text-sm leading-relaxed text-dim">
|
||||
It reads a redacted snapshot of the cluster and answers questions about it. It cannot query anything, run
|
||||
anything, or change anything. When all six public slots are busy, you wait in line — the queue is real,
|
||||
and so is the hardware.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap items-center gap-6">
|
||||
<SlotMeter used={slots} size="lg" showCap />
|
||||
<span className="font-data text-[10px] uppercase tracking-[0.14em] text-dim">
|
||||
{DAILY_BUDGET - used} of {DAILY_BUDGET} messages left today
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_260px]">
|
||||
<Panel label="Session" hint="anonymous">
|
||||
<div ref={scrollRef} className="h-[440px] space-y-5 overflow-y-auto pr-1">
|
||||
{turns.map((turn, i) =>
|
||||
turn.role === 'user' ? (
|
||||
<div key={i} className="flex justify-end">
|
||||
<p className="max-w-[85%] border border-wire-lit bg-riser px-3 py-2 font-plex text-sm text-chalk">
|
||||
{turn.content}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div key={i} className="max-w-[92%] space-y-2">
|
||||
{turn.reasoning && (
|
||||
<div className="border-l-2 border-lamp/50">
|
||||
<button
|
||||
onClick={() => setOpenReasoning(openReasoning === i ? null : i)}
|
||||
aria-expanded={openReasoning === i}
|
||||
className="flex w-full items-center gap-2 py-1 pl-3 font-data text-[10px] uppercase tracking-[0.16em] text-lamp focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp"
|
||||
>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className={`transition-transform ${openReasoning === i ? '' : '-rotate-90'}`}
|
||||
/>
|
||||
thinking
|
||||
</button>
|
||||
{openReasoning === i && (
|
||||
<p className="py-1 pl-3 pr-2 font-data text-[11px] leading-relaxed text-dim">
|
||||
{turn.reasoning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="font-plex text-sm leading-relaxed text-chalk">
|
||||
{turn.content}
|
||||
{turn.streaming && <span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-lamp align-text-bottom" />}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
{queue !== null && (
|
||||
<div className="flex items-center gap-3 border border-lamp/40 px-3 py-2">
|
||||
<SlotMeter used={6} />
|
||||
<p className="font-data text-[11px] text-lamp">
|
||||
all six public slots busy · position {queue} in queue
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
send()
|
||||
}}
|
||||
className="mt-3 flex items-center gap-2 border-t border-wire pt-3"
|
||||
>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
disabled={exhausted}
|
||||
aria-label="Message"
|
||||
placeholder={exhausted ? 'Daily limit reached — resets in 14h' : 'Ask about the cluster'}
|
||||
className="min-w-0 flex-1 bg-transparent font-plex text-sm text-chalk placeholder:text-dim/60 focus:outline-none disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={exhausted || queue !== null}
|
||||
className="border border-lamp px-3 py-1 font-data text-[10px] uppercase tracking-[0.14em] text-lamp transition-colors hover:bg-lamp hover:text-void focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-lamp"
|
||||
>
|
||||
send
|
||||
</button>
|
||||
</form>
|
||||
</Panel>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Panel label="Limits" hint="tier 3">
|
||||
<dl className="space-y-2.5 font-data text-[11px]">
|
||||
{[
|
||||
['public slots', '6 of 8'],
|
||||
['queue depth', '20'],
|
||||
['per session', '12 / day'],
|
||||
['request timeout', '120s'],
|
||||
['output cap', '1500 tokens'],
|
||||
].map(([k, v]) => (
|
||||
<div key={k} className="flex items-baseline gap-2">
|
||||
<dt className="text-dim">{k}</dt>
|
||||
<dd className="ml-auto tabular-nums text-chalk">{v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<p className="mt-4 border-t border-wire pt-3 font-plex text-[11px] leading-relaxed text-dim">
|
||||
Closing this tab frees your slot immediately. Two slots stay reserved so the operator is never locked
|
||||
out of their own hardware.
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel label="Cannot do" hint="by construction">
|
||||
<ul className="space-y-1.5 font-data text-[10px] text-dim">
|
||||
{['call tools', 'query the cluster', 'read logs', 'change anything', 'see Secret contents'].map((x) => (
|
||||
<li key={x} className="flex items-center gap-2">
|
||||
<span className="h-px w-3 bg-wire-lit" />
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Panel } from '@/components/cluster/Panel'
|
||||
import { DEFAULT_KINDS, WAVES, apps, appsInWave, resourceKinds, type App } from '@/lib/clusterMock'
|
||||
|
||||
const TOTAL_RESOURCES = apps.reduce((n, a) => n + a.resources, 0)
|
||||
|
||||
/** Resource count as discrete cells, capped so a 68-resource app stays readable. */
|
||||
function CountBar({ n }: { n: number }) {
|
||||
const cells = Math.min(n, 24)
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-[2px]" aria-label={`${n} resources`}>
|
||||
{Array.from({ length: cells }, (_, i) => (
|
||||
<span key={i} className="h-1.5 w-1.5 rounded-[1px] bg-wire-lit" />
|
||||
))}
|
||||
{n > 24 && <span className="ml-1 font-data text-[9px] leading-none text-dim">+{n - 24}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AppCard({
|
||||
app,
|
||||
state,
|
||||
active,
|
||||
onSelect,
|
||||
}: {
|
||||
app: App
|
||||
state: 'idle' | 'syncing' | 'done'
|
||||
active: boolean
|
||||
onSelect: () => void
|
||||
}) {
|
||||
const degraded = app.health === 'degraded'
|
||||
const drifted = app.sync === 'outofsync'
|
||||
|
||||
const border = active
|
||||
? 'border-lamp'
|
||||
: state === 'syncing'
|
||||
? 'border-lamp/70'
|
||||
: degraded
|
||||
? 'border-rose/50'
|
||||
: drifted
|
||||
? 'border-lamp/40'
|
||||
: 'border-wire'
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onSelect}
|
||||
aria-pressed={active}
|
||||
className={[
|
||||
'w-full border bg-deck p-2.5 text-left transition-all duration-300',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp',
|
||||
border,
|
||||
state === 'syncing' ? 'bg-riser' : '',
|
||||
active ? 'bg-riser' : 'hover:border-wire-lit',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-data text-[11px] leading-tight text-chalk">{app.name}</span>
|
||||
<span
|
||||
className={[
|
||||
'mt-1 h-1.5 w-1.5 shrink-0 rounded-full transition-colors',
|
||||
state === 'syncing' ? 'bg-lamp' : degraded ? 'bg-rose' : drifted ? 'bg-lamp' : 'bg-flux',
|
||||
].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
<CountBar n={app.resources} />
|
||||
{(degraded || drifted) && (
|
||||
<p className={`mt-2 font-data text-[9px] uppercase tracking-[0.14em] ${degraded ? 'text-rose' : 'text-lamp'}`}>
|
||||
{degraded ? 'degraded' : 'out of sync'}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DeliveryPage() {
|
||||
const [selected, setSelected] = useState<string | null>('prometheus')
|
||||
const [front, setFront] = useState<number | null>(null)
|
||||
|
||||
const replay = useCallback(() => setFront(0), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (front === null) return
|
||||
if (front > 8) {
|
||||
const done = setTimeout(() => setFront(null), 700)
|
||||
return () => clearTimeout(done)
|
||||
}
|
||||
const next = setTimeout(() => setFront((w) => (w === null ? null : w + 1)), 520)
|
||||
return () => clearTimeout(next)
|
||||
}, [front])
|
||||
|
||||
const selectedApp = apps.find((a) => a.name === selected) ?? null
|
||||
const kinds = selectedApp ? (resourceKinds[selectedApp.name] ?? DEFAULT_KINDS) : []
|
||||
|
||||
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 E — Delivery</p>
|
||||
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
|
||||
Nothing starts until the thing it needs is already running.
|
||||
</h1>
|
||||
<p className="mt-5 max-w-2xl font-plex text-sm leading-relaxed text-dim">
|
||||
{apps.length} applications, {TOTAL_RESOURCES} resources, applied in nine ordered waves. Certificates
|
||||
before issuers, issuers before ingress, storage before the databases that sit on it. The order is the
|
||||
design.
|
||||
</p>
|
||||
<button
|
||||
onClick={replay}
|
||||
disabled={front !== null}
|
||||
className="mt-6 border border-lamp px-4 py-2 font-data text-[11px] uppercase tracking-[0.16em] text-lamp transition-colors hover:bg-lamp hover:text-void focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-lamp"
|
||||
>
|
||||
{front !== null ? `syncing wave ${Math.min(front, 8)}` : 'Replay sync'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1fr_340px]">
|
||||
<Panel label="Sync waves" hint="0 → 8, left to right" className="min-w-0">
|
||||
<div className="-mx-4 overflow-x-auto px-4 pb-2">
|
||||
<div className="flex min-w-max gap-3">
|
||||
{WAVES.map((wave) => {
|
||||
const inWave = appsInWave(wave)
|
||||
const state = front === null ? 'idle' : front === wave ? 'syncing' : front > wave ? 'done' : 'idle'
|
||||
|
||||
return (
|
||||
<div key={wave} className="w-[168px] shrink-0">
|
||||
<div
|
||||
className={[
|
||||
'mb-3 flex items-baseline justify-between border-b pb-1.5 transition-colors',
|
||||
state === 'syncing' ? 'border-lamp' : 'border-wire',
|
||||
].join(' ')}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
'font-signage text-2xl font-semibold leading-none transition-colors',
|
||||
state === 'syncing' ? 'text-lamp' : inWave.length ? 'text-chalk' : 'text-wire-lit',
|
||||
].join(' ')}
|
||||
>
|
||||
{wave}
|
||||
</span>
|
||||
<span className="font-data text-[9px] uppercase tracking-[0.14em] text-dim">
|
||||
{inWave.length || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{inWave.length === 0 ? (
|
||||
<p className="border border-dashed border-wire p-2.5 font-data text-[10px] leading-relaxed text-dim">
|
||||
unused — waves are sparse by design, not sequential
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{inWave.map((app) => (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
app={app}
|
||||
state={state}
|
||||
active={selected === app.name}
|
||||
onSelect={() => setSelected(app.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
label={selectedApp ? selectedApp.name : 'No selection'}
|
||||
hint={selectedApp ? `wave ${selectedApp.wave ?? '—'} · ${selectedApp.resources} resources` : 'pick an app'}
|
||||
>
|
||||
{selectedApp ? (
|
||||
<div className="space-y-4">
|
||||
<dl className="grid grid-cols-2 gap-3 font-data text-[11px]">
|
||||
<div>
|
||||
<dt className="text-dim">sync</dt>
|
||||
<dd className={selectedApp.sync === 'synced' ? 'text-flux' : 'text-lamp'}>{selectedApp.sync}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-dim">health</dt>
|
||||
<dd className={selectedApp.health === 'healthy' ? 'text-flux' : 'text-rose'}>
|
||||
{selectedApp.health}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 font-data text-[10px] uppercase tracking-[0.16em] text-dim">Resources by kind</p>
|
||||
<ul className="space-y-1.5">
|
||||
{kinds.map((k) => (
|
||||
<li key={k.kind} className="flex items-center gap-3 font-data text-[11px]">
|
||||
<span className={k.named ? 'text-chalk' : 'text-dim'}>{k.kind}</span>
|
||||
<span className="h-px flex-1 bg-wire" />
|
||||
<span className="tabular-nums text-dim">{k.count}</span>
|
||||
{!k.named && (
|
||||
<span className="border border-wire-lit px-1 text-[9px] uppercase tracking-[0.1em] text-dim">
|
||||
count only
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="border-t border-wire pt-3 font-plex text-[11px] leading-relaxed text-dim">
|
||||
Secrets appear as counts. Names, source repositories, and condition messages are never sent to the
|
||||
browser.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="font-plex text-sm text-dim">Select an application to inspect its resources.</p>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { IBM_Plex_Mono, IBM_Plex_Sans, IBM_Plex_Sans_Condensed } from 'next/font/google'
|
||||
import { Rail } from '@/components/cluster/Rail'
|
||||
import { Ribbon } from '@/components/cluster/Ribbon'
|
||||
|
||||
const signage = IBM_Plex_Sans_Condensed({
|
||||
subsets: ['latin'],
|
||||
weight: ['600', '700'],
|
||||
variable: '--font-signage',
|
||||
})
|
||||
|
||||
const plex = IBM_Plex_Sans({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600'],
|
||||
variable: '--font-plex',
|
||||
})
|
||||
|
||||
const data = IBM_Plex_Mono({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500'],
|
||||
variable: '--font-data',
|
||||
})
|
||||
|
||||
export const metadata = {
|
||||
title: 'Cluster — Rock Liang',
|
||||
description: 'Live view of a Talos Kubernetes cluster: topology, GitOps delivery, and GPU inference.',
|
||||
}
|
||||
|
||||
export default function ClusterLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className={`${signage.variable} ${plex.variable} ${data.variable} min-h-screen bg-void font-plex text-chalk`}>
|
||||
<Ribbon />
|
||||
<div className="flex min-h-[calc(100vh-42px)] flex-col md:flex-row">
|
||||
<Rail />
|
||||
<main className="min-w-0 flex-1 p-4 md:p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import './globals.css'
|
||||
import Header from '@/components/Header'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Rock Liang — Portfolio & Live Infrastructure',
|
||||
description: 'Personal portfolio showcasing Kubernetes, Terraform, and cloud-native systems.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="bg-white dark:bg-gray-950 text-gray-900 dark:text-white">
|
||||
<Header />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { FeatureCard } from '@/components/FeatureCard'
|
||||
import { HeroBlobFlow } from '@/components/HeroBlobFlow'
|
||||
import { InteractiveTerminal } from '@/components/InteractiveTerminal'
|
||||
import { ExperienceTimeline } from '@/components/ExperienceTimeline'
|
||||
import {
|
||||
GitBranch,
|
||||
Database,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
Cpu,
|
||||
Layers,
|
||||
} from 'lucide-react'
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: 'Infrastructure Platform',
|
||||
description: 'Multi-region Terraform + Argo CD GitOps on Kubernetes',
|
||||
href: '/infrastructure',
|
||||
stat: '40% CPU reduction',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: 'Distributed Systems',
|
||||
description: 'gRPC, Kafka, AWS Step Functions across 57+ regions',
|
||||
href: '/systems',
|
||||
stat: 'Mission-critical',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: 'LLM Systems',
|
||||
description: 'CPU-bound inference optimization, INT4/INT8 quantization',
|
||||
href: '/llm',
|
||||
stat: '60% latency cut',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: 'Kafka Cluster',
|
||||
description: 'Strimzi KRaft cluster with auto-scaling brokers',
|
||||
href: '/kafka',
|
||||
stat: '3 brokers',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
title: 'Open Source',
|
||||
description: 'go-flink: distributed DataLakeHouse in Go',
|
||||
href: '/opensource',
|
||||
stat: 'Public repo',
|
||||
status: 'live' as const,
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Observability',
|
||||
description: 'Prometheus, Grafana, Dynatrace — live metrics',
|
||||
href: '/infrastructure',
|
||||
stat: '99.2% uptime',
|
||||
status: 'live' as const,
|
||||
},
|
||||
]
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
{/* Hero */}
|
||||
<section className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-950 dark:to-gray-900 py-24 px-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Avatar with flowing outcomes */}
|
||||
<HeroBlobFlow />
|
||||
|
||||
{/* Description */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 1 }}
|
||||
className="text-center mt-16"
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Full-Stack Systems Engineer
|
||||
</h1>
|
||||
<p className="text-xl text-gray-700 dark:text-gray-300 mb-6 font-medium">
|
||||
Infrastructure × Backend × LLM Systems
|
||||
</p>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10">
|
||||
Building observable, fault-tolerant systems from bare metal to cloud. Optimizing cost (infrastructure) × performance (inference) × reliability (SRE).
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4 justify-center flex-wrap mb-8">
|
||||
<a
|
||||
href="#features"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
|
||||
>
|
||||
Explore Systems
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/rockliang"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="border-2 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 transition"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 1.2 }}
|
||||
className="text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
💡 Press <kbd className="bg-gray-200 dark:bg-gray-800 px-2 py-1 rounded">Cmd+K</kbd> to explore via terminal
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Experience Timeline */}
|
||||
<ExperienceTimeline />
|
||||
|
||||
{/* Features Grid */}
|
||||
<section id="features" className="max-w-6xl mx-auto px-6 py-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-4xl font-bold mb-4">What I Build</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
Production systems spanning infrastructure, distributed backends, and LLM optimization. Click any domain to explore.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
{features.map((feature) => (
|
||||
<FeatureCard key={feature.title} {...feature} />
|
||||
))}
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 py-12 px-6">
|
||||
<div className="max-w-6xl mx-auto text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>© 2025 Rock Liang. Deployed on homelab Kubernetes cluster (3-node Talos).</p>
|
||||
<p className="mt-2">
|
||||
<a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 dark:hover:text-blue-400">
|
||||
GitHub
|
||||
</a>
|
||||
{' • '}
|
||||
<a href="mailto:[email protected]" className="hover:text-blue-600 dark:hover:text-blue-400">
|
||||
Email
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Interactive Terminal */}
|
||||
<InteractiveTerminal />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user