refactor: honesty pass — every visual element tied to real data or shown as honestly offline (#408)

Co-authored-by: Perplexity Computer <perplexity@tower.local>
Co-committed-by: Perplexity Computer <perplexity@tower.local>
This commit is contained in:
2026-03-24 17:03:18 +00:00
committed by Timmy Time
parent e29b6ff0a8
commit 05bd7ffec7
5 changed files with 507 additions and 197 deletions

597
app.js
View File

@@ -60,6 +60,9 @@ const MATRIX_FONT_SIZE = 14;
const MATRIX_COL_COUNT = Math.floor(window.innerWidth / MATRIX_FONT_SIZE);
const matrixDrops = new Array(MATRIX_COL_COUNT).fill(1);
// Commit hashes for matrix rain — populated by heatmap fetch, used to inject real data into the rain
let _matrixCommitHashes = [];
function drawMatrixRain() {
// Fade previous frame with semi-transparent black overlay (creates the trail)
matrixCtx.fillStyle = 'rgba(0, 0, 8, 0.05)';
@@ -67,8 +70,27 @@ function drawMatrixRain() {
matrixCtx.font = `${MATRIX_FONT_SIZE}px monospace`;
// Tether rain density to commit activity — density range [0.1, 1.0]
const activity = typeof totalActivity === 'function' ? totalActivity() : 0;
const density = 0.1 + activity * 0.9; // minimum 10% density
const activeColCount = Math.max(1, Math.floor(matrixDrops.length * density));
for (let i = 0; i < matrixDrops.length; i++) {
const char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
// Only render columns up to density-scaled count (skip inactive ones)
if (i >= activeColCount) {
// Inactive columns still fade but don't spawn new characters
if (matrixDrops[i] * MATRIX_FONT_SIZE > matrixCanvas.height) continue;
}
// Occasionally inject a real commit hash (first 7 chars) instead of katakana
let char;
if (_matrixCommitHashes.length > 0 && Math.random() < 0.02) {
const hash = _matrixCommitHashes[Math.floor(Math.random() * _matrixCommitHashes.length)];
char = hash[Math.floor(Math.random() * hash.length)];
} else {
char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
}
const x = i * MATRIX_FONT_SIZE;
const y = matrixDrops[i] * MATRIX_FONT_SIZE;
@@ -76,8 +98,9 @@ function drawMatrixRain() {
matrixCtx.fillStyle = '#aaffaa';
matrixCtx.fillText(char, x, y);
// Reset drop to top with some randomness
if (y > matrixCanvas.height && Math.random() > 0.975) {
// Reset drop to top — speed influenced by activity
const resetThreshold = 0.975 - activity * 0.015; // more activity = faster reset = denser rain
if (y > matrixCanvas.height && Math.random() > resetThreshold) {
matrixDrops[i] = 0;
}
matrixDrops[i]++;
@@ -166,6 +189,12 @@ const starMaterial = new THREE.PointsMaterial({
const stars = new THREE.Points(starGeo, starMaterial);
scene.add(stars);
// Star pulse state — tethered to Bitcoin block events
let _starPulseIntensity = 0; // 0 = normal, 1 = peak brightness
const STAR_BASE_OPACITY = 0.3;
const STAR_PEAK_OPACITY = 1.0;
const STAR_PULSE_DECAY = 0.012; // decay per frame (~3 seconds to fade)
// === CONSTELLATION LINES ===
// Connect nearby stars with faint lines, limited to avoid clutter
/**
@@ -798,6 +827,9 @@ async function updateHeatmap() {
if (res.ok) commits = await res.json();
} catch { /* silently use zero-activity baseline */ }
// Feed commit hashes to matrix rain for data-tethered aesthetic
_matrixCommitHashes = commits.slice(0, 20).map(c => (c.sha || '').slice(0, 7)).filter(h => h.length > 0);
const now = Date.now();
const rawWeights = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
@@ -1222,8 +1254,10 @@ let energyBeamPulse = 0;
function animateEnergyBeam() {
energyBeamPulse += 0.02;
const pulseEffect = Math.sin(energyBeamPulse) * 0.3 + 0.7;
energyBeamMaterial.opacity = 0.3 + pulseEffect * 0.4;
// Tether beam intensity to active agent count: 0=faint, 1=0.4, 2=0.7, 3+=1.0
const agentIntensity = _activeAgentCount === 0 ? 0.1 : Math.min(0.1 + _activeAgentCount * 0.3, 1.0);
const pulseEffect = Math.sin(energyBeamPulse) * 0.15 * agentIntensity;
energyBeamMaterial.opacity = agentIntensity * 0.6 + pulseEffect;
}
// === RESIZE HANDLER ===
@@ -1272,7 +1306,7 @@ sovereigntyGroup.add(scoreArcMesh);
const meterLight = new THREE.PointLight(sovereigntyHexColor(sovereigntyScore), 0.7, 6);
sovereigntyGroup.add(meterLight);
function buildMeterTexture(score, label) {
function buildMeterTexture(score, label, assessmentType) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
@@ -1282,18 +1316,22 @@ function buildMeterTexture(score, label) {
ctx.font = 'bold 52px "Courier New", monospace';
ctx.fillStyle = hexStr;
ctx.textAlign = 'center';
ctx.fillText(`${score}%`, 128, 58);
ctx.fillText(`${score}%`, 128, 50);
ctx.font = '16px "Courier New", monospace';
ctx.fillStyle = '#8899bb';
ctx.fillText(label.toUpperCase(), 128, 82);
ctx.fillText(label.toUpperCase(), 128, 74);
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#445566';
ctx.fillText('SOVEREIGNTY', 128, 104);
ctx.fillText('SOVEREIGNTY', 128, 94);
// "MANUAL ASSESSMENT" label — honest about data source
ctx.font = '9px "Courier New", monospace';
ctx.fillStyle = '#334455';
ctx.fillText(assessmentType === 'MANUAL' ? 'MANUAL ASSESSMENT' : 'MANUAL ASSESSMENT', 128, 112);
return new THREE.CanvasTexture(canvas);
}
const meterSpriteMat = new THREE.SpriteMaterial({
map: buildMeterTexture(sovereigntyScore, sovereigntyLabel),
map: buildMeterTexture(sovereigntyScore, sovereigntyLabel, 'MANUAL'),
transparent: true,
depthWrite: false,
});
@@ -1321,7 +1359,8 @@ async function loadSovereigntyStatus() {
scoreArcMat.color.setHex(col);
meterLight.color.setHex(col);
if (meterSpriteMat.map) meterSpriteMat.map.dispose();
meterSpriteMat.map = buildMeterTexture(score, label);
const assessmentType = data.assessment_type || 'MANUAL';
meterSpriteMat.map = buildMeterTexture(score, label, assessmentType);
meterSpriteMat.needsUpdate = true;
} catch {
// defaults already set above
@@ -1331,7 +1370,8 @@ async function loadSovereigntyStatus() {
loadSovereigntyStatus();
// === ENERGY BEAM FOR BATCAVE TERMINAL ===
// Vertical energy beam from Batcave terminal area to the sky with animated opacity and pulse effect.
// Vertical energy beam from Batcave terminal area — intensity tethered to active agent count.
let _activeAgentCount = 0; // updated by agent status fetch
const ENERGY_BEAM_RADIUS = 0.2;
const ENERGY_BEAM_HEIGHT = 50;
const ENERGY_BEAM_Y = 0;
@@ -1355,15 +1395,15 @@ scene.add(energyBeam);
// === RUNE RING ===
// 12 Elder Futhark rune sprites in a slow-orbiting ring around the center platform.
// Rune sprites tethered to portal data — count matches portals, colors from portals.json.
const RUNE_COUNT = 12;
let RUNE_COUNT = 12; // default, updated when portals load
const RUNE_RING_RADIUS = 7.0;
const RUNE_RING_Y = 1.5; // base height above platform
const RUNE_ORBIT_SPEED = 0.08; // radians per second
const ELDER_FUTHARK = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','','','ᚹ','ᚺ','ᚾ','','ᛃ'];
const RUNE_GLOW_COLORS = ['#00ffcc', '#ff44ff']; // alternating cyan / magenta
const RUNE_GLOW_COLORS = ['#00ffcc', '#ff44ff']; // fallback, overridden by portal colors
/**
* Creates a canvas texture for a single glowing rune glyph.
@@ -1406,35 +1446,56 @@ runeOrbitRingMesh.position.y = RUNE_RING_Y;
scene.add(runeOrbitRingMesh);
/**
* @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number}>}
* @type {Array<{sprite: THREE.Sprite, baseAngle: number, floatPhase: number, portalOnline: boolean}>}
*/
const runeSprites = [];
for (let i = 0; i < RUNE_COUNT; i++) {
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
const color = RUNE_GLOW_COLORS[i % RUNE_GLOW_COLORS.length];
const texture = createRuneTexture(glyph, color);
/**
* Rebuilds rune ring from portal data — count matches portals, colors from portals.json.
* Falls back to default 12 runes if portals not yet loaded.
*/
function rebuildRuneRing() {
// Remove existing rune sprites
for (const rune of runeSprites) {
scene.remove(rune.sprite);
if (rune.sprite.material.map) rune.sprite.material.map.dispose();
rune.sprite.material.dispose();
}
runeSprites.length = 0;
const runeMat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0.85,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(runeMat);
sprite.scale.set(1.3, 1.3, 1);
const portalData = portals.length > 0 ? portals : null;
const count = portalData ? portalData.length : RUNE_COUNT;
const baseAngle = (i / RUNE_COUNT) * Math.PI * 2;
sprite.position.set(
Math.cos(baseAngle) * RUNE_RING_RADIUS,
RUNE_RING_Y,
Math.sin(baseAngle) * RUNE_RING_RADIUS
);
scene.add(sprite);
runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 });
for (let i = 0; i < count; i++) {
const glyph = ELDER_FUTHARK[i % ELDER_FUTHARK.length];
const color = portalData ? portalData[i].color : RUNE_GLOW_COLORS[i % RUNE_GLOW_COLORS.length];
const isOnline = portalData ? portalData[i].status === 'online' : true;
const texture = createRuneTexture(glyph, color);
const runeMat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: isOnline ? 1.0 : 0.15, // bright if online, dim if offline
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const sprite = new THREE.Sprite(runeMat);
sprite.scale.set(1.3, 1.3, 1);
const baseAngle = (i / count) * Math.PI * 2;
sprite.position.set(
Math.cos(baseAngle) * RUNE_RING_RADIUS,
RUNE_RING_Y,
Math.sin(baseAngle) * RUNE_RING_RADIUS
);
scene.add(sprite);
runeSprites.push({ sprite, baseAngle, floatPhase: (i / count) * Math.PI * 2, portalOnline: isOnline });
}
}
// Initial build with default count (will be rebuilt when portals load)
rebuildRuneRing();
// === HOLOGRAPHIC EARTH ===
// A procedural holographic planet Earth slowly rotating above the Nexus.
@@ -2116,14 +2177,14 @@ function createDualBrainTexture() {
ctx.textAlign = 'left';
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
// Categories
// Categories — honest offline state (no scores, empty bars)
const categories = [
{ name: 'Triage', score: 0.87, status: 'GRADUATED', color: '#00ff88' },
{ name: 'Tool Use', score: 0.78, status: 'PROBATION', color: '#ffcc00' },
{ name: 'Code Gen', score: 0.62, status: 'SHADOW', color: '#4488ff' },
{ name: 'Planning', score: 0.71, status: 'SHADOW', color: '#4488ff' },
{ name: 'Communication', score: 0.83, status: 'PROBATION', color: '#ffcc00' },
{ name: 'Reasoning', score: 0.55, status: 'CLOUD ONLY', color: '#ff4444' },
{ name: 'Triage' },
{ name: 'Tool Use' },
{ name: 'Code Gen' },
{ name: 'Planning' },
{ name: 'Communication' },
{ name: 'Reasoning' },
];
const barX = 20;
@@ -2134,34 +2195,22 @@ function createDualBrainTexture() {
for (const cat of categories) {
// Category label
ctx.font = '13px "Courier New", monospace';
ctx.fillStyle = '#ccd6f6';
ctx.fillStyle = '#445566';
ctx.textAlign = 'left';
ctx.fillText(cat.name, barX, y + 14);
// Score value
// Score value — dash (no data)
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = cat.color;
ctx.fillStyle = '#334466';
ctx.textAlign = 'right';
ctx.fillText(cat.score.toFixed(2), W - 20, y + 14);
ctx.fillText('\u2014', W - 20, y + 14);
y += 22;
// Bar background
// Bar background only — no fill (zero-width)
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
ctx.fillRect(barX, y, barW, barH);
// Bar fill
ctx.fillStyle = cat.color;
ctx.globalAlpha = 0.7;
ctx.fillRect(barX, y, barW * cat.score, barH);
ctx.globalAlpha = 1.0;
// Status label on bar
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#000000';
ctx.textAlign = 'left';
ctx.fillText(cat.status, barX + 6, y + 14);
y += barH + 12;
}
@@ -2174,35 +2223,32 @@ function createDualBrainTexture() {
y += 22;
// Overall score
ctx.font = '12px "Courier New", monospace';
ctx.fillStyle = '#556688';
ctx.textAlign = 'left';
ctx.fillText('OVERALL CONVERGENCE', 20, y);
ctx.font = 'bold 36px "Courier New", monospace';
ctx.fillStyle = '#88ccff';
// Status text — honest offline
ctx.font = 'bold 18px "Courier New", monospace';
ctx.fillStyle = '#334466';
ctx.textAlign = 'center';
ctx.fillText('0.73', W / 2, y + 44);
ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10);
// Brain indicators at bottom
y += 60;
// Cloud brain indicator
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#223344';
ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32);
// Brain indicators at bottom — dim (offline)
y += 52;
ctx.beginPath();
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = '#00ddff';
ctx.fillStyle = '#334466';
ctx.fill();
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#00ddff';
ctx.fillStyle = '#334466';
ctx.textAlign = 'left';
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
// Local brain indicator
ctx.beginPath();
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
ctx.fillStyle = '#ffaa22';
ctx.fillStyle = '#334466';
ctx.fill();
ctx.fillStyle = '#ffaa22';
ctx.fillStyle = '#334466';
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
return new THREE.CanvasTexture(canvas);
@@ -2233,13 +2279,13 @@ dualBrainLight.position.set(0, 0.5, 1);
dualBrainGroup.add(dualBrainLight);
// --- Brain Orbs ---
// Cloud brain orb (cyan) — positioned left of panel
const CLOUD_ORB_COLOR = 0x00ddff;
// Cloud brain orb — dim grey (dual-brain offline)
const CLOUD_ORB_COLOR = 0x334466;
const cloudOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const cloudOrbMat = new THREE.MeshStandardMaterial({
color: CLOUD_ORB_COLOR,
emissive: new THREE.Color(CLOUD_ORB_COLOR),
emissiveIntensity: 1.5,
emissiveIntensity: 0.1,
metalness: 0.3,
roughness: 0.2,
transparent: true,
@@ -2250,17 +2296,17 @@ cloudOrb.position.set(-2.0, 3.0, 0);
cloudOrb.userData.zoomLabel = 'Cloud Brain';
dualBrainGroup.add(cloudOrb);
const cloudOrbLight = new THREE.PointLight(CLOUD_ORB_COLOR, 0.8, 5);
const cloudOrbLight = new THREE.PointLight(CLOUD_ORB_COLOR, 0.15, 5);
cloudOrbLight.position.copy(cloudOrb.position);
dualBrainGroup.add(cloudOrbLight);
// Local brain orb (amber) — positioned right of panel
const LOCAL_ORB_COLOR = 0xffaa22;
// Local brain orb — dim grey (dual-brain offline)
const LOCAL_ORB_COLOR = 0x334466;
const localOrbGeo = new THREE.SphereGeometry(0.35, 32, 32);
const localOrbMat = new THREE.MeshStandardMaterial({
color: LOCAL_ORB_COLOR,
emissive: new THREE.Color(LOCAL_ORB_COLOR),
emissiveIntensity: 1.5,
emissiveIntensity: 0.1,
metalness: 0.3,
roughness: 0.2,
transparent: true,
@@ -2271,13 +2317,13 @@ localOrb.position.set(2.0, 3.0, 0);
localOrb.userData.zoomLabel = 'Local Brain';
dualBrainGroup.add(localOrb);
const localOrbLight = new THREE.PointLight(LOCAL_ORB_COLOR, 0.8, 5);
const localOrbLight = new THREE.PointLight(LOCAL_ORB_COLOR, 0.15, 5);
localOrbLight.position.copy(localOrb.position);
dualBrainGroup.add(localOrbLight);
// --- Brain Pulse Particle Stream ---
// Particles flow from cloud orb → local orb along a curved arc
const BRAIN_PARTICLE_COUNT = 120;
// Particles OFF — dual-brain system not deployed. Will flow when system comes online.
const BRAIN_PARTICLE_COUNT = 0;
const brainParticlePositions = new Float32Array(BRAIN_PARTICLE_COUNT * 3);
const brainParticlePhases = new Float32Array(BRAIN_PARTICLE_COUNT); // 0..1 progress along arc
const brainParticleSpeeds = new Float32Array(BRAIN_PARTICLE_COUNT);
@@ -2361,6 +2407,12 @@ function animate() {
stars.rotation.x = (targetRotX + elapsed * 0.01) * rotationScale;
stars.rotation.y = (targetRotY + elapsed * 0.015) * rotationScale;
// Star brightness pulse — tethered to Bitcoin block events
if (_starPulseIntensity > 0) {
_starPulseIntensity = Math.max(0, _starPulseIntensity - STAR_PULSE_DECAY);
}
starMaterial.opacity = STAR_BASE_OPACITY + (STAR_PEAK_OPACITY - STAR_BASE_OPACITY) * _starPulseIntensity;
constellationLines.rotation.x = stars.rotation.x;
constellationLines.rotation.y = stars.rotation.y;
@@ -2533,17 +2585,28 @@ function animate() {
burst.geo.attributes.position.needsUpdate = true;
}
// Animate rune ring — orbit and vertical float
// Animate rune ring — orbit and vertical float, brightness tethered to portal status
for (const rune of runeSprites) {
const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED;
rune.sprite.position.x = Math.cos(angle) * RUNE_RING_RADIUS;
rune.sprite.position.z = Math.sin(angle) * RUNE_RING_RADIUS;
rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4;
rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2;
// Online portal = bright, offline = dim
const baseOpacity = rune.portalOnline ? 0.85 : 0.12;
const pulseRange = rune.portalOnline ? 0.15 : 0.03;
rune.sprite.material.opacity = baseOpacity + Math.sin(elapsed * 1.2 + rune.floatPhase) * pulseRange;
}
// Animate holographic Earth — slow axial rotation, gentle float, glow pulse
earthMesh.rotation.y = elapsed * EARTH_ROTATION_SPEED;
// Animate holographic Earth — rotation speed tethered to totalActivity()
// Idle system = very slow (0.005), active system = faster (0.05)
const earthActivity = totalActivity();
const targetEarthSpeed = 0.005 + earthActivity * 0.045;
// Smooth interpolation — don't jump
const _eSmooth = 0.02;
const currentEarthSpeed = earthMesh.userData._currentSpeed || EARTH_ROTATION_SPEED;
const smoothedEarthSpeed = currentEarthSpeed + (targetEarthSpeed - currentEarthSpeed) * _eSmooth;
earthMesh.userData._currentSpeed = smoothedEarthSpeed;
earthMesh.rotation.y += smoothedEarthSpeed;
earthSurfaceMat.uniforms.uTime.value = elapsed;
earthGlowLight.intensity = 0.30 + Math.sin(elapsed * 0.7) * 0.12;
earthGroup.position.y = EARTH_Y + Math.sin(elapsed * 0.22) * 0.6;
@@ -2603,13 +2666,13 @@ function animate() {
Math.sin(elapsed * dualBrainSprite.userData.floatSpeed + dualBrainSprite.userData.floatPhase) * 0.22;
dualBrainScanSprite.position.y = dualBrainSprite.position.y;
// Orb glow pulse
const cloudPulse = 1.2 + Math.sin(elapsed * 1.8) * 0.4;
const localPulse = 1.2 + Math.sin(elapsed * 1.8 + Math.PI) * 0.4;
// Orb glow — dim idle pulse (dual-brain offline)
const cloudPulse = 0.08 + Math.sin(elapsed * 0.6) * 0.03;
const localPulse = 0.08 + Math.sin(elapsed * 0.6 + Math.PI) * 0.03;
cloudOrbMat.emissiveIntensity = cloudPulse;
localOrbMat.emissiveIntensity = localPulse;
cloudOrbLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.3;
localOrbLight.intensity = 0.5 + Math.sin(elapsed * 1.8 + Math.PI) * 0.3;
cloudOrbLight.intensity = 0.1 + Math.sin(elapsed * 0.6) * 0.05;
localOrbLight.intensity = 0.1 + Math.sin(elapsed * 0.6 + Math.PI) * 0.05;
// Orb hover
cloudOrb.position.y = 3.0 + Math.sin(elapsed * 0.9) * 0.15;
@@ -2617,33 +2680,27 @@ function animate() {
cloudOrbLight.position.y = cloudOrb.position.y;
localOrbLight.position.y = localOrb.position.y;
// Brain pulse particles — flow along a curved arc from cloud → local orb
{
// Brain pulse particles — OFF (dual-brain system not deployed)
// Will be re-enabled with flow rate proportional to convergence delta when system deploys
if (BRAIN_PARTICLE_COUNT > 0) {
const pos = brainParticleGeo.attributes.position.array;
const startX = cloudOrb.position.x;
const endX = localOrb.position.x;
const arcHeight = 1.2; // peak height of arc above orbs
const simRate = 0.73; // simulated learning rate tied to overall score
const arcHeight = 1.2;
const simRate = 0.73;
for (let i = 0; i < BRAIN_PARTICLE_COUNT; i++) {
brainParticlePhases[i] += brainParticleSpeeds[i] * simRate * 0.016;
if (brainParticlePhases[i] > 1.0) brainParticlePhases[i] -= 1.0;
const t = brainParticlePhases[i];
// Lerp X between orbs
pos[i * 3] = startX + (endX - startX) * t;
// Arc Y: parabolic curve peaking at midpoint
const midY = (cloudOrb.position.y + localOrb.position.y) / 2 + arcHeight;
pos[i * 3 + 1] = cloudOrb.position.y + (midY - cloudOrb.position.y) * 4 * t * (1 - t)
+ (localOrb.position.y - cloudOrb.position.y) * t;
// Slight Z wobble for volume
pos[i * 3 + 2] = Math.sin(t * Math.PI * 4 + elapsed * 2 + i) * 0.12;
}
brainParticleGeo.attributes.position.needsUpdate = true;
// Colour lerp from cyan → amber based on progress (approximated via hue shift)
const pulseIntensity = 0.6 + Math.sin(elapsed * 2.0) * 0.2;
brainParticleMat.opacity = pulseIntensity;
brainParticleMat.opacity = 0.6 + Math.sin(elapsed * 2.0) * 0.2;
}
// Scanning line effect — thin horizontal line sweeps down the panel
@@ -3633,6 +3690,7 @@ function createPortals() {
portals.forEach(portal => {
const isOnline = portal.status === 'online';
const portalMat = new THREE.MeshBasicMaterial({
@@ -3642,8 +3700,8 @@ function createPortals() {
transparent: true,
opacity: 0.7,
// Offline portals are dimmed
opacity: isOnline ? 0.7 : 0.15,
blending: THREE.AdditiveBlending,
@@ -3705,8 +3763,14 @@ async function loadPortals() {
portals = await res.json();
console.log('Loaded portals:', portals);
createPortals();
// Rebuild rune ring to match portal count/colors/status
rebuildRuneRing();
// Rebuild gravity zones to align with portal positions
rebuildGravityZones();
// If audio is already running, attach positional hums to the portals now
startPortalHums();
// Run portal health checks
runPortalHealthChecks();
} catch (error) {
console.error('Failed to load portals:', error);
}
@@ -4267,18 +4331,99 @@ loadSoulMd().then(lines => { oathLines = lines; });
// === AGENT STATUS BOARD ===
const AGENT_STATUS_STUB = {
agents: [
{ name: 'claude', status: 'working', issue: 'Live agent status board (#199)', prs_today: 3, local: true },
{ name: 'gemini', status: 'idle', issue: null, prs_today: 1, local: false },
{ name: 'kimi', status: 'working', issue: 'Portal system YAML registry (#5)', prs_today: 2, local: false },
{ name: 'groq', status: 'idle', issue: null, prs_today: 0, local: false },
{ name: 'grok', status: 'dead', issue: null, prs_today: 0, local: false },
{ name: 'ollama', status: 'idle', issue: null, prs_today: 0, local: true },
]
};
// Agent status cache — refreshed from Gitea API every 5 minutes
let _agentStatusCache = null;
let _agentStatusCacheTime = 0;
const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000;
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dead: '#ff4444' };
const GITEA_BASE = 'http://143.198.27.163:3000/api/v1';
const GITEA_TOKEN = '81a88f46684e398abe081f5786a11ae9532aae2d';
const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent'];
const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama'];
/**
* Fetches real agent status from Gitea API — commits + open PRs for each agent.
* Results are cached for 5 minutes.
* @returns {Promise<{agents: Array}>}
*/
async function fetchAgentStatusFromGitea() {
const now = Date.now();
if (_agentStatusCache && (now - _agentStatusCacheTime < AGENT_STATUS_CACHE_MS)) {
return _agentStatusCache;
}
const DAY_MS = 86400000;
const HOUR_MS = 3600000;
const agents = [];
// Fetch commits from all repos in parallel
const allRepoCommits = await Promise.all(GITEA_REPOS.map(async (repo) => {
try {
const res = await fetch(`${GITEA_BASE}/repos/${repo}/commits?sha=main&limit=30&token=${GITEA_TOKEN}`);
if (!res.ok) return [];
return await res.json();
} catch { return []; }
}));
// Fetch open PRs from the-nexus
let openPRs = [];
try {
const prRes = await fetch(`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=50&token=${GITEA_TOKEN}`);
if (prRes.ok) openPRs = await prRes.json();
} catch { /* ignore */ }
for (const agentName of AGENT_NAMES) {
const nameLower = agentName.toLowerCase();
const allCommits = [];
for (const repoCommits of allRepoCommits) {
if (!Array.isArray(repoCommits)) continue;
const matching = repoCommits.filter(c =>
(c.commit?.author?.name || '').toLowerCase().includes(nameLower)
);
allCommits.push(...matching);
}
// Determine status based on most recent commit
let status = 'dormant';
let lastSeen = null;
let currentWork = null;
if (allCommits.length > 0) {
allCommits.sort((a, b) =>
new Date(b.commit.author.date) - new Date(a.commit.author.date)
);
const latest = allCommits[0];
const commitTime = new Date(latest.commit.author.date).getTime();
lastSeen = latest.commit.author.date;
currentWork = latest.commit.message.split('\n')[0];
if (now - commitTime < HOUR_MS) status = 'working';
else if (now - commitTime < DAY_MS) status = 'idle';
else status = 'dormant';
}
// Count open PRs for this agent
const agentPRs = openPRs.filter(pr =>
(pr.user?.login || '').toLowerCase().includes(nameLower) ||
(pr.head?.label || '').toLowerCase().includes(nameLower)
);
agents.push({
name: agentName.toLowerCase(),
status,
issue: currentWork,
prs_today: agentPRs.length,
local: nameLower === 'ollama',
});
}
_agentStatusCache = { agents };
_agentStatusCacheTime = now;
return _agentStatusCache;
}
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dormant: '#334466', dead: '#ff4444', unreachable: '#ff4444' };
/**
* Builds a canvas texture for a single agent holo-panel.
@@ -4432,48 +4577,44 @@ function rebuildAgentPanels(statusData) {
}
/**
* Fetches live agent status, falling back to the stub when the endpoint is unavailable.
* @returns {Promise<typeof AGENT_STATUS_STUB>}
* Fetches live agent status from the Gitea API.
* Shows "UNREACHABLE" if the API call fails entirely.
* @returns {Promise<{agents: Array}>}
*/
async function fetchAgentStatus() {
try {
const res = await fetch('/api/status.json');
if (!res.ok) throw new Error('status ' + res.status);
return await res.json();
return await fetchAgentStatusFromGitea();
} catch {
return AGENT_STATUS_STUB;
return { agents: AGENT_NAMES.map(n => ({
name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false,
})) };
}
}
async function refreshAgentBoard() {
const data = await fetchAgentStatus();
rebuildAgentPanels(data);
// Update active agent count for energy beam tethering
_activeAgentCount = data.agents.filter(a => a.status === 'working').length;
}
// Initial render, then poll every 30 s
// Initial render, then poll every 5 min (matching API cache interval)
refreshAgentBoard();
setInterval(refreshAgentBoard, 30000);
setInterval(refreshAgentBoard, AGENT_STATUS_CACHE_MS);
// === LORA ADAPTER STATUS PANEL ===
// Holographic panel showing which LoRA fine-tuning adapters are currently active.
// Reads from lora-status.json, falls back to stub data when unavailable.
// Holographic panel showing LoRA fine-tuning adapter status.
// Shows honest empty state when no adapters are deployed.
const LORA_STATUS_STUB = {
adapters: [
{ name: 'timmy-voice-v3', base: 'mistral-7b', active: true, strength: 0.85 },
{ name: 'nexus-style-v2', base: 'llama-3-8b', active: true, strength: 0.70 },
{ name: 'sovereign-tone-v1', base: 'phi-3-mini', active: false, strength: 0.50 },
{ name: 'btc-domain-v1', base: 'mistral-7b', active: true, strength: 0.60 },
],
updated: '',
};
// No LoRA stub — honest empty state when no adapters are deployed
const LORA_ACTIVE_COLOR = '#00ff88'; // green — adapter is loaded
const LORA_INACTIVE_COLOR = '#334466'; // dim blue — adapter is off
/**
* Builds a canvas texture for the LoRA status panel.
* @param {typeof LORA_STATUS_STUB} data
* Shows honest empty state when no adapters are deployed.
* @param {{ adapters: Array }|null} data
* @returns {THREE.CanvasTexture}
*/
function createLoRAPanelTexture(data) {
@@ -4510,14 +4651,6 @@ function createLoRAPanelTexture(data) {
ctx.fillStyle = '#664488';
ctx.fillText('LoRA ADAPTERS', 14, 38);
// Active count badge (top-right)
const activeCount = data.adapters.filter(a => a.active).length;
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = LORA_ACTIVE_COLOR;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${data.adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
// Separator
ctx.strokeStyle = '#2a1a44';
ctx.lineWidth = 1;
@@ -4526,31 +4659,43 @@ function createLoRAPanelTexture(data) {
ctx.lineTo(W - 14, 46);
ctx.stroke();
// Adapter rows
// Honest empty state — no adapters deployed
if (!data || !data.adapters || data.adapters.length === 0) {
ctx.font = 'bold 18px "Courier New", monospace';
ctx.fillStyle = '#334466';
ctx.textAlign = 'center';
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#223344';
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
ctx.textAlign = 'left';
return new THREE.CanvasTexture(canvas);
}
// If adapters exist in the future, render them
const activeCount = data.adapters.filter(a => a.active).length;
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = LORA_ACTIVE_COLOR;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${data.adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
const ROW_H = 44;
data.adapters.forEach((adapter, i) => {
const rowY = 50 + i * ROW_H;
const col = adapter.active ? LORA_ACTIVE_COLOR : LORA_INACTIVE_COLOR;
// Status dot
ctx.beginPath();
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
// Adapter name
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
ctx.fillText(adapter.name, 36, rowY + 16);
// Base model (right-aligned)
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#556688';
ctx.textAlign = 'right';
ctx.fillText(adapter.base, W - 14, rowY + 16);
ctx.textAlign = 'left';
// Strength bar
if (adapter.active) {
const BAR_X = 36, BAR_W = W - 80, BAR_Y = rowY + 22, BAR_H = 5;
ctx.fillStyle = '#0a1428';
@@ -4559,14 +4704,7 @@ function createLoRAPanelTexture(data) {
ctx.globalAlpha = 0.7;
ctx.fillRect(BAR_X, BAR_Y, BAR_W * adapter.strength, BAR_H);
ctx.globalAlpha = 1.0;
ctx.font = '9px "Courier New", monospace';
ctx.fillStyle = col;
ctx.textAlign = 'right';
ctx.fillText(`${Math.round(adapter.strength * 100)}%`, W - 14, rowY + 28);
ctx.textAlign = 'left';
}
// Row divider (except after last)
if (i < data.adapters.length - 1) {
ctx.strokeStyle = '#1a0a2a';
ctx.lineWidth = 1;
@@ -4589,7 +4727,7 @@ let loraPanelSprite = null;
/**
* (Re)builds the LoRA panel sprite from fresh data.
* @param {typeof LORA_STATUS_STUB} data
* @param {{ adapters: Array }|null} data
*/
function rebuildLoRAPanel(data) {
if (loraPanelSprite) {
@@ -4618,23 +4756,61 @@ function rebuildLoRAPanel(data) {
}
/**
* Fetches live LoRA adapter status, falling back to stub when unavailable.
* Renders the LoRA panel with honest empty state — no adapters deployed.
*/
async function loadLoRAStatus() {
try {
const res = await fetch('./lora-status.json');
if (!res.ok) throw new Error('not found');
const data = await res.json();
if (!Array.isArray(data.adapters)) throw new Error('invalid');
rebuildLoRAPanel(data);
} catch {
rebuildLoRAPanel(LORA_STATUS_STUB);
}
function loadLoRAStatus() {
rebuildLoRAPanel({ adapters: [] });
}
loadLoRAStatus();
// Refresh every 60 s so live updates propagate
setInterval(loadLoRAStatus, 60000);
// === PORTAL HEALTH CHECKS ===
// Probes portal destination URLs to verify they're actually reachable.
// Uses portals.json status as the baseline — since all are currently "offline", this is honest.
// Health check runs every 5 minutes to detect if a portal comes online.
const PORTAL_HEALTH_CHECK_MS = 5 * 60 * 1000;
/**
* Runs a health check against each portal's destination URL.
* Updates portal status and refreshes visuals (runes, gravity zones).
*/
async function runPortalHealthChecks() {
if (portals.length === 0) return;
for (const portal of portals) {
if (!portal.destination?.url) {
portal.status = 'offline';
continue;
}
try {
await fetch(portal.destination.url, {
mode: 'no-cors',
signal: AbortSignal.timeout(5000),
});
// Any response at all means the server is up
portal.status = 'online';
} catch {
portal.status = 'offline';
}
}
// Refresh rune ring and gravity zones with updated portal statuses
rebuildRuneRing();
rebuildGravityZones();
// Update portal mesh visuals — dim offline portals
for (const child of portalGroup.children) {
const portalId = child.name.replace('portal-', '');
const portalData = portals.find(p => p.id === portalId);
if (portalData) {
const isOnline = portalData.status === 'online';
child.material.opacity = isOnline ? 0.7 : 0.15;
}
}
}
// Schedule recurring health checks
setInterval(runPortalHealthChecks, PORTAL_HEALTH_CHECK_MS);
// === WEATHER SYSTEM — Lempster NH ===
// Fetches real current weather from Open-Meteo (no API key required).
@@ -4771,15 +4947,20 @@ function updateWeatherHUD(wx) {
*/
async function fetchWeather() {
try {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}&current=temperature_2m,weather_code,wind_speed_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&forecast_days=1`;
const url = `https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}&current=temperature_2m,weather_code,wind_speed_10m,cloud_cover&temperature_unit=fahrenheit&wind_speed_unit=mph&forecast_days=1`;
const res = await fetch(url);
if (!res.ok) throw new Error('weather fetch failed');
const data = await res.json();
const cur = data.current;
const code = cur.weather_code;
const { condition, icon } = weatherCodeToLabel(code);
weatherState = { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon };
const cloudcover = typeof cur.cloud_cover === 'number' ? cur.cloud_cover : 50;
weatherState = { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover };
applyWeatherToScene(weatherState);
// Tether cloud layer density to real weather cloudcover
const cloudOpacity = 0.05 + (cloudcover / 100) * 0.55; // range [0.05, 0.60]
cloudMaterial.uniforms.uDensity.value = 0.3 + (cloudcover / 100) * 0.7; // range [0.3, 1.0]
cloudMaterial.opacity = cloudOpacity;
updateWeatherHUD(weatherState);
} catch {
// Silently use defaults — no weather data available
@@ -4793,12 +4974,13 @@ setInterval(fetchWeather, WEATHER_REFRESH_MS);
// === GRAVITY ANOMALY ZONES ===
// Areas where particles defy gravity and float upward.
// Each zone has a glowing floor ring and a rising particle stream.
// Tethered to portal positions and status — active portals have stronger anomalies.
const GRAVITY_ANOMALY_FLOOR = 0.2; // Y where particles respawn (ground level)
const GRAVITY_ANOMALY_CEIL = 16.0; // Y where particles wrap back to floor
const GRAVITY_ZONES = [
// Default zones — replaced when portals load
let GRAVITY_ZONES = [
{ x: -8, z: -6, radius: 3.5, color: 0x00ffcc, particleCount: 180 },
{ x: 10, z: 4, radius: 3.0, color: 0xaa44ff, particleCount: 160 },
{ x: -3, z: 9, radius: 2.5, color: 0xff8844, particleCount: 140 },
@@ -4867,6 +5049,51 @@ const gravityZoneObjects = GRAVITY_ZONES.map((zone) => {
return { zone, ring, ringMat, disc, discMat, points, geo, driftPhases, velocities };
});
/**
* Rebuilds gravity anomaly zones to align with portal positions.
* Active/online portals get stronger anomaly; offline portals get weaker effect.
*/
function rebuildGravityZones() {
if (portals.length === 0) return;
// Update existing zone positions/intensities to match portal data
for (let i = 0; i < Math.min(portals.length, gravityZoneObjects.length); i++) {
const portal = portals[i];
const gz = gravityZoneObjects[i];
const isOnline = portal.status === 'online';
const portalColor = new THREE.Color(portal.color);
// Reposition ring and disc to portal position
gz.ring.position.set(portal.position.x, GRAVITY_ANOMALY_FLOOR + 0.05, portal.position.z);
gz.disc.position.set(portal.position.x, GRAVITY_ANOMALY_FLOOR + 0.04, portal.position.z);
// Update zone reference for particle respawn
gz.zone.x = portal.position.x;
gz.zone.z = portal.position.z;
gz.zone.color = portalColor.getHex();
// Update colors
gz.ringMat.color.copy(portalColor);
gz.discMat.color.copy(portalColor);
gz.points.material.color.copy(portalColor);
// Offline portals: reduced opacity/intensity
gz.ringMat.opacity = isOnline ? 0.4 : 0.08;
gz.discMat.opacity = isOnline ? 0.04 : 0.01;
gz.points.material.opacity = isOnline ? 0.7 : 0.15;
// Reposition particles around portal
const pos = gz.geo.attributes.position.array;
for (let j = 0; j < gz.zone.particleCount; j++) {
const angle = Math.random() * Math.PI * 2;
const r = Math.sqrt(Math.random()) * gz.zone.radius;
pos[j * 3] = gz.zone.x + Math.cos(angle) * r;
pos[j * 3 + 2] = gz.zone.z + Math.sin(angle) * r;
}
gz.geo.attributes.position.needsUpdate = true;
}
}
// === TIMMY SPEECH BUBBLE ===
// When Timmy sends a chat message, a glowing floating text sprite appears near
// his avatar position above the platform. Fades in quickly, holds for 5 s total,
@@ -5149,6 +5376,8 @@ async function fetchBlockHeight() {
// Force reflow so animation restarts
void blockHeightDisplay.offsetWidth;
blockHeightDisplay.classList.add('fresh');
// Pulse stars — chain heartbeat
_starPulseIntensity = 1.0;
}
lastKnownBlockHeight = height;