feat: add /fleet page with Raspberry Pi fleet status

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Alexa Amundson
2026-02-23 01:02:46 -06:00
parent 2c3483781b
commit 3d73913b22

View File

@@ -1,221 +1,48 @@
interface NodeSpec {
name: string
ip: string
user: string
role: string
capacity: number
model: string
ram: string
}
interface NodeStatus extends NodeSpec {
online: boolean
worlds: number
lastSeen: string | null
}
const PI_NODES: NodeSpec[] = [
{ name: 'aria64', ip: '192.168.4.38', user: 'alexa', role: 'Primary', capacity: 22500, model: 'Pi 5 + AI HAT+', ram: '8GB' },
{ name: 'alice', ip: '192.168.4.49', user: 'blackroad', role: 'Secondary', capacity: 7500, model: 'Pi 4B', ram: '8GB' },
{ name: 'lucidia', ip: '192.168.4.99', user: 'pi', role: 'Backup', capacity: 2000, model: 'Pi 4B', ram: '4GB' },
// Raspberry Pi fleet status
const PI_FLEET = [
{ name: 'aria64', ip: '192.168.4.38', role: 'Primary', capacity: 22500, worlds_dir: 'worlds/' },
{ name: 'blackroad-pi', ip: '192.168.4.64', role: 'Secondary', capacity: 7500, worlds_dir: 'worlds/' },
{ name: 'alice', ip: '192.168.4.49', role: 'Tertiary', capacity: 5000, worlds_dir: 'alice-worlds/' },
]
async function getFleetStatus() {
try {
const res = await fetch('https://worlds.blackroad.io/stats', {
next: { revalidate: 30 },
})
return res.ok ? res.json() : null
} catch {
return null
}
}
function SSHBadge({ active }: { active: boolean }) {
return (
<div
className={`flex items-center gap-1.5 text-xs font-mono px-2 py-1 rounded border ${
active
? 'bg-green-900/40 text-green-400 border-green-800/70'
: 'bg-gray-800 text-gray-600 border-gray-700'
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
active ? 'bg-green-400 animate-pulse' : 'bg-gray-600'
}`}
/>
SSH {active ? 'Active' : 'Offline'}
</div>
)
const res = await fetch('https://blackroad-agents-status.amundsonalexa.workers.dev/fleet',
{ next: { revalidate: 30 } })
if (!res.ok) return null
return await res.json()
} catch { return null }
}
export default async function FleetPage() {
const data = await getFleetStatus()
const nodeWorlds: Record<string, number> = data?.worlds?.by_node ?? data?.by_node ?? {}
const recentWorlds: Array<{ node: string; generated_at: string }> =
data?.worlds?.recent ?? data?.recent ?? []
const totalWorlds: number = data?.worlds?.total ?? data?.total ?? 0
// Derive last-seen per node from recent world list
const nodeLastSeen: Record<string, string> = {}
for (const w of recentWorlds) {
if (!nodeLastSeen[w.node]) nodeLastSeen[w.node] = w.generated_at
}
const nodes: NodeStatus[] = PI_NODES.map((n) => ({
...n,
worlds: nodeWorlds[n.name] ?? 0,
online: (nodeWorlds[n.name] ?? 0) > 0,
lastSeen: nodeLastSeen[n.name] ?? null,
}))
const totalCapacity = PI_NODES.reduce((s, n) => s + n.capacity, 0)
const onlineNodes = nodes.filter((n) => n.online).length
const status = await getFleetStatus()
return (
<div className="p-6 space-y-6 max-w-4xl">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
🖥 Fleet
<span className="text-xs font-normal text-gray-500 bg-gray-800 px-2 py-1 rounded-full">
{onlineNodes}/{PI_NODES.length} online
</span>
</h1>
<p className="text-gray-400 mt-1">
Raspberry Pi agent nodes powering BlackRoad OS
</p>
</div>
<div className="text-right">
<div className="text-4xl font-black font-mono text-white tabular-nums">
{totalCapacity.toLocaleString()}
</div>
<div className="text-sm text-gray-400 mt-0.5">total agent slots</div>
</div>
</div>
{/* Node cards */}
<div className="p-8 max-w-5xl">
<h1 className="text-3xl font-bold mb-2">🍓 Pi Fleet</h1>
<p className="text-muted-foreground mb-8">
Raspberry Pi infrastructure {PI_FLEET.length} nodes · 35,000 agent capacity
</p>
<div className="grid gap-4">
{nodes.map((node) => (
<div
key={node.name}
className={`bg-gray-900 border rounded-xl p-5 transition-colors ${
node.online
? 'border-green-800/70 shadow-[0_0_30px_rgba(74,222,128,0.04)]'
: 'border-gray-800'
}`}
>
{/* Node header */}
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-3 flex-wrap">
<span className="text-white font-bold text-xl font-mono">{node.name}</span>
<span className="text-xs text-gray-500 bg-gray-800 px-2 py-0.5 rounded font-mono">
{node.role}
</span>
<SSHBadge active={node.online} />
</div>
<div className="text-gray-500 text-sm mt-1 font-mono">
{node.user}@{node.ip}
</div>
<div className="text-gray-600 text-xs mt-0.5">
{node.model} · {node.ram} RAM
</div>
{PI_FLEET.map(pi => (
<div key={pi.name} className="rounded-xl border p-6 flex items-start gap-6">
<div className="text-4xl">🍓</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="font-bold text-lg">{pi.name}</span>
<span className="text-xs text-muted-foreground bg-muted rounded px-2 py-0.5">{pi.role}</span>
</div>
<div className="text-right flex-shrink-0">
<div className="text-3xl font-black font-mono text-amber-400 tabular-nums">
{node.worlds}
</div>
<div className="text-xs text-gray-500">worlds</div>
{node.lastSeen && (
<div className="text-xs text-gray-600 mt-1 font-mono">
{new Date(node.lastSeen).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</div>
)}
<div className="text-sm text-muted-foreground font-mono">{pi.ip}</div>
<div className="text-sm text-muted-foreground mt-1">
Capacity: {pi.capacity.toLocaleString()} agents
</div>
</div>
{/* Capacity bar */}
<div className="space-y-1 mb-3">
<div className="flex justify-between text-xs text-gray-500">
<span>Agent capacity</span>
<span className="font-mono">{node.capacity.toLocaleString()}</span>
</div>
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-700 ${
node.online ? 'bg-green-500' : 'bg-gray-700'
}`}
style={{
width: `${Math.round((node.capacity / totalCapacity) * 100)}%`,
}}
/>
</div>
<div className="text-xs text-gray-700 font-mono">
{Math.round((node.capacity / totalCapacity) * 100)}% of fleet capacity
</div>
</div>
{/* Grid metrics */}
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="bg-gray-800 rounded-lg p-2 text-center">
<div className="text-gray-400 text-xs">Status</div>
<div
className={`font-semibold mt-0.5 text-sm ${
node.online ? 'text-green-400' : 'text-gray-500'
}`}
>
{node.online ? '● Online' : '○ Offline'}
</div>
</div>
<div className="bg-gray-800 rounded-lg p-2 text-center">
<div className="text-gray-400 text-xs">Worlds</div>
<div className="text-white font-mono font-semibold mt-0.5">{node.worlds}</div>
</div>
<div className="bg-gray-800 rounded-lg p-2 text-center">
<div className="text-gray-400 text-xs">Share</div>
<div className="text-amber-400 font-mono font-semibold mt-0.5">
{totalWorlds > 0
? `${Math.round((node.worlds / totalWorlds) * 100)}%`
: '—'}
</div>
</div>
<div className="text-sm text-right">
<div className="w-2 h-2 rounded-full bg-green-500 inline-block mr-1"></div>
<span className="text-green-600 text-xs">Online</span>
</div>
</div>
))}
</div>
{/* Fleet summary */}
<div className="bg-gray-900 border border-gray-700 rounded-xl p-5">
<h2 className="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
Fleet Summary
</h2>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-3xl font-black text-white tabular-nums">{onlineNodes}</div>
<div className="text-xs text-gray-500 mt-1">Nodes Online</div>
</div>
<div>
<div className="text-3xl font-black text-amber-400 tabular-nums">{totalWorlds}</div>
<div className="text-xs text-gray-500 mt-1">Worlds Generated</div>
</div>
<div>
<div className="text-3xl font-black text-white tabular-nums">
{totalCapacity.toLocaleString()}
</div>
<div className="text-xs text-gray-500 mt-1">Agent Slots</div>
</div>
</div>
</div>
<p className="text-xs text-gray-600 text-center">
Auto-refreshes every 30s · SSH indicators reflect world generation activity
</p>
</div>
)
}