feat: Phase 3 — wire panel modules (heatmap, agent-board, dual-brain, LoRA, sovereignty, earth)
- core/theme.js: export NEXUS with full NEXUS.theme.* properties used by all 6 panels - core/ticker.js: add subscribe() convenience export so panels can self-register - data/gitea.js: also write state.agentStatus, activeAgentCount, zoneIntensity, commits, commitHashes - data/loaders.js: also write state.sovereignty - data/bitcoin.js: also write state.blockHeight, lastBlockHeight, newBlockDetected, starPulseIntensity - data/weather.js: also write state.weather - app.js: import + init all 6 panel modules, bootstrap data polling, call globalTicker.tick() Fixes #412 Refs #409 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
44
app.js
44
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.');
|
||||
|
||||
@@ -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' },
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user