From 48b8247a0c4762e0a67da0678e6bdcc8cea91eaf Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 3 Jun 2026 18:20:46 -0700 Subject: [PATCH] Worker orbs: nucleus reflects worker health, not just activity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hub now reads as a health gauge on top of the activity gauge. A new decaying errorHeat (0..1) is bumped by onStatus whenever a worker reports a real error increment, and cools over ~6s. While stressed the nucleus blends toward red, its heartbeat quickens (agitation), and a fast-flickering red warning ring appears — so a glance distinguishes 'busy and healthy' from 'something's actually failing'. Since 404s are classified as not_found now, this only lights up on genuine failures (timeouts, 5xx). --- webui/static/worker-orbs.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/webui/static/worker-orbs.js b/webui/static/worker-orbs.js index 5e1789c9..1d7db8ed 100644 --- a/webui/static/worker-orbs.js +++ b/webui/static/worker-orbs.js @@ -53,6 +53,7 @@ let orbs = []; let sparks = []; // particle emissions from active orbs let inflows = []; // pulses traveling from active orbs into the hub + let errorHeat = 0; // 0..1 aggregate "stress" — bumps on real worker errors, decays over time let state = 'idle'; let animFrame = null; let onDashboard = false; @@ -464,6 +465,9 @@ return; } + // Health stress cools off when errors stop (~6s to settle from a spike) + if (errorHeat > 0.0001) errorHeat *= 0.992; else errorHeat = 0; + // Check active state every 30 frames (button ref is cached at init) if (frameCount % 30 === 0) { orbs.forEach(orb => { @@ -646,14 +650,20 @@ const workers = visible.filter(o => !o.hub); const activeCount = workers.filter(o => o.active).length; const energy = workers.length ? activeCount / workers.length : 0; // 0..1 + const stress = errorHeat; // 0..1 health gauge - const beatSpeed = 1.0 + energy * 1.8; // faster heartbeat when busy + // Stress quickens the heartbeat (agitation) and blends the + // nucleus toward red — a calm green-purple hub means "all good". + const beatSpeed = 1.0 + energy * 1.8 + stress * 2.5; const slow = 0.5 + 0.5 * Math.sin(time * beatSpeed); const hubR = (ORB_RADIUS + 3 + energy * 4) + slow * (2 + energy * 2); + const hr = Math.round(r + (235 - r) * stress); + const hg = Math.round(g + (60 - g) * stress); + const hb = Math.round(b + (60 - b) * stress); // Wide ambient glow — brighter + wider with energy (cached sprite) const glowR = hubR * (4 + energy * 1.5); - drawGlow(ctx, orb.x, orb.y, glowR, r, g, b, 0.18 + energy * 0.18 + slow * 0.12); + drawGlow(ctx, orb.x, orb.y, glowR, hr, hg, hb, 0.18 + energy * 0.18 + slow * 0.12 + stress * 0.15); if (hubImageReady) { // SoulSync logo as the nucleus — fit to the pulsing radius while @@ -687,10 +697,22 @@ const ringR = hubR + ringPhase * hubR * 2.5; ctx.beginPath(); ctx.arc(orb.x, orb.y, ringR, 0, Math.PI * 2); - ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${(1 - ringPhase) * (0.25 + energy * 0.2)})`; + ctx.strokeStyle = `rgba(${hr}, ${hg}, ${hb}, ${(1 - ringPhase) * (0.25 + energy * 0.2)})`; ctx.lineWidth = 1.5; ctx.stroke(); } + + // Health warning: a fast-flickering red ring when workers are + // actually erroring (fades out as stress cools). + if (stress > 0.05) { + const warn = 0.5 + 0.5 * Math.sin(time * 12); + const wr = hubR + 3 + warn * 5; + ctx.beginPath(); + ctx.arc(orb.x, orb.y, wr, 0, Math.PI * 2); + ctx.strokeStyle = `rgba(255, 70, 70, ${stress * (0.3 + warn * 0.35)})`; + ctx.lineWidth = 2; + ctx.stroke(); + } continue; } @@ -857,6 +879,9 @@ if (dErr > 0) { orb.pendingErr = Math.min(PULSE_CAP, orb.pendingErr + dErr); orb.errRate = Math.max(MIN_RELEASE_RATE, orb.pendingErr / STATUS_FRAMES); + // Feed the nucleus health gauge — each real error raises the hub's + // stress (404s are not_found now, so this only fires on true failures). + errorHeat = Math.min(1, errorHeat + 0.25 * dErr); } }