feat: resource_pressure - disk free, CPU freq/throttle/temp, GPU temp via nvidia-smi
This commit is contained in:
@@ -114,6 +114,15 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
(update.shares_accepted ?? a.shares_good)
|
||||
),
|
||||
status: 'online' as const,
|
||||
...(update.cpu_freq_mhz !== undefined ? { cpu_freq_mhz: update.cpu_freq_mhz } : {}),
|
||||
...(update.cpu_max_mhz !== undefined ? { cpu_max_mhz: update.cpu_max_mhz } : {}),
|
||||
...(update.cpu_throttle !== undefined ? { cpu_throttle: update.cpu_throttle } : {}),
|
||||
...(update.cpu_temp_c !== undefined ? { cpu_temp_c: update.cpu_temp_c } : {}),
|
||||
...(update.disk_free_gb !== undefined ? { disk_free_gb: update.disk_free_gb } : {}),
|
||||
...(update.disk_total_gb !== undefined ? { disk_total_gb: update.disk_total_gb } : {}),
|
||||
...(update.disk_free_pct !== undefined ? { disk_free_pct: update.disk_free_pct } : {}),
|
||||
...(update.gpu_temp_c !== undefined ? { gpu_temp_c: update.gpu_temp_c } : {}),
|
||||
...(update.gpu_usage_pct !== undefined ? { gpu_usage_pct: update.gpu_usage_pct } : {}),
|
||||
...(update.ssh_available !== undefined ? { ssh_available: update.ssh_available } : {}),
|
||||
...(update.posture_score !== undefined ? { posture_score: update.posture_score } : {}),
|
||||
...(update.last_patch_days !== undefined ? { last_patch_days: update.last_patch_days } : {}),
|
||||
|
||||
@@ -226,6 +226,21 @@
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
|
||||
/* ── Resource pressure badges ─────────────────────────────────────────────── */
|
||||
.cn-thermal, .cn-disk, .cn-throttle {
|
||||
font-size: 0.62rem;
|
||||
font-family: var(--font-tech);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: default;
|
||||
}
|
||||
.cn-thermal.therm-hot { color: #ff3333; background: rgba(255,51,51,0.14); font-weight: 700; animation: rb-blink 1.6s step-end infinite; }
|
||||
.cn-thermal.therm-warm { color: var(--neon-amber); background: rgba(255,176,32,0.1); }
|
||||
.cn-disk.disk-crit { color: #ff3333; background: rgba(255,51,51,0.14); font-weight: 700; }
|
||||
.cn-disk.disk-warn { color: var(--neon-amber); background: rgba(255,176,32,0.1); }
|
||||
.cn-throttle.therm-warm{ color: var(--neon-amber); background: rgba(255,176,32,0.1); }
|
||||
|
||||
/* ── T1007 service row ─────────────────────────────────────────────────────── */
|
||||
.cn-services {
|
||||
display: flex;
|
||||
|
||||
@@ -78,6 +78,20 @@ function postureTooltip(agent: Agent): string {
|
||||
if (agent.reboot_pending !== undefined) {
|
||||
lines.push(`Reboot required: ${agent.reboot_pending ? 'YES ⚠' : 'no ✓'}`);
|
||||
}
|
||||
// Resource pressure
|
||||
const tempLabel = agent.gpu_temp_c !== undefined ? `GPU ${agent.gpu_temp_c}°C` : agent.cpu_temp_c !== undefined ? `CPU ${agent.cpu_temp_c}°C` : null;
|
||||
if (tempLabel || agent.disk_free_pct !== undefined || agent.cpu_throttle !== undefined) {
|
||||
lines.push('──────────────────────');
|
||||
if (tempLabel) lines.push(`Temp: ${tempLabel}${agent.cpu_throttle ? ' THROTTLED' : ''}`);
|
||||
if (agent.disk_free_pct !== undefined) {
|
||||
lines.push(`Disk: ${agent.disk_free_gb?.toFixed(1) ?? '?'} GB free (${agent.disk_free_pct}% of ${agent.disk_total_gb?.toFixed(0) ?? '?'} GB)`);
|
||||
}
|
||||
if (agent.cpu_freq_mhz && agent.cpu_max_mhz) {
|
||||
lines.push(`CPU freq: ${agent.cpu_freq_mhz} / ${agent.cpu_max_mhz} MHz`);
|
||||
}
|
||||
if (agent.gpu_usage_pct !== undefined) lines.push(`GPU util: ${agent.gpu_usage_pct}%`);
|
||||
}
|
||||
|
||||
if (agent.services?.length) {
|
||||
lines.push('──────────────────────');
|
||||
lines.push('Services (T1007):');
|
||||
@@ -102,7 +116,34 @@ function pendingBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
function rebootBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
if (agent.reboot_pending === undefined) return null;
|
||||
if (agent.reboot_pending) return { label: 'REBOOT!', cls: 'rb-pending' };
|
||||
return null; // no badge when not pending — cleaner UI
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Resource pressure badges ───────────────────────────────────────────────
|
||||
|
||||
function thermalBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
const t = agent.gpu_temp_c ?? agent.cpu_temp_c;
|
||||
if (t === undefined) return null;
|
||||
if (t > 80) return { label: `${t}°`, cls: 'therm-hot' };
|
||||
if (t > 65) return { label: `${t}°`, cls: 'therm-warm' };
|
||||
return null; // cool enough — no badge clutter
|
||||
}
|
||||
|
||||
function diskBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
const pct = agent.disk_free_pct;
|
||||
if (pct === undefined) return null;
|
||||
if (pct < 5) return { label: `DISK ${pct}%`, cls: 'disk-crit' };
|
||||
if (pct < 15) return { label: `DISK ${pct}%`, cls: 'disk-warn' };
|
||||
return null; // plenty of space — no badge
|
||||
}
|
||||
|
||||
function throttleBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
if (!agent.cpu_throttle) return null;
|
||||
const pct = agent.cpu_freq_mhz && agent.cpu_max_mhz
|
||||
? Math.round(agent.cpu_freq_mhz / agent.cpu_max_mhz * 100)
|
||||
: null;
|
||||
const label = pct !== null ? `THRTTL ${pct}%` : 'THRTTL';
|
||||
return { label, cls: 'therm-warm' };
|
||||
}
|
||||
|
||||
// ── Service helpers (T1007) ────────────────────────────────────────────────
|
||||
@@ -516,6 +557,24 @@ export default function CruciblePage() {
|
||||
{a.agent_elevated && (
|
||||
<div className="cn-elevated" title="Running as Administrator / root">ADMIN</div>
|
||||
)}
|
||||
{(() => { const tb = thermalBadge(a); return tb && (
|
||||
<div
|
||||
className={`cn-thermal ${tb.cls}`}
|
||||
title={`CPU: ${a.cpu_temp_c ?? '?'}°C GPU: ${a.gpu_temp_c ?? '?'}°C`}
|
||||
>{tb.label}</div>
|
||||
); })()}
|
||||
{(() => { const db = diskBadge(a); return db && (
|
||||
<div
|
||||
className={`cn-disk ${db.cls}`}
|
||||
title={`Disk: ${a.disk_free_gb?.toFixed(1) ?? '?'} GB free of ${a.disk_total_gb?.toFixed(0) ?? '?'} GB`}
|
||||
>{db.label}</div>
|
||||
); })()}
|
||||
{(() => { const trb = throttleBadge(a); return trb && (
|
||||
<div
|
||||
className={`cn-throttle ${trb.cls}`}
|
||||
title={`CPU running at ${a.cpu_freq_mhz ?? '?'} MHz (max ${a.cpu_max_mhz ?? '?'} MHz)`}
|
||||
>{trb.label}</div>
|
||||
); })()}
|
||||
</div>
|
||||
{a.services && a.services.length > 0 && (
|
||||
<div className="cn-services">
|
||||
|
||||
@@ -171,6 +171,35 @@ export default function DashboardPage() {
|
||||
return Math.round((times.length / spanMs) * 3_600_000);
|
||||
}, [shares]);
|
||||
|
||||
// ── Resource pressure fleet stats ─────────────────────────────────────────
|
||||
const onlineAgents = useMemo(() => agents.filter(a => a.status === 'online'), [agents]);
|
||||
|
||||
const hottestNode = useMemo(() => {
|
||||
let best: typeof agents[0] | null = null;
|
||||
let max = -Infinity;
|
||||
for (const a of onlineAgents) {
|
||||
const t = a.gpu_temp_c ?? a.cpu_temp_c ?? -1;
|
||||
if (t > max) { max = t; best = a; }
|
||||
}
|
||||
return best && max > 0 ? { agent: best, temp: max } : null;
|
||||
}, [onlineAgents]);
|
||||
|
||||
const minDiskNode = useMemo(() => {
|
||||
let best: typeof agents[0] | null = null;
|
||||
let min = Infinity;
|
||||
for (const a of onlineAgents) {
|
||||
if (a.disk_free_pct !== undefined && a.disk_free_pct < min) {
|
||||
min = a.disk_free_pct; best = a;
|
||||
}
|
||||
}
|
||||
return best ? { agent: best, pct: min } : null;
|
||||
}, [onlineAgents]);
|
||||
|
||||
const throttledCount = useMemo(
|
||||
() => onlineAgents.filter(a => a.cpu_throttle).length,
|
||||
[onlineAgents]
|
||||
);
|
||||
|
||||
// ── Analytics ─────────────────────────────────────────────────────────────
|
||||
const fleetHealth = useMemo(() => computeFleetHealth(agents, pools), [agents, pools]);
|
||||
const contribs = useMemo(() => contributionBars(agents), [agents]);
|
||||
@@ -349,6 +378,69 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div className="stat-sub">{totalShares.toLocaleString()} total submitted</div>
|
||||
</NeonCard>
|
||||
|
||||
{/* ── Resource Pressure cards ──────────────────────────────────────── */}
|
||||
<NeonCard
|
||||
accent={hottestNode && hottestNode.temp > 80 ? 'amber' : 'cyan'}
|
||||
className="stat-card-wrap"
|
||||
>
|
||||
<div className="stat-label font-tech">Hottest Node</div>
|
||||
{hottestNode ? (
|
||||
<>
|
||||
<div className={`stat-value ${hottestNode.temp > 80 ? 'neon-glow-amber' : 'neon-glow-cyan'}`}>
|
||||
{hottestNode.temp}
|
||||
<span className="stat-dim" style={{ fontSize: '0.8em' }}>°C</span>
|
||||
</div>
|
||||
<div className="stat-sub" title={hottestNode.agent.id}>
|
||||
{hottestNode.agent.name.length > 14 ? hottestNode.agent.name.slice(0, 13) + '…' : hottestNode.agent.name}
|
||||
{hottestNode.temp > 80 && ' ⚠'}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="stat-value stat-dim">—</div>
|
||||
<div className="stat-sub">no temp data</div>
|
||||
</>
|
||||
)}
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard
|
||||
accent={minDiskNode && minDiskNode.pct < 10 ? 'amber' : 'green'}
|
||||
className="stat-card-wrap"
|
||||
>
|
||||
<div className="stat-label font-tech">Min Disk Free</div>
|
||||
{minDiskNode ? (
|
||||
<>
|
||||
<div className={`stat-value ${minDiskNode.pct < 10 ? 'neon-glow-amber' : 'accepted'}`}>
|
||||
{minDiskNode.pct}
|
||||
<span className="stat-dim" style={{ fontSize: '0.8em' }}>%</span>
|
||||
</div>
|
||||
<div className="stat-sub" title={minDiskNode.agent.id}>
|
||||
{minDiskNode.agent.name.length > 14 ? minDiskNode.agent.name.slice(0, 13) + '…' : minDiskNode.agent.name}
|
||||
{minDiskNode.pct < 10 && ' ⚠ low'}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="stat-value stat-dim">—</div>
|
||||
<div className="stat-sub">no disk data</div>
|
||||
</>
|
||||
)}
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard
|
||||
accent={throttledCount > 0 ? 'amber' : 'purple'}
|
||||
className="stat-card-wrap"
|
||||
>
|
||||
<div className="stat-label font-tech">CPU Throttled</div>
|
||||
<div className={`stat-value ${throttledCount > 0 ? 'neon-glow-amber' : 'neon-glow-purple'}`}>
|
||||
{throttledCount}
|
||||
<span className="stat-dim" style={{ fontSize: '0.8em' }}> node{throttledCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div className="stat-sub">
|
||||
{throttledCount === 0 ? 'fleet running at full speed' : `${throttledCount} below 80% rated freq`}
|
||||
</div>
|
||||
</NeonCard>
|
||||
</div>
|
||||
|
||||
{/* ── Analytics row — always visible ─────────────────────────────────── */}
|
||||
|
||||
@@ -24,6 +24,17 @@ export interface Agent {
|
||||
arch?: string;
|
||||
os_version?: string;
|
||||
capabilities?: AgentCapabilities;
|
||||
// Resource pressure
|
||||
cpu_freq_mhz?: number;
|
||||
cpu_max_mhz?: number;
|
||||
cpu_throttle?: boolean;
|
||||
cpu_temp_c?: number;
|
||||
disk_free_gb?: number;
|
||||
disk_total_gb?: number;
|
||||
disk_free_pct?: number;
|
||||
gpu_temp_c?: number;
|
||||
gpu_usage_pct?: number;
|
||||
|
||||
ssh_available?: boolean;
|
||||
posture_score?: number;
|
||||
last_patch_days?: number;
|
||||
|
||||
@@ -19,6 +19,16 @@ export interface WSStatsUpdate {
|
||||
uptime_seconds?: number;
|
||||
shares_submitted?: number;
|
||||
shares_accepted?: number;
|
||||
// Resource pressure
|
||||
cpu_freq_mhz?: number;
|
||||
cpu_max_mhz?: number;
|
||||
cpu_throttle?: boolean;
|
||||
cpu_temp_c?: number;
|
||||
disk_free_gb?: number;
|
||||
disk_total_gb?: number;
|
||||
disk_free_pct?: number;
|
||||
gpu_temp_c?: number;
|
||||
gpu_usage_pct?: number;
|
||||
ssh_available?: boolean;
|
||||
posture_score?: number;
|
||||
last_patch_days?: number;
|
||||
|
||||
Reference in New Issue
Block a user