mesh-cathedral / src /kernels.js
betterwithage's picture
feat(mesh): live 45-kernel observability — per-kernel status colours (green/amber/red from /api/<organ>/v3/kernels), heartbeat pulse on each dot, last-5-heartbeats in kernel side panel, 45-kernel signed-receipt ticker. ADDITIVE. Doctrine v11 749/14/163. Sign: Yachay
320c18f verified
// Kernel orbits — 7 universal kernel dots + 2 vertical kernel dots orbit each
// chakra. Click a kernel dot -> side panel shows its docs + recent activity polled
// from the live flagship. Top-HUD status badges per chakra polled from /healthz
// every 30s (green/amber/red). Doctrine v11 LOCKED. ZERO BANDAID.
import { CHAKRAS, UNIVERSAL_KERNELS, GOLD } from './config.js';
import { openKernelPanel } from './codex.js';
import { kernelStatus, consumePulse } from './heartbeats.js';
// status → emissive tint applied on top of the chakra/gold base colour
const STATUS_COLOR = { green:0x34d399, amber:0xfbbf24, red:0xf87171 };
let THREEref;
const orbiters = []; // {mesh, chakraId, kernel, radius, speed, phase, tilt, vertical}
export function buildKernels(scene, THREE, chakraGroups, _openCodex){
THREEref = THREE;
buildBadges();
CHAKRAS.forEach(c => {
const g = chakraGroups[c.id];
if (!g) return;
const col = new THREE.Color(c.color);
// 7 universal kernels on inner orbit
UNIVERSAL_KERNELS.forEach((k, i) => {
const dot = kernelDot(THREE, col, false);
addOrbiter(scene, g, dot, c, k, 3.4, 0.5 + i*0.07, (i/7)*Math.PI*2, (i%2?0.4:-0.4), false);
});
// 2 vertical kernels on outer orbit, slightly different glow (gold-tinted)
c.vertical.forEach((k, i) => {
const dot = kernelDot(THREE, new THREE.Color(GOLD), true);
addOrbiter(scene, g, dot, c, k, 4.6, 0.35 + i*0.05, (i*Math.PI), 0.9, true);
});
});
}
function kernelDot(THREE, col, vertical){
const m = new THREE.Mesh(
new THREE.SphereGeometry(vertical ? 0.22 : 0.17, 12, 12),
new THREE.MeshStandardMaterial({ color:col, emissive:col, emissiveIntensity: vertical?1.0:0.7, roughness:0.3 }));
if (vertical){
const halo = new THREE.Mesh(new THREE.TorusGeometry(0.34,0.025,6,16),
new THREE.MeshBasicMaterial({ color:GOLD, transparent:true, opacity:0.7 }));
m.add(halo);
}
return m;
}
function addOrbiter(scene, parentGroup, dot, chakra, kernel, radius, speed, phase, tilt, vertical){
parentGroup.add(dot);
dot.userData.kernelClick = () => openKernelPanel(chakra, kernel, vertical);
// remember the dot's resting emissive so a heartbeat pulse can flare then settle
dot.userData.baseEmissive = dot.material.emissiveIntensity;
dot.userData.pulse = 0;
orbiters.push({ mesh:dot, chakra, kernel, radius, speed, phase, tilt, vertical });
}
let _statusTick = 0;
export function updateKernels(t, dt){
// refresh per-kernel status colour ~3x/sec (cheap; registry is updated by heartbeats.js)
const doStatus = (++_statusTick % 20) === 0;
for (const o of orbiters){
const a = t * o.speed + o.phase;
o.mesh.position.set(
Math.cos(a) * o.radius,
Math.sin(a*0.7 + o.tilt) * o.radius * 0.35,
Math.sin(a) * o.radius
);
const m = o.mesh;
if (doStatus){
const st = kernelStatus(o.chakra.id, o.kernel.name);
const tint = STATUS_COLOR[st] || 0x6b7280;
m.material.emissive.setHex(tint);
// red kernels dim out; healthy ones keep their glow
m.userData.restGlow = (st === 'red') ? 0.25 : (o.vertical ? 1.0 : 0.7);
}
// heartbeat pulse: a fresh tick flares the dot then eases back to rest glow
if (consumePulse(o.chakra.id, o.kernel.name)) m.userData.pulse = 1.0;
m.userData.pulse = Math.max(0, (m.userData.pulse || 0) - (dt || 0.016) * 2.0);
const rest = (m.userData.restGlow != null) ? m.userData.restGlow : (o.vertical ? 1.0 : 0.7);
m.material.emissiveIntensity = rest + m.userData.pulse * 1.6;
m.scale.setScalar(1 + m.userData.pulse * 0.5);
}
}
// ---------------- top-HUD status badges ----------------
function buildBadges(){
const hud = document.getElementById('topHUD');
const allBtn = document.getElementById('allCodices');
CHAKRAS.forEach(c => {
const b = document.createElement('button');
b.className = 'badge'; b.id = 'badge-'+c.id;
b.setAttribute('aria-label', c.label+' status — tap to open codex');
b.innerHTML = `<span class="dot" id="dot-${c.id}"></span>${c.label}`;
b.onclick = () => import('./codex.js').then(m => m.openChakraCodex(c.id));
hud.insertBefore(b, allBtn);
});
}
export async function pollStatus(){
await Promise.all(CHAKRAS.map(async c => {
const dot = document.getElementById('dot-'+c.id);
if (!dot) return;
try {
const r = await fetch(c.health.url, { cache:'no-store' });
if (r.ok){
let ok = true;
try { const j = await r.json(); ok = (j.status === 'ok' || j.status === undefined); } catch(e){}
dot.className = 'dot ' + (ok ? 'green' : 'amber');
} else dot.className = 'dot amber';
} catch(e){ dot.className = 'dot red'; }
}));
setTimeout(pollStatus, 30000);
}