diff --git a/CLAUDE.md b/CLAUDE.md index 9251335..9db3884 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,3 +75,92 @@ npx serve . -l 3000 Base URL: http://143.198.27.163:3000/api/v1 Repo: Timmy_Foundation/the-nexus ``` + +--- + +## Nexus Data Integrity Standard + +**This is law. Every contributor — human or AI — must follow these rules. No exceptions.** + +### Core Principle + +Every visual element in the Nexus must be tethered to reality. Nothing displayed may present fabricated data as if it were live. If a system is offline, the Nexus shows it as offline. If data doesn't exist yet, the element shows an honest empty state. There are zero acceptable reasons to display mocked data in the Nexus. + +### The Three Categories + +Every visual element falls into exactly one category: + +1. **REAL** — Connected to a live data source (API, file, computed value). Displays truthful, current information. Examples: commit heatmap from Gitea, weather from Open-Meteo, Bitcoin block height. + +2. **HONEST-OFFLINE** — The system it represents doesn't exist yet or is currently unreachable. The element is visible but clearly shows its offline/empty/awaiting state. Dim colors, empty bars, "OFFLINE" or "AWAITING DEPLOYMENT" labels. No fake numbers. Examples: dual-brain panel before deployment, LoRA panel with no adapters trained. + +3. **DATA-TETHERED AESTHETIC** — Visually beautiful and apparently decorative, but its behavior (speed, density, brightness, color, intensity) is driven by a real data stream. The connection doesn't need to be obvious to the viewer, but it must exist in code. Examples: matrix rain density driven by commit activity, star brightness pulsing on Bitcoin blocks, cloud layer density from weather data. + +### Banned Practices + +- **No hardcoded stubs presented as live data.** No `AGENT_STATUS_STUB`, no `LORA_STATUS_STUB`, no hardcoded scores. If the data source isn't ready, show an empty/offline state. +- **No static JSON files pretending to be APIs.** Files like `api/status.json` with hardcoded agent statuses are lies. Either fetch from the real API or show the element as disconnected. +- **No fictional artifacts.** Files like `lora-status.json` containing invented adapter names that don't exist must be deleted. The filesystem must not contain fiction. +- **No untethered aesthetics.** Every moving, glowing, or animated element must be connected to at least one real data stream. Pure decoration with no data connection is not permitted. Constellation lines (structural) are the sole exception. +- **No "online" status for unreachable services.** If a URL doesn't respond to a health check, it is offline. The Nexus does not lie about availability. + +### PR Requirements (Mandatory) + +Every PR to this repository must include: + +1. **Data Integrity Audit** — A table in the PR description listing every visual element the PR touches, its category (REAL / HONEST-OFFLINE / DATA-TETHERED AESTHETIC), and the data source it connects to. Format: + + ``` + | Element | Category | Data Source | + |---------|----------|-------------| + | Agent Status Board | REAL | Gitea API /repos/.../commits | + | Matrix Rain | DATA-TETHERED AESTHETIC | zoneIntensity (commit count) | + | Dual-Brain Panel | HONEST-OFFLINE | Shows "AWAITING DEPLOYMENT" | + ``` + +2. **Test Plan** — Specific steps to verify that every changed element displays truthful data or an honest offline state. Include: + - How to trigger each state (online, offline, empty, active) + - What the element should look like in each state + - How to confirm the data source is real (API endpoint, computed value, etc.) + +3. **Verification Screenshot** — At least one screenshot or recording showing the before-and-after state of changed elements. The screenshot must demonstrate: + - Elements displaying real data or honest offline states + - No hardcoded stubs visible + - Aesthetic elements visibly responding to their data tether + +4. **Syntax Check** — `node --check app.js` must pass. (Existing rule, restated for completeness.) + +A PR missing any of these four items must not be merged. + +### Existing Element Registry + +Canonical reference for every Nexus element and its required data source: + +| # | Element | Category | Data Source | Status | +|---|---------|----------|-------------|--------| +| 1 | Commit Heatmap | REAL | Gitea commits API | ✅ Connected | +| 2 | Weather System | REAL | Open-Meteo API | ✅ Connected | +| 3 | Bitcoin Block Height | REAL | blockstream.info | ✅ Connected | +| 4 | Commit Banners | REAL | Gitea commits API | ✅ Connected | +| 5 | Floating Bookshelves / Oath | REAL | SOUL.md file | ✅ Connected | +| 6 | Portal System | REAL + Health Check | portals.json + URL probe | ✅ Connected | +| 7 | Dual-Brain Panel | HONEST-OFFLINE | — (system not deployed) | ✅ Honest | +| 8 | Agent Status Board | REAL | Gitea API (commits + PRs) | ✅ Connected | +| 9 | LoRA Panel | HONEST-OFFLINE | — (no adapters deployed) | ✅ Honest | +| 10 | Sovereignty Meter | REAL (manual) | sovereignty-status.json + MANUAL label | ✅ Connected | +| 11 | Matrix Rain | DATA-TETHERED AESTHETIC | zoneIntensity (commits) + commit hashes | ✅ Tethered | +| 12 | Star Field | DATA-TETHERED AESTHETIC | Bitcoin block events (brightness pulse) | ✅ Tethered | +| 13 | Constellation Lines | STRUCTURAL (exempt) | — | ✅ No change needed | +| 14 | Crystal Formations | DATA-TETHERED AESTHETIC | totalActivity() | 🔍 Verify connection | +| 15 | Cloud Layer | DATA-TETHERED AESTHETIC | Weather API (cloud_cover) | ✅ Tethered | +| 16 | Rune Ring | DATA-TETHERED AESTHETIC | portals.json (count + status + colors) | ✅ Tethered | +| 17 | Holographic Earth | DATA-TETHERED AESTHETIC | totalActivity() (rotation speed) | ✅ Tethered | +| 18 | Energy Beam | DATA-TETHERED AESTHETIC | Active agent count | ✅ Tethered | +| 19 | Gravity Anomaly Zones | DATA-TETHERED AESTHETIC | Portal positions + status | ✅ Tethered | +| 20 | Brain Pulse Particles | HONEST-OFFLINE | — (dual-brain not deployed, particles OFF) | ✅ Honest | + +When a new visual element is added, it must be added to this registry in the same PR. + +### Enforcement + +Any agent or contributor that introduces mocked data, untethered aesthetics, or fake statuses into the Nexus is in violation of this standard. The merge-bot should reject PRs that lack the required audit table, test plan, or verification screenshot. This standard is permanent and retroactive — existing violations must be fixed, not grandfathered. diff --git a/app.js b/app.js index 8fc0a80..7713964 100644 --- a/app.js +++ b/app.js @@ -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} + * 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}¤t=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}¤t=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; diff --git a/lora-status.json b/lora-status.json deleted file mode 100644 index 9ae41e3..0000000 --- a/lora-status.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "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": "2026-03-24T00:00:00Z" -} diff --git a/portals.json b/portals.json index f319cf0..5bfbdc8 100644 --- a/portals.json +++ b/portals.json @@ -3,7 +3,7 @@ "id": "morrowind", "name": "Morrowind", "description": "The Vvardenfell harness. Ash storms and ancient mysteries.", - "status": "online", + "status": "offline", "color": "#ff6600", "position": { "x": 15, "y": 0, "z": -10 }, "rotation": { "y": -0.5 }, @@ -17,7 +17,7 @@ "id": "bannerlord", "name": "Bannerlord", "description": "Calradia battle harness. Massive armies, tactical command.", - "status": "online", + "status": "offline", "color": "#ffd700", "position": { "x": -15, "y": 0, "z": -10 }, "rotation": { "y": 0.5 }, @@ -31,7 +31,7 @@ "id": "workshop", "name": "Workshop", "description": "The creative harness. Build, script, and manifest.", - "status": "online", + "status": "offline", "color": "#4af0c0", "position": { "x": 0, "y": 0, "z": -20 }, "rotation": { "y": 0 }, diff --git a/sovereignty-status.json b/sovereignty-status.json index 1926bb9..8d26375 100644 --- a/sovereignty-status.json +++ b/sovereignty-status.json @@ -2,5 +2,6 @@ "score": 85, "local": 85, "cloud": 15, - "label": "Mostly Sovereign" + "label": "Mostly Sovereign", + "assessment_type": "MANUAL" }