feat: real-time live data integration

- lib/live-data.ts: Shared TypeScript client for blackroad-live-data Worker
- components/live-stats.tsx: LiveStatsBar, RecentRepos, AgentStatusGrid components
- app/page.tsx: Import LiveStatsBar in main page header

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Alexa Amundson
2026-02-24 14:18:59 -06:00
parent 263f9f171e
commit 458c2c044b
97 changed files with 8715 additions and 1701 deletions

View File

@@ -1,48 +1,168 @@
// 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/' },
]
'use client';
async function getFleetStatus() {
try {
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 }
import { useState, useEffect } from 'react';
import { Cpu, Globe, Wifi, WifiOff, Activity, Zap, Server, ArrowRight } from 'lucide-react';
import Link from 'next/link';
interface Node {
name: string; ip: string; role: string; capacity: number;
model: string; status: string; latencyMs: number; services: string[];
}
interface TunnelRoute { hostname: string; service: string; pi: string; group: string; }
interface FleetData { nodes: Node[]; summary: { total_nodes: number; online_nodes: number; total_capacity: number; tunnel_routes: number }; }
interface TunnelData { routes: TunnelRoute[]; by_pi: Record<string, TunnelRoute[]>; tunnel_id: string; }
const ROLE_COLORS: Record<string, string> = {
PRIMARY: '#FF1D6C', SECONDARY: '#2979FF', TERTIARY: '#F5A623', IDENTITY: '#9C27B0',
};
const ROLE_GLOW: Record<string, string> = {
PRIMARY: 'shadow-[0_0_20px_rgba(255,29,108,0.3)]',
SECONDARY: 'shadow-[0_0_20px_rgba(41,121,255,0.3)]',
TERTIARY: 'shadow-[0_0_20px_rgba(245,166,35,0.3)]',
IDENTITY: 'shadow-[0_0_20px_rgba(156,39,176,0.3)]',
};
export default function FleetPage() {
const [fleet, setFleet] = useState<FleetData | null>(null);
const [tunnel, setTunnel] = useState<TunnelData | null>(null);
const [loading, setLoading] = useState(true);
const [lastChecked, setLastChecked] = useState('');
const load = async () => {
try {
const [f, t] = await Promise.all([fetch('/api/fleet'), fetch('/api/tunnel')]);
if (f.ok) setFleet(await f.json());
if (t.ok) setTunnel(await t.json());
setLastChecked(new Date().toLocaleTimeString());
} catch {}
setLoading(false);
};
useEffect(() => { load(); const iv = setInterval(load, 30000); return () => clearInterval(iv); }, []);
const onlineCount = fleet?.summary.online_nodes ?? 0;
const totalCapacity = fleet?.summary.total_capacity ?? 30000;
const tunnelRoutes = fleet?.summary.tunnel_routes ?? 14;
export default async function FleetPage() {
const status = await getFleetStatus()
return (
<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">
{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-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>
<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 className="min-h-screen bg-black text-white p-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Pi Fleet</h1>
<p className="text-gray-400 mt-1">Cloudflare Tunnel Raspberry Pi Network</p>
</div>
<button onClick={load} className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-sm text-gray-400 transition-all">
<Activity className="w-4 h-4" />
Refresh {lastChecked && <span className="text-gray-600">{lastChecked}</span>}
</button>
</div>
</div>
{/* Stats bar */}
<div className="grid grid-cols-4 gap-4 mb-8">
{[
{ label: 'Nodes', value: `${onlineCount}/${fleet?.summary.total_nodes ?? 4}`, icon: Server, color: '#22c55e' },
{ label: 'Agent Slots', value: totalCapacity.toLocaleString(), icon: Cpu, color: '#FF1D6C' },
{ label: 'Tunnel Routes', value: tunnelRoutes, icon: Globe, color: '#2979FF' },
{ label: 'Tunnel ID', value: '8ae67ab0', icon: Zap, color: '#F5A623' },
].map(s => (
<div key={s.label} className="rounded-2xl bg-white/5 border border-white/10 p-5">
<div className="flex items-center gap-3 mb-2">
<s.icon className="w-5 h-5" style={{ color: s.color }} />
<span className="text-sm text-gray-400">{s.label}</span>
</div>
<p className="text-2xl font-bold font-mono" style={{ color: s.color }}>{s.value}</p>
</div>
))}
</div>
{/* Node cards */}
<div className="grid grid-cols-2 gap-6 mb-8">
{(fleet?.nodes ?? []).map(node => {
const routes = tunnel?.by_pi[node.name] ?? [];
const color = ROLE_COLORS[node.role] ?? '#fff';
const glow = ROLE_GLOW[node.role] ?? '';
return (
<div key={node.name} className={`rounded-2xl bg-white/5 border border-white/10 p-6 ${glow} transition-all`}>
{/* Node header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
{node.status === 'online'
? <Wifi className="w-5 h-5 text-green-400" />
: <WifiOff className="w-5 h-5 text-gray-600" />}
<div>
<h3 className="font-bold text-lg">{node.name}</h3>
<p className="text-sm text-gray-500">{node.ip} · {node.model}</p>
</div>
</div>
<div className="flex flex-col items-end gap-1">
<span className="text-xs font-bold px-2 py-1 rounded-full" style={{ backgroundColor: color + '22', color }}>
{node.role}
</span>
{node.capacity > 0 && (
<span className="text-xs text-gray-500">{node.capacity.toLocaleString()} slots</span>
)}
</div>
</div>
{/* Latency */}
<div className="flex items-center gap-2 mb-4">
<div className={`w-2 h-2 rounded-full ${node.status === 'online' ? 'bg-green-400 animate-pulse' : 'bg-gray-600'}`} />
<span className="text-xs text-gray-400">
{node.status === 'online'
? node.latencyMs > 0 ? `${node.latencyMs}ms` : 'online'
: 'offline / unreachable'}
</span>
</div>
{/* Routes */}
{routes.length > 0 && (
<div className="space-y-1">
<p className="text-xs text-gray-600 uppercase tracking-wider mb-2">Tunnel Routes</p>
{routes.map(r => (
<div key={r.hostname} className="flex items-center gap-2 text-sm">
<ArrowRight className="w-3 h-3 flex-shrink-0" style={{ color }} />
<a href={`https://${r.hostname}`} target="_blank" rel="noreferrer"
className="hover:underline truncate" style={{ color }}>
{r.hostname}
</a>
<span className="text-gray-600 text-xs ml-auto flex-shrink-0">
{r.service.split(':').pop()}
</span>
</div>
))}
</div>
)}
</div>
);
})}
</div>
{/* All routes table */}
{tunnel && (
<div className="rounded-2xl bg-white/5 border border-white/10 p-6">
<h2 className="text-lg font-bold mb-4">All Tunnel Routes ({tunnel.routes.length})</h2>
<div className="space-y-2">
{tunnel.routes.map(r => (
<div key={r.hostname} className="flex items-center gap-4 py-2 border-b border-white/5 last:border-0">
<div className="w-2 h-2 rounded-full bg-green-400 flex-shrink-0" />
<a href={`https://${r.hostname}`} target="_blank" rel="noreferrer"
className="text-sm font-mono hover:text-[#FF1D6C] transition-colors flex-1">
{r.hostname}
</a>
<span className="text-sm text-gray-500 font-mono">{r.service}</span>
<span className="text-xs px-2 py-0.5 rounded-full"
style={{ backgroundColor: (ROLE_COLORS[r.group] ?? '#888') + '22', color: ROLE_COLORS[r.group] ?? '#888' }}>
{r.pi}
</span>
</div>
))}
</div>
</div>
)}
</div>
)
);
}