MotorSignal-App/website/components/ops/operations-infrastructure.tsx

625 lines
26 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
OperationsInfrastructureData,
ServiceStatusValue,
ComponentHealthStatus,
OperationsRole,
} from './types';
import { normalizeInfrastructureResponse } from '../../lib/ops-data';
import { OperationsActions } from './operations-actions';
interface OperationsInfrastructureProps {
role: OperationsRole;
}
export function OperationsInfrastructure({ role }: OperationsInfrastructureProps) {
const [data, setData] = useState<OperationsInfrastructureData | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastFetchedAt, setLastFetchedAt] = useState<string | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
const [copiedSha, setCopiedSha] = useState(false);
const isFetchingRef = useRef(false);
const pollTimerRef = useRef<NodeJS.Timeout | null>(null);
const fetchData = useCallback(async () => {
if (isFetchingRef.current) return;
isFetchingRef.current = true;
setIsRefreshing(true);
try {
const resp = await fetch('/api/ops/infrastructure', {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (resp.ok) {
const raw = await resp.json();
const normalized = normalizeInfrastructureResponse(raw);
setData(normalized);
setLastFetchedAt(new Date().toLocaleTimeString());
setFetchError(null);
} else if (resp.status === 401 || resp.status === 403) {
setFetchError('Authorization failed. Access restricted.');
} else {
const raw = await resp.json().catch(() => null);
setData(normalizeInfrastructureResponse(raw));
setFetchError('Metrics collector degraded or partially unavailable.');
}
} catch {
setFetchError('Network error reaching infrastructure endpoint.');
} finally {
isFetchingRef.current = false;
setIsRefreshing(false);
}
}, []);
// Initial fetch and 30-second visibility-gated polling loop
useEffect(() => {
void fetchData();
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
void fetchData();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
pollTimerRef.current = setInterval(() => {
if (document.visibilityState === 'visible') {
void fetchData();
}
}, 30000);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
}
};
}, [fetchData]);
const handleCopySha = (sha: string) => {
if (!sha) return;
navigator.clipboard.writeText(sha).then(() => {
setCopiedSha(true);
setTimeout(() => setCopiedSha(false), 2000);
});
};
const formatBytes = (bytes: number | null): string => {
if (bytes === null || bytes === undefined) return 'N/A';
if (bytes >= 1e12) return `${(bytes / 1e12).toFixed(2)} TB`;
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
return `${(bytes / 1e3).toFixed(1)} KB`;
};
const getServiceStatusBadge = (status: ServiceStatusValue) => {
switch (status) {
case 'active':
return {
label: 'Active (Running)',
icon: 'check_circle',
classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60',
};
case 'inactive':
return {
label: 'Inactive (Stopped)',
icon: 'pause_circle',
classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60',
};
case 'failed':
return {
label: 'Failed',
icon: 'cancel',
classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60',
};
case 'unknown':
default:
return {
label: 'Unknown / Unreachable',
icon: 'help',
classes: 'bg-slate-900/60 text-slate-400 border-slate-700/60',
};
}
};
const getComponentHealthBadge = (status: ComponentHealthStatus) => {
switch (status) {
case 'available':
return {
label: 'Available',
icon: 'check_circle',
classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60',
};
case 'degraded':
return {
label: 'Degraded',
icon: 'warning',
classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60',
};
case 'unavailable':
return {
label: 'Unavailable',
icon: 'cancel',
classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60',
};
case 'unknown':
default:
return {
label: 'Unknown',
icon: 'help',
classes: 'bg-slate-900/60 text-slate-400 border-slate-700/60',
};
}
};
const resources = data?.resources;
const services = data?.services ?? [];
const network = data?.network;
const deployment = data?.deployment;
return (
<div className="space-y-8">
{/* Header & Controls */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-outline-variant/20">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="material-symbols-outlined text-primary-container text-2xl">
dns
</span>
<h1 className="text-xl font-bold tracking-tight text-on-surface">
Infrastructure & Runtime
</h1>
<span className="px-2 py-0.5 rounded text-[11px] font-mono bg-surface-container-high border border-outline-variant/30 text-primary-fixed-dim">
Phase 2 Operations
</span>
</div>
<p className="text-xs text-on-surface-variant">
Host system resources, systemd core services, network totals, and deployment runtime health.
</p>
</div>
<div className="flex items-center gap-3">
{/* Polling indicator */}
<div className="flex items-center gap-1.5 text-[11px] text-on-surface-variant font-mono bg-surface-container-low px-2.5 py-1.5 rounded-lg border border-outline-variant/20">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
<span>Auto 30s</span>
</div>
{/* Last refreshed */}
{lastFetchedAt && (
<span className="text-[11px] text-outline font-mono hidden md:inline">
Updated: {lastFetchedAt}
</span>
)}
{/* Manual Refresh Button */}
<button
type="button"
onClick={() => void fetchData()}
disabled={isRefreshing}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold bg-surface-container-high hover:bg-surface-container-highest border border-outline-variant/40 text-on-surface disabled:opacity-50 transition-all focus-visible:ring-2 focus-visible:ring-primary-container"
>
<span
className={`material-symbols-outlined text-[16px] ${
isRefreshing ? 'animate-spin' : ''
}`}
>
refresh
</span>
<span>{isRefreshing ? 'Refreshing...' : 'Refresh'}</span>
</button>
</div>
</div>
{/* Optional Warning Banner */}
{fetchError && (
<div className="p-3.5 rounded-lg bg-amber-950/40 border border-amber-800/50 flex items-center gap-2.5 text-xs text-amber-300">
<span className="material-symbols-outlined text-amber-400 text-base">
warning
</span>
<span>{fetchError}</span>
</div>
)}
{/* SECTION 1: System Resources */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-bold uppercase tracking-wider text-outline">
Server Resources
</h2>
<span className="text-[10px] text-outline font-mono">
Target Host: Ubuntu VPS
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
{/* CPU Usage */}
<div className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-on-surface-variant">CPU Usage</span>
<span className="material-symbols-outlined text-base text-primary-fixed-dim">
memory
</span>
</div>
<div>
<div className="text-2xl font-bold font-mono tracking-tight text-on-surface">
{resources?.cpu_percent !== null && resources?.cpu_percent !== undefined
? `${resources.cpu_percent}%`
: 'Unavailable'}
</div>
<p className="text-[11px] text-outline font-mono mt-0.5">
Load: {resources?.load_avg ? resources.load_avg.join(', ') : 'N/A'}
</p>
</div>
{/* Progress Bar */}
<div className="w-full bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div
className="bg-primary-container h-full rounded-full transition-all duration-300"
style={{ width: `${Math.min(100, Math.max(0, resources?.cpu_percent ?? 0))}%` }}
/>
</div>
</div>
{/* Memory (RAM) */}
<div className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-on-surface-variant">Memory (RAM)</span>
<span className="material-symbols-outlined text-base text-primary-fixed-dim">
developer_board
</span>
</div>
<div>
<div className="text-xl font-bold font-mono tracking-tight text-on-surface">
{resources?.memory
? `${formatBytes(resources.memory.used)} / ${formatBytes(resources.memory.total)}`
: 'Unavailable'}
</div>
<p className="text-[11px] text-outline font-mono mt-0.5">
{resources?.memory
? `${resources.memory.percent}% used (${formatBytes(resources.memory.available)} free)`
: 'N/A'}
</p>
</div>
{/* Progress Bar */}
<div className="w-full bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${
(resources?.memory?.percent ?? 0) >= 90
? 'bg-rose-500'
: (resources?.memory?.percent ?? 0) >= 80
? 'bg-amber-500'
: 'bg-primary-container'
}`}
style={{ width: `${Math.min(100, Math.max(0, resources?.memory?.percent ?? 0))}%` }}
/>
</div>
</div>
{/* Swap Memory */}
<div className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-on-surface-variant">Swap Space</span>
<span className="material-symbols-outlined text-base text-primary-fixed-dim">
swap_horiz
</span>
</div>
<div>
<div className="text-xl font-bold font-mono tracking-tight text-on-surface">
{resources?.swap
? `${formatBytes(resources.swap.used)} / ${formatBytes(resources.swap.total)}`
: 'Unavailable'}
</div>
<p className="text-[11px] text-outline font-mono mt-0.5">
{resources?.swap ? `${resources.swap.percent}% utilized` : 'N/A'}
</p>
</div>
{/* Progress Bar */}
<div className="w-full bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div
className="bg-primary-container/70 h-full rounded-full transition-all duration-300"
style={{ width: `${Math.min(100, Math.max(0, resources?.swap?.percent ?? 0))}%` }}
/>
</div>
</div>
{/* Disk Storage (/) */}
<div className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<span className="text-xs font-medium text-on-surface-variant">Root Disk (/)</span>
{resources?.disk?.critical ? (
<span className="px-1.5 py-0.2 rounded text-[9px] font-mono font-bold bg-rose-950 text-rose-300 border border-rose-800">
CRITICAL
</span>
) : resources?.disk?.warning ? (
<span className="px-1.5 py-0.2 rounded text-[9px] font-mono font-bold bg-amber-950 text-amber-300 border border-amber-800">
WARNING
</span>
) : null}
</div>
<span className="material-symbols-outlined text-base text-primary-fixed-dim">
hard_drive
</span>
</div>
<div>
<div className="text-xl font-bold font-mono tracking-tight text-on-surface">
{resources?.disk
? `${formatBytes(resources.disk.used)} / ${formatBytes(resources.disk.total)}`
: 'Unavailable'}
</div>
<p className="text-[11px] text-outline font-mono mt-0.5">
{resources?.disk
? `${resources.disk.percent}% used (${formatBytes(resources.disk.available)} free)`
: 'N/A'}
</p>
</div>
{/* Progress Bar */}
<div className="w-full bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${
resources?.disk?.critical
? 'bg-rose-500'
: resources?.disk?.warning
? 'bg-amber-500'
: 'bg-primary-container'
}`}
style={{ width: `${Math.min(100, Math.max(0, resources?.disk?.percent ?? 0))}%` }}
/>
</div>
</div>
{/* Server Uptime */}
<div className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-on-surface-variant">Server Uptime</span>
<span className="material-symbols-outlined text-base text-primary-fixed-dim">
schedule
</span>
</div>
<div>
<div className="text-lg font-bold font-mono tracking-tight text-on-surface truncate">
{resources?.uptime_formatted || 'Unavailable'}
</div>
<p className="text-[11px] text-outline font-mono mt-0.5">
{resources?.uptime_seconds
? `${resources.uptime_seconds.toLocaleString()}s total uptime`
: 'N/A'}
</p>
</div>
<div className="text-[10px] text-primary-fixed-dim font-mono">
System running continuously
</div>
</div>
</div>
</div>
{/* SECTION 2: Core Systemd Services */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-bold uppercase tracking-wider text-outline">
Core Services (systemd)
</h2>
<span className="text-[10px] text-outline font-mono">
Strict Allowlist: 4 Services
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{services.map((svc) => {
const badge = getServiceStatusBadge(svc.status);
return (
<div
key={svc.name}
className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 flex flex-col justify-between space-y-3"
>
<div>
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="text-sm font-semibold text-on-surface">
{svc.display_name}
</h3>
<p className="text-[11px] font-mono text-outline mt-0.5">
{svc.name}.service
</p>
</div>
</div>
<p className="text-xs text-on-surface-variant mt-2 leading-relaxed">
{svc.description}
</p>
</div>
<div className="pt-3 border-t border-outline-variant/10 flex items-center justify-between">
<span className="text-[11px] text-outline">Service Status</span>
<div
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-mono font-medium border ${badge.classes}`}
>
<span className="material-symbols-outlined text-[14px]">
{badge.icon}
</span>
<span>{badge.label}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* SECTION 3 & 4: Network Cumulative Totals & Deployment Health */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Network Cumulative Totals */}
<div className="p-5 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container text-xl">
network_check
</span>
<div>
<h3 className="text-sm font-bold text-on-surface">
Network Cumulative Totals
</h3>
<p className="text-[11px] text-outline">
Aggregated across non-loopback interfaces since host boot
</p>
</div>
</div>
<span className="px-2 py-0.5 rounded text-[10px] font-mono bg-surface-container-high text-primary-fixed-dim border border-outline-variant/30">
Not a rate
</span>
</div>
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/15 space-y-1">
<div className="flex items-center gap-1.5 text-xs text-on-surface-variant">
<span className="material-symbols-outlined text-[15px] text-emerald-400">
download
</span>
<span>Total Received</span>
</div>
<div className="text-xl font-bold font-mono text-on-surface">
{formatBytes(network?.bytes_received ?? null)}
</div>
<p className="text-[10px] text-outline font-mono">
{network?.bytes_received ? `${network.bytes_received.toLocaleString()} bytes` : 'N/A'}
</p>
</div>
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/15 space-y-1">
<div className="flex items-center gap-1.5 text-xs text-on-surface-variant">
<span className="material-symbols-outlined text-[15px] text-primary-container">
upload
</span>
<span>Total Transmitted</span>
</div>
<div className="text-xl font-bold font-mono text-on-surface">
{formatBytes(network?.bytes_transmitted ?? null)}
</div>
<p className="text-[10px] text-outline font-mono">
{network?.bytes_transmitted ? `${network.bytes_transmitted.toLocaleString()} bytes` : 'N/A'}
</p>
</div>
</div>
<div className="p-2.5 rounded-lg bg-surface-container-lowest/60 border border-outline-variant/10 text-[11px] text-outline flex items-center gap-2">
<span className="material-symbols-outlined text-[14px] text-primary-fixed-dim">
info
</span>
<span>
Cumulative Total Since Boot across physical network interfaces. Instantaneous bandwidth rates are not tracked.
</span>
</div>
</div>
{/* Deployment Health Foundations */}
<div className="p-5 rounded-xl bg-surface-container-low border border-outline-variant/20 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container text-xl">
deployed_code
</span>
<div>
<h3 className="text-sm font-bold text-on-surface">
Deployment Health Foundations
</h3>
<p className="text-[11px] text-outline">
Active code revision and subsystem runtime readiness
</p>
</div>
</div>
<span className="px-2 py-0.5 rounded text-[10px] font-mono bg-surface-container-high text-on-surface-variant border border-outline-variant/30">
Foundation
</span>
</div>
<div className="space-y-3 pt-1">
{/* Active Git Revision */}
<div className="p-3 rounded-lg bg-surface-container-lowest border border-outline-variant/15 flex items-center justify-between">
<div>
<div className="text-xs text-on-surface-variant">Active Git Revision</div>
<div className="flex items-center gap-2 mt-1">
<span className="font-mono text-sm font-bold text-on-surface">
{deployment?.short_revision || 'Unavailable'}
</span>
{deployment?.git_revision && (
<button
type="button"
onClick={() => handleCopySha(deployment.git_revision!)}
title="Copy full SHA"
className="p-1 rounded text-outline hover:text-on-surface hover:bg-surface-container transition-colors"
>
<span className="material-symbols-outlined text-[14px]">
{copiedSha ? 'check' : 'content_copy'}
</span>
</button>
)}
</div>
</div>
<div className="flex items-center gap-1.5 font-mono text-[11px]">
<span
className={`w-2 h-2 rounded-full ${
deployment?.revision_status === 'known' ? 'bg-emerald-400' : 'bg-slate-500'
}`}
/>
<span className="capitalize text-outline">
{deployment?.revision_status ?? 'unavailable'}
</span>
</div>
</div>
{/* Health indicators */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-surface-container-lowest border border-outline-variant/15">
<div className="text-xs text-on-surface-variant mb-1">Website Ingress</div>
{(() => {
const badge = getComponentHealthBadge(deployment?.website_health ?? 'unknown');
return (
<div
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-mono border ${badge.classes}`}
>
<span className="material-symbols-outlined text-[12px]">{badge.icon}</span>
<span>{badge.label}</span>
</div>
);
})()}
</div>
<div className="p-3 rounded-lg bg-surface-container-lowest border border-outline-variant/15">
<div className="text-xs text-on-surface-variant mb-1">API Readiness</div>
{(() => {
const badge = getComponentHealthBadge(deployment?.api_health ?? 'unknown');
return (
<div
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-mono border ${badge.classes}`}
>
<span className="material-symbols-outlined text-[12px]">{badge.icon}</span>
<span>{badge.label}</span>
</div>
);
})()}
</div>
</div>
{/* Deployment History Status */}
<div className="p-2.5 rounded-lg bg-surface-container-lowest/60 border border-outline-variant/10 text-[11px] text-outline flex items-center justify-between">
<span className="font-mono">Deployment History:</span>
<span className="text-primary-fixed-dim font-mono">Audited in Phase 2</span>
</div>
</div>
</div>
</div>
{/* Operations Actions & Deployment Controls */}
<div className="mt-8 pt-6 border-t border-outline-variant/20">
<OperationsActions role={role} onActionCompleted={fetchData} />
</div>
</div>
);
}