File size: 4,746 Bytes
b58e0b6
 
 
 
 
 
320c18f
 
 
 
b58e0b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320c18f
 
 
b58e0b6
 
 
320c18f
 
 
 
b58e0b6
 
 
 
 
 
 
320c18f
 
 
 
 
 
 
 
 
 
 
 
 
 
b58e0b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 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);
}