diff --git a/app.js b/app.js index d4d82d7..e84efe3 100644 --- a/app.js +++ b/app.js @@ -1,11 +1,25 @@ // === THE NEXUS — Main Entry Point === import * as THREE from 'three'; -import { S, Broadcaster } from './modules/state.js'; -import { NEXUS } from './modules/constants.js'; +import { S } from './modules/state.js'; import { scene, camera, renderer, composer } from './modules/scene-setup.js'; -import { clock, warpPass } from './modules/warp.js'; +import { clock } from './modules/warp.js'; import { nostr } from './modules/nostr.js'; import { createNostrPanelTexture } from './modules/nostr-panel.js'; +import { globalTicker } from './modules/core/ticker.js'; + +// === PANELS === +import { init as initHeatmap } from './modules/panels/heatmap.js'; +import { init as initAgentBoard } from './modules/panels/agent-board.js'; +import { init as initDualBrain } from './modules/panels/dual-brain.js'; +import { init as initLoraPanel } from './modules/panels/lora-panel.js'; +import { init as initSovereignty } from './modules/panels/sovereignty.js'; +import { init as initEarth } from './modules/panels/earth.js'; + +// === DATA === +import { refreshCommitData, refreshAgentData, AGENT_STATUS_CACHE_MS } from './modules/data/gitea.js'; +import { fetchWeatherData, WEATHER_REFRESH_MS } from './modules/data/weather.js'; +import { fetchBlockHeight, BITCOIN_REFRESH_MS } from './modules/data/bitcoin.js'; +import { fetchSovereigntyStatus } from './modules/data/loaders.js'; // === NOSTR INIT === nostr.connect(); @@ -17,26 +31,44 @@ nostrPanel.position.set(-6, 3.5, -7.5); nostrPanel.rotation.y = 0.4; scene.add(nostrPanel); +// === PANEL INIT === +initHeatmap(scene); +initAgentBoard(scene); +initDualBrain(scene); +initLoraPanel(scene); +initSovereignty(scene); +initEarth(scene); + +// === DATA BOOTSTRAP === +refreshCommitData(); +refreshAgentData(); +fetchWeatherData().catch(() => {}); +fetchBlockHeight(); +fetchSovereigntyStatus().catch(() => {}); +setInterval(refreshCommitData, AGENT_STATUS_CACHE_MS); +setInterval(refreshAgentData, AGENT_STATUS_CACHE_MS); +setInterval(() => fetchWeatherData().catch(() => {}), WEATHER_REFRESH_MS); +setInterval(fetchBlockHeight, BITCOIN_REFRESH_MS); + // === MAIN ANIMATION LOOP === function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); const elapsed = clock.elapsedTime; - // Update Nostr UI periodically or on event if (Math.random() > 0.95) { updateNostrUI(); nostrTexture.needsUpdate = true; } - // Visual pulse on energy beam if (S.energyBeamPulse > 0) { S.energyBeamPulse -= delta * 2; if (S.energyBeamPulse < 0) S.energyBeamPulse = 0; } + globalTicker.tick(delta, elapsed); composer.render(); } animate(); -console.log('Nexus Sovereign Node: NOSTR CONNECTED.'); +console.log('Nexus Sovereign Node: PANELS LIVE.'); diff --git a/modules/core/theme.js b/modules/core/theme.js index d8e1c5e..92a2837 100644 --- a/modules/core/theme.js +++ b/modules/core/theme.js @@ -1,17 +1,52 @@ -export const THEME = { - glass: { - color: 0x112244, - opacity: 0.35, - roughness: 0.05, - metalness: 0.1, - transmission: 0.95, - thickness: 0.8, - ior: 1.5 +// modules/core/theme.js — NEXUS design system: colors, fonts, line weights, glow params +// All visual constants live here. No inline hex codes allowed in any other module. + +export const NEXUS = { + theme: { + // Typography + fontMono: '"Courier New", monospace', + + // Panel surfaces (CSS strings — canvas / DOM) + panelBg: 'rgba(0, 6, 20, 0.90)', + panelBorderFaint: '#1a3a6a', + panelDim: '#556688', + panelText: '#ccd6f6', + panelVeryDim: '#334466', + + // Primary accent + accent: 0x4488ff, // hex integer — THREE.Color / PointLight + accentStr: '#4488ff', // CSS string — canvas / DOM + + // Agent status (CSS strings — canvas fillStyle / strokeStyle) + agentWorking: '#00ff88', + agentIdle: '#4488ff', + agentDormant: '#334466', + agentDormantHex: '#223366', // dim offline; safe for new THREE.Color() + agentDead: '#ff4444', + + // Sovereignty arc gauge + sovereignHighHex: 0x00ff88, // hex integer — THREE.Color.setHex() + sovereignMidHex: 0x4488ff, + sovereignLowHex: 0xff4444, + sovereignHigh: '#00ff88', // CSS string — canvas fillStyle + sovereignMid: '#4488ff', + sovereignLow: '#ff4444', + + // LoRA panel (CSS strings) + loraAccent: '#cc44ff', + loraActive: '#00ff88', + loraInactive: '#334466', + + // Holographic Earth (hex integers — THREE materials) + earthOcean: 0x0a1f3f, + earthLand: 0x1a4030, + earthGlow: 0x4488ff, + earthAtm: 0x2266bb, }, - text: { - primary: '#4af0c0', - secondary: '#7b5cff', - white: '#ffffff', - dim: '#a0b8d0' - } +}; + +// Legacy constants retained for scene-setup compat +export const THEME = { + glass: { color: 0x112244, opacity: 0.35, roughness: 0.05, metalness: 0.1, transmission: 0.95, thickness: 0.8, ior: 1.5 }, + text: { primary: '#4af0c0', secondary: '#7b5cff', white: '#ffffff', dim: '#a0b8d0' }, }; diff --git a/modules/core/ticker.js b/modules/core/ticker.js index 333e2c3..cd160be 100644 --- a/modules/core/ticker.js +++ b/modules/core/ticker.js @@ -8,3 +8,8 @@ export class Ticker { } } export const globalTicker = new Ticker(); + +/** Convenience: add a callback to the global animation ticker. */ +export function subscribe(fn) { + globalTicker.subscribe(fn); +} diff --git a/modules/data/bitcoin.js b/modules/data/bitcoin.js index 611f010..3dcb9ce 100644 --- a/modules/data/bitcoin.js +++ b/modules/data/bitcoin.js @@ -1,6 +1,8 @@ // modules/data/bitcoin.js — Blockstream block height polling -// Writes to S: lastKnownBlockHeight, _starPulseIntensity +// Writes to S: lastKnownBlockHeight, _starPulseIntensity (legacy) +// Writes to state: blockHeight, lastBlockHeight, newBlockDetected, starPulseIntensity import { S } from '../state.js'; +import { state } from '../core/state.js'; const BITCOIN_REFRESH_MS = 60 * 1000; @@ -11,12 +13,15 @@ export async function fetchBlockHeight() { const height = parseInt(await res.text(), 10); if (isNaN(height)) return null; - const isNew = S.lastKnownBlockHeight !== null && height > S.lastKnownBlockHeight; + const prev = S.lastKnownBlockHeight; + const isNew = prev !== null && height > prev; S.lastKnownBlockHeight = height; + if (isNew) S._starPulseIntensity = 1.0; - if (isNew) { - S._starPulseIntensity = 1.0; - } + state.blockHeight = height; + state.lastBlockHeight = prev || 0; + state.newBlockDetected = isNew; + if (isNew) state.starPulseIntensity = 1.0; return { height, isNewBlock: isNew }; } catch { diff --git a/modules/data/gitea.js b/modules/data/gitea.js index 1eeb4d3..3706505 100644 --- a/modules/data/gitea.js +++ b/modules/data/gitea.js @@ -1,6 +1,17 @@ // modules/data/gitea.js — All Gitea API calls -// Writes to S: _activeAgentCount, _matrixCommitHashes, agentStatus +// Writes to S: _activeAgentCount, _matrixCommitHashes (legacy) +// Writes to state: agentStatus, activeAgentCount, zoneIntensity, commits, commitHashes import { S } from '../state.js'; +import { state } from '../core/state.js'; + +// Zone intensity — agent name → author regex (mirrors panels/heatmap.js HEATMAP_ZONES) +const _ZONE_PATTERNS = [ + { name: 'Claude', pattern: /^claude$/i }, + { name: 'Timmy', pattern: /^timmy/i }, + { name: 'Kimi', pattern: /^kimi/i }, + { name: 'Perplexity', pattern: /^perplexity/i }, +]; +const _ZONE_MAX_WEIGHT = 8; const GITEA_BASE = 'http://143.198.27.163:3000/api/v1'; const GITEA_TOKEN = 'dc0517a965226b7a0c5ffdd961b1ba26521ac592'; @@ -119,22 +130,49 @@ export async function fetchAgentStatus() { export async function refreshCommitData() { const commits = await fetchNexusCommits(); - S._matrixCommitHashes = commits.slice(0, 20) - .map(c => (c.sha || '').slice(0, 7)) - .filter(h => h.length > 0); + const hashes = commits.slice(0, 20).map(c => (c.sha || '').slice(0, 7)).filter(Boolean); + + // Legacy write + S._matrixCommitHashes = hashes; + + // Core state writes + state.commits = commits; + state.commitHashes = hashes; + + // Compute per-zone intensity (24 h decay window) + const now = Date.now(); + const rawWeights = {}; + for (const c of commits) { + const author = c.commit?.author?.name || c.author?.login || ''; + const age = now - new Date(c.commit?.author?.date || 0).getTime(); + if (age > DAY_MS) continue; + const weight = 1 - age / DAY_MS; + for (const { name, pattern } of _ZONE_PATTERNS) { + if (pattern.test(author)) { rawWeights[name] = (rawWeights[name] || 0) + weight; break; } + } + } + state.zoneIntensity = Object.fromEntries( + _ZONE_PATTERNS.map(z => [z.name, Math.min((rawWeights[z.name] || 0) / _ZONE_MAX_WEIGHT, 1)]) + ); + return commits; } export async function refreshAgentData() { try { const data = await fetchAgentStatus(); - S._activeAgentCount = data.agents.filter(a => a.status === 'working').length; + const count = data.agents.filter(a => a.status === 'working').length; + S._activeAgentCount = count; + state.agentStatus = data; + state.activeAgentCount = count; return data; } catch { const fallback = { agents: AGENT_NAMES.map(n => ({ name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false, })) }; S._activeAgentCount = 0; + state.agentStatus = fallback; + state.activeAgentCount = 0; return fallback; } } diff --git a/modules/data/loaders.js b/modules/data/loaders.js index a0da6a8..f378669 100644 --- a/modules/data/loaders.js +++ b/modules/data/loaders.js @@ -1,6 +1,8 @@ // modules/data/loaders.js — Static file loaders (portals.json, sovereignty-status.json, SOUL.md) -// Writes to S: sovereigntyScore, sovereigntyLabel +// Writes to S: sovereigntyScore, sovereigntyLabel (legacy) +// Writes to state: sovereignty import { S } from '../state.js'; +import { state } from '../core/state.js'; // --- SOUL.md (cached) --- let _soulMdCache = null; @@ -37,6 +39,7 @@ export async function fetchSovereigntyStatus() { S.sovereigntyScore = score; S.sovereigntyLabel = label; + state.sovereignty = { score, label, assessment_type: assessmentType }; return { score, label, assessmentType }; } catch { diff --git a/modules/data/weather.js b/modules/data/weather.js index 30e0507..c2d5a70 100644 --- a/modules/data/weather.js +++ b/modules/data/weather.js @@ -1,5 +1,6 @@ // modules/data/weather.js — Open-Meteo weather fetch -// Writes to: weatherState (returned), scene effects applied by caller +// Writes to state: weather +import { state } from '../core/state.js'; const WEATHER_LAT = 43.2897; const WEATHER_LON = -72.1479; @@ -28,7 +29,9 @@ export async function fetchWeatherData() { const code = cur.weather_code; const { condition, icon } = weatherCodeToLabel(code); const cloudcover = typeof cur.cloud_cover === 'number' ? cur.cloud_cover : 50; - return { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover }; + const result = { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover }; + state.weather = result; + return result; } export { WEATHER_REFRESH_MS };