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

@@ -0,0 +1,129 @@
'use client'
import { useEffect, useState } from 'react'
import { Rocket, RefreshCw, ExternalLink, CheckCircle, Clock, AlertCircle, Cloud, Zap, Train } from 'lucide-react'
interface Deployment {
id: string; platform: 'vercel' | 'cloudflare' | 'railway'
project: string; url: string; state: string
branch: string; commit: string; createdAt: string; triggeredBy: string
}
const PLATFORM_COLORS: Record<string, string> = {
vercel: '#fff',
cloudflare: '#F5A623',
railway: '#9C27B0',
}
const PLATFORM_ICONS: Record<string, any> = { vercel: Cloud, cloudflare: Zap, railway: Train }
const STATE_COLORS: Record<string, string> = {
ready: '#22c55e', active: '#22c55e', succeeded: '#22c55e',
building: '#F5A623', queued: '#2979FF',
error: '#ef4444', failed: '#ef4444', cancelled: '#888',
}
const STATE_ICONS: Record<string, any> = {
ready: CheckCircle, active: CheckCircle, succeeded: CheckCircle,
building: Clock, queued: Clock, error: AlertCircle, failed: AlertCircle,
}
function timeAgo(iso: string) {
const diff = Date.now() - new Date(iso).getTime()
const m = Math.floor(diff / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24)
if (d > 0) return `${d}d ago`
if (h > 0) return `${h}h ago`
if (m > 0) return `${m}m ago`
return 'just now'
}
export default function DeploymentsPage() {
const [data, setData] = useState<{ deployments: Deployment[]; counts: any } | null>(null)
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState<string>('all')
const [refreshing, setRefreshing] = useState(false)
const load = async () => {
setRefreshing(true)
try {
const r = await fetch('/api/deployments')
setData(await r.json())
} finally { setRefreshing(false); setLoading(false) }
}
useEffect(() => { load() }, [])
const deployments = (data?.deployments || []).filter(d => filter === 'all' || d.platform === filter)
return (
<div style={{ padding: 32, maxWidth: 1100 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 28 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Rocket size={28} style={{ color: '#FF1D6C' }} />
<div>
<h1 style={{ fontSize: 24, fontWeight: 700, color: '#fff', margin: 0 }}>Deployments</h1>
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>
Vercel · Railway · Cloudflare
</p>
</div>
</div>
<button onClick={load} disabled={refreshing} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 14px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#aaa', fontSize: 13, cursor: 'pointer' }}>
<RefreshCw size={13} style={{ animation: refreshing ? 'spin 1s linear infinite' : 'none' }} />Refresh
</button>
</div>
{/* Platform counts */}
<div style={{ display: 'flex', gap: 10, marginBottom: 20, flexWrap: 'wrap' }}>
{['all', 'vercel', 'cloudflare', 'railway'].map(p => {
const count = p === 'all' ? data?.counts?.total : data?.counts?.[p]
const PlatIcon = p !== 'all' ? PLATFORM_ICONS[p] : Rocket
return (
<button key={p} onClick={() => setFilter(p)} style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '6px 14px',
background: filter === p ? 'rgba(255,29,108,0.15)' : 'rgba(255,255,255,0.04)',
border: `1px solid ${filter === p ? '#FF1D6C' : 'rgba(255,255,255,0.08)'}`,
borderRadius: 20, color: filter === p ? '#FF1D6C' : '#aaa', fontSize: 13, cursor: 'pointer',
}}>
<PlatIcon size={12} />
{p.charAt(0).toUpperCase() + p.slice(1)}
{count != null && <span style={{ opacity: 0.6 }}>({count})</span>}
</button>
)
})}
</div>
{loading ? (
<div style={{ color: 'rgba(255,255,255,0.3)', padding: 40, textAlign: 'center' }}>Loading deployments</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{deployments.map(dep => {
const PlatIcon = PLATFORM_ICONS[dep.platform] || Cloud
const StateIcon = STATE_ICONS[dep.state] || CheckCircle
const stateColor = STATE_COLORS[dep.state] || '#888'
const platColor = PLATFORM_COLORS[dep.platform] || '#aaa'
return (
<div key={dep.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 10 }}>
<PlatIcon size={16} style={{ color: platColor, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color: '#fff', fontWeight: 600, fontSize: 14 }}>{dep.project}</span>
<span style={{ color: 'rgba(255,255,255,0.3)', fontSize: 11, fontFamily: 'monospace', background: 'rgba(255,255,255,0.05)', padding: '1px 6px', borderRadius: 4 }}>{dep.branch}</span>
</div>
<div style={{ color: 'rgba(255,255,255,0.35)', fontSize: 12, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dep.commit}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<StateIcon size={13} style={{ color: stateColor }} />
<span style={{ color: stateColor, fontSize: 12, fontWeight: 600 }}>{dep.state}</span>
</div>
<div style={{ color: 'rgba(255,255,255,0.3)', fontSize: 12, minWidth: 70, textAlign: 'right' }}>{timeAgo(dep.createdAt)}</div>
<a href={dep.url} target="_blank" rel="noreferrer" style={{ color: 'rgba(255,255,255,0.3)', display: 'flex', alignItems: 'center' }}>
<ExternalLink size={13} />
</a>
</div>
)
})}
{deployments.length === 0 && (
<div style={{ color: 'rgba(255,255,255,0.3)', textAlign: 'center', padding: 40 }}>No deployments found for this filter</div>
)}
</div>
)}
</div>
)
}