733 lines
32 KiB
TypeScript
733 lines
32 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
OperationsInfrastructureData,
|
|
ServiceStatusValue,
|
|
ComponentHealthStatus,
|
|
OperationsRole,
|
|
InfrastructureSpeedtestResult,
|
|
} 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 [refreshInterval, setRefreshInterval] = useState<number>(30);
|
|
|
|
// Speedtest state
|
|
const [isRunningSpeedtest, setIsRunningSpeedtest] = useState(false);
|
|
const [speedtestResult, setSpeedtestResult] = useState<InfrastructureSpeedtestResult | null>(null);
|
|
const [speedtestError, setSpeedtestError] = useState<string | null>(null);
|
|
|
|
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);
|
|
}
|
|
}, []);
|
|
|
|
const handleRunSpeedtest = async () => {
|
|
if (isRunningSpeedtest) return;
|
|
setIsRunningSpeedtest(true);
|
|
setSpeedtestError(null);
|
|
try {
|
|
const resp = await fetch('/api/ops/infrastructure/speedtest', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (!resp.ok) {
|
|
const data = await resp.json().catch(() => null);
|
|
setSpeedtestError(data?.detail || 'Failed to complete speedtest');
|
|
return;
|
|
}
|
|
const data = (await resp.json()) as InfrastructureSpeedtestResult;
|
|
setSpeedtestResult(data);
|
|
} catch {
|
|
setSpeedtestError('Network error executing speedtest');
|
|
} finally {
|
|
setIsRunningSpeedtest(false);
|
|
}
|
|
};
|
|
|
|
// Initial fetch and user-selectable visibility-gated polling loop (5s, 10s, 30s)
|
|
useEffect(() => {
|
|
void fetchData();
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'visible') {
|
|
void fetchData();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
const pollIntervalMs = refreshInterval ? refreshInterval * 1000 : 30000;
|
|
pollTimerRef.current = setInterval(() => {
|
|
if (document.visibilityState === 'visible') {
|
|
void fetchData();
|
|
}
|
|
}, pollIntervalMs || 30000);
|
|
|
|
return () => {
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
if (pollTimerRef.current) {
|
|
clearInterval(pollTimerRef.current);
|
|
}
|
|
};
|
|
}, [fetchData, refreshInterval]);
|
|
|
|
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">
|
|
{/* Selectable Auto-refresh interval (5s, 10s, 30s) */}
|
|
<div className="flex items-center gap-1.5 text-[11px] text-on-surface-variant font-mono bg-surface-container-low px-2 py-1 rounded-lg border border-outline-variant/20">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse mr-0.5" />
|
|
<span className="text-outline">Auto:</span>
|
|
{[5, 10, 30].map((sec) => (
|
|
<button
|
|
key={sec}
|
|
type="button"
|
|
onClick={() => setRefreshInterval(sec)}
|
|
className={`px-1.5 py-0.5 rounded text-[11px] font-semibold transition-colors ${
|
|
refreshInterval === sec
|
|
? 'bg-primary-container text-on-primary-container font-bold'
|
|
: 'text-on-surface-variant hover:text-on-surface'
|
|
}`}
|
|
title={`Auto-refresh every ${sec} seconds`}
|
|
>
|
|
{sec}s
|
|
</button>
|
|
))}
|
|
</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>
|
|
<div className="flex items-center gap-2">
|
|
<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>
|
|
<button
|
|
type="button"
|
|
onClick={() => void handleRunSpeedtest()}
|
|
disabled={isRunningSpeedtest}
|
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-semibold bg-primary-container text-on-primary-container hover:opacity-90 disabled:opacity-50 transition-all shadow-sm"
|
|
>
|
|
<span className={`material-symbols-outlined text-[15px] ${isRunningSpeedtest ? 'animate-spin' : ''}`}>
|
|
{isRunningSpeedtest ? 'sync' : 'speed'}
|
|
</span>
|
|
<span>{isRunningSpeedtest ? 'Testing...' : 'Run Speedtest'}</span>
|
|
</button>
|
|
</div>
|
|
</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>
|
|
|
|
{isRunningSpeedtest && (
|
|
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-primary-container/30 flex items-center justify-between animate-pulse text-xs font-mono">
|
|
<div className="flex items-center gap-2 text-primary-fixed-dim">
|
|
<span className="material-symbols-outlined text-[18px] animate-spin">network_check</span>
|
|
<span>Benchmarking host latency and network throughput...</span>
|
|
</div>
|
|
<span className="text-outline">Live test</span>
|
|
</div>
|
|
)}
|
|
|
|
{speedtestError && (
|
|
<div className="p-3 rounded-lg bg-rose-950/40 border border-rose-800/40 text-xs text-rose-300 flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-[16px]">error</span>
|
|
<span>{speedtestError}</span>
|
|
</div>
|
|
)}
|
|
|
|
{speedtestResult && !isRunningSpeedtest && (
|
|
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-2.5">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<div className="flex items-center gap-1.5 font-bold text-on-surface">
|
|
<span className="material-symbols-outlined text-[16px] text-emerald-400">speed</span>
|
|
<span>Live Network Speedtest</span>
|
|
</div>
|
|
<span className="text-[10px] font-mono text-outline">
|
|
{new Date(speedtestResult.tested_at).toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2 text-center">
|
|
<div className="p-2 rounded bg-surface-container-low border border-outline-variant/10">
|
|
<span className="block text-[10px] text-outline font-mono">LATENCY</span>
|
|
<span className="text-sm font-bold font-mono text-emerald-400">{speedtestResult.latency_ms} ms</span>
|
|
</div>
|
|
<div className="p-2 rounded bg-surface-container-low border border-outline-variant/10">
|
|
<span className="block text-[10px] text-outline font-mono">DOWNLOAD</span>
|
|
<span className="text-sm font-bold font-mono text-cyan-400">{speedtestResult.download_mbps} Mbps</span>
|
|
</div>
|
|
<div className="p-2 rounded bg-surface-container-low border border-outline-variant/10">
|
|
<span className="block text-[10px] text-outline font-mono">UPLOAD</span>
|
|
<span className="text-sm font-bold font-mono text-primary-container">{speedtestResult.upload_mbps} Mbps</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-[10px] font-mono text-outline flex items-center justify-between pt-1">
|
|
<span>Target: {speedtestResult.server_target}</span>
|
|
{speedtestResult.client_ip && <span>IP: {speedtestResult.client_ip}</span>}
|
|
</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>
|
|
);
|
|
}
|