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:
146
app/(app)/network/page.tsx
Normal file
146
app/(app)/network/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowRight, Globe, Server, Cpu, Shield, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface TunnelRoute { hostname: string; service: string; pi: string; group: string; }
|
||||
interface Node { name: string; ip: string; role: string; capacity: number; status: string; services: string[]; }
|
||||
|
||||
const GROUP_COLORS: Record<string, string> = {
|
||||
PRIMARY: '#FF1D6C', SECONDARY: '#2979FF', TERTIARY: '#F5A623', IDENTITY: '#9C27B0',
|
||||
};
|
||||
|
||||
const PI_ICONS: Record<string, typeof Server> = {
|
||||
aria64: Cpu, 'blackroad-pi': Server, alice: Shield, cecilia: Globe,
|
||||
};
|
||||
|
||||
export default function NetworkPage() {
|
||||
const [tunnel, setTunnel] = useState<{ routes: TunnelRoute[]; by_pi: Record<string, TunnelRoute[]>; tunnel_id: string } | null>(null);
|
||||
const [fleet, setFleet] = useState<{ nodes: Node[] } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [t, f] = await Promise.all([fetch('/api/tunnel'), fetch('/api/fleet')]);
|
||||
if (t.ok) setTunnel(await t.json());
|
||||
if (f.ok) setFleet(await f.json());
|
||||
} catch {}
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const nodes = fleet?.nodes ?? [];
|
||||
const byPi = tunnel?.by_pi ?? {};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-8">
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Network</h1>
|
||||
<p className="text-gray-400 mt-1">Cloudflare Edge → Tunnel → Pi Fleet → Services</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">
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tunnel header */}
|
||||
<div className="rounded-2xl bg-gradient-to-r from-white/5 to-white/3 border border-white/10 p-6 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 via-[#FF1D6C] to-violet-600 flex items-center justify-center flex-shrink-0">
|
||||
<Globe className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-400 uppercase tracking-wider">Cloudflare Tunnel</p>
|
||||
<p className="font-mono text-white font-bold">{tunnel?.tunnel_id ?? '8ae67ab0-71fb-4461-befc-a91302369a7e'}</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{tunnel?.routes.length ?? 14} routes · QUIC protocol · Dallas edge</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
||||
<span className="text-sm text-green-400">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Topology grid */}
|
||||
<div className="flex items-start gap-4 mb-8 overflow-x-auto pb-4">
|
||||
{/* CF Edge */}
|
||||
<div className="flex-shrink-0 rounded-2xl bg-white/5 border border-white/10 p-5 w-52">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-3">Cloudflare Edge</p>
|
||||
<div className="space-y-1">
|
||||
{['*.blackroad.io', '*.blackroad.ai', 'CNAME → tunnel'].map(d => (
|
||||
<div key={d} className="text-xs text-gray-400 font-mono bg-white/5 rounded px-2 py-1">{d}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<div className="flex-shrink-0 flex items-center justify-center h-full pt-10">
|
||||
<ArrowRight className="w-6 h-6 text-[#FF1D6C]" />
|
||||
</div>
|
||||
|
||||
{/* Pi nodes */}
|
||||
{nodes.map((node) => {
|
||||
const Icon = PI_ICONS[node.name] ?? Server;
|
||||
const color = GROUP_COLORS[node.role] ?? '#888';
|
||||
const routes = byPi[node.name] ?? [];
|
||||
return (
|
||||
<div key={node.name} className="flex-shrink-0" style={{ minWidth: '220px' }}>
|
||||
<div className="rounded-2xl border p-5 h-full" style={{ borderColor: color + '44', backgroundColor: color + '0d' }}>
|
||||
{/* Node header */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center" style={{ backgroundColor: color + '22' }}>
|
||||
<Icon className="w-5 h-5" style={{ color }} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-sm">{node.name}</p>
|
||||
<p className="text-xs text-gray-500">{node.ip}</p>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<div className={`w-2 h-2 rounded-full ${node.status === 'online' ? 'bg-green-400 animate-pulse' : 'bg-gray-600'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-bold px-2 py-0.5 rounded-full inline-block mb-3" style={{ backgroundColor: color + '22', color }}>
|
||||
{node.role} {node.capacity > 0 && `· ${(node.capacity/1000).toFixed(1)}k slots`}
|
||||
</div>
|
||||
|
||||
{/* Routes */}
|
||||
<div className="space-y-1">
|
||||
{routes.map(r => (
|
||||
<div key={r.hostname} className="flex items-center gap-1.5 text-xs">
|
||||
<ArrowRight className="w-2.5 h-2.5 flex-shrink-0" style={{ color }} />
|
||||
<a href={`https://${r.hostname}`} target="_blank" rel="noreferrer"
|
||||
className="font-mono truncate hover:underline" style={{ color }}>
|
||||
{r.hostname}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
{routes.length === 0 && (
|
||||
<p className="text-xs text-gray-600 italic">No tunnel routes</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Stats footer */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Total Routes', value: tunnel?.routes.length ?? 14, color: '#FF1D6C' },
|
||||
{ label: 'Pi Nodes', value: nodes.length || 4, color: '#2979FF' },
|
||||
{ label: 'Online', value: nodes.filter(n => n.status === 'online').length || '—', color: '#22c55e' },
|
||||
{ label: 'Agent Slots', value: '30,000', color: '#9C27B0' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="rounded-xl bg-white/5 border border-white/10 px-4 py-3 flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">{s.label}</span>
|
||||
<span className="font-bold font-mono" style={{ color: s.color }}>{s.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user