diff --git a/.gitea/workflows/smoke-test.yml b/.gitea/workflows/smoke-test.yml index 7c332352..e9788973 100644 --- a/.gitea/workflows/smoke-test.yml +++ b/.gitea/workflows/smoke-test.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Check staging environment uptime run: | - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/) + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://staging.the-nexus.com/) if [ "$HTTP_CODE" -eq 200 ]; then echo "Staging environment is up (HTTP 200)" else diff --git a/RESEARCH_DROP_456.md b/RESEARCH_DROP_456.md deleted file mode 100644 index 71493054..00000000 --- a/RESEARCH_DROP_456.md +++ /dev/null @@ -1,117 +0,0 @@ -# Research Drop - Issue #456: Ingest and Triage Work - -This document summarizes the key findings from the four PDF attachments in Issue #456, "Research Drop," and proposes how these insights can be integrated into the Nexus project, adhering to the Nexus Data Integrity Standard. - ---- - -## 1. Lean Manufacturing Implementation ($5,000 Upfront Budget with $500 Monthly Recurring Capital) - -**Summary:** This document outlines a strategy for implementing lean manufacturing principles, focusing on strategic budget allocation, a five-step implementation process, foundational lean principles (waste elimination), core lean tools (5S, Kanban), performance measurement with KPIs, and risk mitigation. - -**Relevance to Nexus & Data Integrity Proposals:** - -While not directly related to a visual element, this research can inform Timmy's internal operational efficiency. - -* **Indirect Impact (REAL Data Source):** If Timmy were to expose its internal "lean" metrics (e.g., task throughput, waste reduction, project velocity) as real-time data, these could be integrated into the Nexus. - * **Proposed Element:** A new "Timmy Operations Efficiency" panel. - * **Category:** REAL. - * **Data Source:** Timmy's internal operational metrics (e.g., a dedicated API endpoint or internal log file that can be parsed). - * **Description:** Displays key performance indicators related to Timmy's task processing efficiency, resource utilization, and adherence to lean principles. - ---- - -## 2. State-of-the-Art Open-Source Local AI Agents for Personal Neural System Development - -**Summary:** This PDF details a shift towards hybrid cloud-local AI agent architectures, emphasizing local sovereignty, reduced cloud dependency, and continuous learning through Reinforcement Learning from Human Feedback (RLHF) using the OpenClaw ecosystem. It covers architecture, deployment modes, memory systems, LORA fine-tuning, security, governance, and a roadmap. - -**Relevance to Nexus & Data Integrity Proposals:** - -This document is highly relevant to the Nexus's core mission of "Timmy's Sovereign Home" and advanced AI agent capabilities. It provides numerous opportunities to populate existing `HONEST-OFFLINE` elements and introduce new `REAL` and `DATA-TETHERED AESTHETIC` elements. - -* **Existing Element Enhancement (LoRA Panel):** - * **Proposed Enhancement:** Populate the existing "LoRA Panel" with real-time LORA training status from the OpenClaw ecosystem. - * **Category:** REAL (from HONEST-OFFLINE). - * **Data Source:** OpenClaw LORA training status API or internal module. - * **Description:** Displays active LORA fine-tuning jobs, their progress, and completion status. -* **Existing Element Enhancement (Agent Status Board):** - * **Proposed Enhancement:** Expand the "Agent Status Board" to include detailed OpenClaw agent activities (Terminal-RL, GUI-RL, SWE-RL, Toolcall-RL). - * **Category:** REAL. - * **Data Source:** OpenClaw agent activity API or internal module. - * **Description:** Provides granular status updates on different types of tasks and learning activities performed by Timmy. -* **New Element (Local Inference Metrics):** - * **Proposed Element:** "Local Inference Efficiency" display. - * **Category:** REAL. - * **Data Source:** OpenClaw inference engine metrics (e.g., percentage of local vs. cloud inference). - * **Description:** Visualizes Timmy's reliance on local processing, aiming for >90% local inference. -* **New Element (Knowledge System Metrics):** - * **Proposed Element:** "Knowledge Base Activity" display. - * **Category:** REAL. - * **Data Source:** OpenClaw memory systems (vector database size, query rates, RAG activity). - * **Description:** Shows the growth and utilization of Timmy's knowledge base. -* **New Element (Security & Governance Panel):** - * **Proposed Element:** "Agent Governance Status" panel. - * **Category:** REAL. - * **Data Source:** OpenClaw security and governance signals (sandboxing status, capability control logs, oversight signals). - * **Description:** Provides real-time insights into the security posture and human oversight of Timmy's autonomous actions. -* **Data-Tethered Aesthetic (Agent Activity Visualization):** - * **Proposed Element:** Nexus particle effects or light intensity tethered to OpenClaw agent activity levels. - * **Category:** DATA-TETHERED AESTHETIC. - * **Data Source:** OpenClaw agent activity API or internal module (e.g., a normalized activity score). - * **Description:** Dynamic visual feedback within the Nexus reflecting Timmy's current operational intensity. - ---- - -## 3. The Timmy Time Hardware Decision: A Complete Cost-to-Capability Breakdown - -**Summary:** This PDF analyzes hardware options (Apple Silicon, NVIDIA GPUs, cloud providers) for AI development, emphasizing local sovereignty. It recommends a hybrid approach and a three-phase "phased sovereignty plan" to scale hardware investment for faster fine-tuning, larger model inference, and OpenClaw-RL. - -**Relevance to Nexus & Data Integrity Proposals:** - -This document provides context for the "Sovereignty Meter" and informs potential `REAL` and `HONEST-OFFLINE` elements reflecting Timmy's hardware and capabilities. - -* **Existing Element Enhancement (Sovereignty Meter):** - * **Proposed Enhancement:** Enhance the "Sovereignty Meter" to dynamically reflect the current phase of Timmy's hardware evolution and actual local processing capabilities. - * **Category:** REAL (from REAL (manual) + JSON). - * **Data Source:** System hardware detection, OpenClaw configuration (e.g., reporting active hardware phase), or internal metrics on local computation. - * **Description:** A visual indicator of Timmy's current hardware phase (Phase 1, 2, or 3) and its resulting degree of local operational sovereignty. -* **New Element (Hardware Capabilities Panel):** - * **Proposed Element:** "Timmy Hardware Status" panel. - * **Category:** REAL / HONEST-OFFLINE. - * **Data Source:** System hardware inventory, OpenClaw hardware detection. - * **Description:** Displays currently active hardware (e.g., "M3 Max," "RTX 4090") and indicates capabilities that are "HONEST-OFFLINE" because required hardware is not yet present (e.g., "70B Model Inference: AWAITING SECOND RTX 4090"). -* **New Element (Cost Efficiency Metrics):** - * **Proposed Element:** "Operational Cost Efficiency" display. - * **Category:** REAL. - * **Data Source:** Timmy's internal cost tracking for cloud vs. local operations. - * **Description:** Visualizes the cost savings achieved through local-first hardware investments compared to cloud-only alternatives. (Requires secure and aggregated cost data). - ---- - -## 4. Wiring the Research Pipeline - -**Summary:** This PDF outlines the architecture for Timmy's Autonomous Deep Research System, aiming to automate research without human intervention. It recommends specific open-source tools (Local Deep Research, SearXNG, Crawl4AI, LanceDB, Qwen3-Embedding) for the research pipeline, detailing data flow, components, and a build order. - -**Relevance to Nexus & Data Integrity Proposals:** - -This document offers concrete components and metrics that can be directly integrated into the Nexus to represent Timmy's autonomous research capabilities. - -* **New Element (Research Pipeline Status):** - * **Proposed Element:** "Timmy Research Pipeline" panel. - * **Category:** REAL. - * **Data Source:** Autonomous Deep Research System's internal status (e.g., current stage: "Ingesting," "Processing," "Analyzing," "Synthesizing"). - * **Description:** Shows the real-time progress of Timmy's research tasks. -* **New Element (Knowledge Crystallization Metrics):** - * **Proposed Element:** "Knowledge Growth" display. - * **Category:** REAL. - * **Data Source:** Autonomous Deep Research System's knowledge base metrics (e.g., size of LanceDB, number of unique facts, growth rate). - * **Description:** Visualizes the expansion of Timmy's crystallized knowledge base. -* **New Element (Research Tool Status):** - * **Proposed Element:** "Research Tool Health" panel. - * **Category:** REAL / HONEST-OFFLINE. - * **Data Source:** Health checks or status reports from SearXNG, Crawl4AI, LanceDB components. - * **Description:** Displays the operational status of key tools within the research pipeline. -* **Data-Tethered Aesthetic (Research Activity Visualization):** - * **Proposed Element:** Nexus visual effects (e.g., light patterns, energy flows) tethered to the intensity or volume of Timmy's research activity. - * **Category:** DATA-TETHERED AESTHETIC. - * **Data Source:** Autonomous Deep Research System's activity metrics (e.g., data ingestion rate, processing load). - * **Description:** Dynamic visual feedback within the Nexus reflecting the current level of autonomous research. diff --git a/app.js b/app.js index e59fa5a5..5aa87013 100644 --- a/app.js +++ b/app.js @@ -1,47 +1,528 @@ // === THE NEXUS — Main Entry Point === -// Thin orchestrator: imports core modules, wires updates, starts the ticker. +// All modules are imported here. This file wires them together. import * as THREE from 'three'; -import { scene, composer } from './modules/core/scene.js'; -import { subscribe, start } from './modules/core/ticker.js'; -import { NEXUS } from './modules/core/theme.js'; // eslint-disable-line no-unused-vars -import { state } from './modules/core/state.js'; // eslint-disable-line no-unused-vars import { S } from './modules/state.js'; -import { warpPass } from './modules/warp.js'; -import { nostr } from './modules/nostr.js'; -import { createNostrPanelTexture } from './modules/nostr-panel.js'; +import { NEXUS } from './modules/constants.js'; +import { setAnimateFn, setTotalActivityFn } from './modules/matrix-rain.js'; +import { scene, camera, renderer, raycaster, forwardVector, + ambientLight, overheadLight, + stars, starMaterial, constellationLines, + STAR_BASE_OPACITY, STAR_PEAK_OPACITY, STAR_PULSE_DECAY } from './modules/scene-setup.js'; +import { glassEdgeMaterials, voidLight, cloudMaterial, GLASS_RADIUS } from './modules/platform.js'; +import { heatmapMat, zoneIntensity, drawHeatmap, updateHeatmap, HEATMAP_ZONES } from './modules/heatmap.js'; +import { sigilMesh, sigilMat, sigilRing1, sigilRing1Mat, + sigilRing2, sigilRing2Mat, sigilRing3, sigilRing3Mat, + sigilLight } from './modules/sigil.js'; +import { NORMAL_CAM, OVERVIEW_CAM, composer, orbitControls, bokehPass, WARP_DURATION } from './modules/controls.js'; +import { animateEnergyBeam, sovereigntyGroup, meterLight, + runeSprites, RUNE_RING_Y, RUNE_ORBIT_SPEED, rebuildRuneRing } from './modules/effects.js'; +import { earthGroup, earthMesh, earthSurfaceMat, earthGlowLight, + EARTH_Y, EARTH_ROTATION_SPEED } from './modules/earth.js'; +import { clock, warpPass, startWarp, totalActivity, + crystals, CRYSTAL_COLORS, LIGHTNING_POOL_SIZE, LIGHTNING_REFRESH_MS, + lightningArcs, lightningArcMeta, updateLightningArcs, + batcaveGroup, batcaveProbe, batcaveMetallicMats, batcaveProbeTarget_texture } from './modules/warp.js'; +import { dualBrainSprite, dualBrainLight, dualBrainScanSprite, dualBrainScanTexture, + cloudOrb, cloudOrbMat, cloudOrbLight, + localOrb, localOrbMat, localOrbLight, + BRAIN_PARTICLE_COUNT, brainParticleGeo, brainParticleMat, + brainParticlePhases, brainParticleSpeeds, _scanCtx } from './modules/dual-brain.js'; +import { updateAudioListener, initAudioListeners, startPortalHums } from './modules/audio.js'; +import { initDebug, initWebSocket, wsClient, logMessage, initSessionExport } from './modules/debug.js'; +import { triggerSovereigntyEasterEgg, triggerFireworks, triggerMergeFlash, triggerShockwave, + initSovereigntyEasterEgg, + shockwaveRings, SHOCKWAVE_DURATION, + fireworkBursts, FIREWORK_BURST_PARTICLES, FIREWORK_BURST_DURATION, FIREWORK_GRAVITY } from './modules/celebrations.js'; +import { portalGroup, portals, loadPortals, setRebuildGravityZonesFn, setRunPortalHealthChecksFn } from './modules/portals.js'; +import { commitBanners, bookshelfGroups, agentPanelSprites, + initCommitBanners, initBookshelves } from './modules/bookshelves.js'; +import { tomeGroup, tomeGlow, oathSpot, enterOath, exitOath, initOathListeners } from './modules/oath.js'; +import { loraPanelSprite, refreshAgentBoard, initAgentBoard, loadLoRAStatus } from './modules/panels.js'; +import { rainParticles, rainGeo, rainVelocities, snowParticles, snowGeo, snowDrift, + PRECIP_COUNT, PRECIP_AREA, PRECIP_HEIGHT, PRECIP_FLOOR, + runPortalHealthChecks, initPortalHealthChecks, setWeatherPortalRefs, + initWeather } from './modules/weather.js'; +import { gravityZoneObjects, GRAVITY_ANOMALY_CEIL, rebuildGravityZones, + TIMMY_SPEECH_POS, SPEECH_DURATION, SPEECH_FADE_IN, SPEECH_FADE_OUT, + showTimmySpeech, setExtrasPortalsRef, + timelapseCommits, timelapseWindow, TIMELAPSE_DURATION_S, + fireTimelapseCommit, updateTimelapseHeatmap, updateTimelapseHUD, stopTimelapse, + initTimelapse, initBitcoin } from './modules/extras.js'; -// === NOSTR INIT === -nostr.connect(); -const { canvas: nostrCanvas, update: updateNostrUI } = createNostrPanelTexture(); -const nostrTexture = new THREE.CanvasTexture(nostrCanvas); -const nostrMat = new THREE.MeshBasicMaterial({ map: nostrTexture, transparent: true, side: THREE.DoubleSide }); -const nostrPanel = new THREE.Mesh(new THREE.PlaneGeometry(3, 3), nostrMat); -nostrPanel.position.set(-6, 3.5, -7.5); -nostrPanel.rotation.y = 0.4; -scene.add(nostrPanel); +// === WIRE UP CROSS-MODULE REFERENCES === +setTotalActivityFn(totalActivity); +setAnimateFn(() => animate()); +setRebuildGravityZonesFn(rebuildGravityZones); +setRunPortalHealthChecksFn(runPortalHealthChecks); -// === MAIN UPDATE — subscribed to the single RAF loop in ticker.js === -subscribe((elapsed, delta) => { - // Update Nostr UI periodically - if (Math.random() > 0.95) { - updateNostrUI(); - nostrTexture.needsUpdate = true; +// === ANIMATION LOOP === +function animate() { + requestAnimationFrame(animate); + animateEnergyBeam(); + const elapsed = clock.getElapsedTime(); + + // Overview mode + const targetT = S.overviewMode ? 1 : 0; + S.overviewT += (targetT - S.overviewT) * 0.04; + const _basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, S.overviewT); + + // Zoom-to-object + if (!S.photoMode) { + S.zoomT += (S.zoomTargetT - S.zoomT) * 0.07; + } + if (S.zoomT > 0.001 && !S.photoMode && !S.overviewMode) { + camera.position.lerpVectors(_basePos, S._zoomCamTarget, S.zoomT); + camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(S._zoomLookTarget, S.zoomT)); + } else { + camera.position.copy(_basePos); + camera.lookAt(0, 0, 0); } - // Visual pulse on energy beam - if (S.energyBeamPulse > 0) { - S.energyBeamPulse -= delta * 2; - if (S.energyBeamPulse < 0) S.energyBeamPulse = 0; + const rotationScale = S.photoMode ? 0 : (1 - S.overviewT); + S.targetRotX += (S.mouseY * 0.3 - S.targetRotX) * 0.02; + S.targetRotY += (S.mouseX * 0.3 - S.targetRotY) * 0.02; + + stars.rotation.x = (S.targetRotX + elapsed * 0.01) * rotationScale; + stars.rotation.y = (S.targetRotY + elapsed * 0.015) * rotationScale; + + // Star pulse + if (S._starPulseIntensity > 0) { + S._starPulseIntensity = Math.max(0, S._starPulseIntensity - STAR_PULSE_DECAY); + } + starMaterial.opacity = STAR_BASE_OPACITY + (STAR_PEAK_OPACITY - STAR_BASE_OPACITY) * S._starPulseIntensity; + + constellationLines.rotation.x = stars.rotation.x; + constellationLines.rotation.y = stars.rotation.y; + constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + + // Batcave reflection probe + if (elapsed - S.batcaveProbeLastUpdate > 2.0) { + S.batcaveProbeLastUpdate = elapsed; + batcaveGroup.visible = false; + batcaveProbe.update(renderer, scene); + batcaveGroup.visible = true; + for (const mat of batcaveMetallicMats) { + mat.envMap = batcaveProbeTarget_texture.texture; + mat.needsUpdate = true; + } } - // Warp pass time uniform + // Glass platform edge glow + for (const { mat, distFromCenter } of glassEdgeMaterials) { + const phase = elapsed * 1.1 - distFromCenter * 0.18; + mat.opacity = 0.25 + Math.sin(phase) * 0.22; + } + voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2; + + heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2; + + // Sigil animation + sigilMesh.rotation.z = elapsed * 0.04; + sigilRing1.rotation.z = elapsed * 0.06; + sigilRing2.rotation.z = -elapsed * 0.10; + sigilRing3.rotation.z = elapsed * 0.08; + sigilMat.opacity = 0.65 + Math.sin(elapsed * 1.3) * 0.18; + sigilRing1Mat.opacity = 0.38 + Math.sin(elapsed * 0.9) * 0.14; + sigilRing2Mat.opacity = 0.32 + Math.sin(elapsed * 1.6 + 1.2) * 0.12; + sigilRing3Mat.opacity = 0.28 + Math.sin(elapsed * 0.7 + 2.4) * 0.10; + sigilLight.intensity = 0.30 + Math.sin(elapsed * 1.1) * 0.15; + + cloudMaterial.uniforms.uTime.value = elapsed; + + if (S.photoMode) { + orbitControls.update(); + } + + // Sovereignty meter + sovereigntyGroup.position.y = 3.8 + Math.sin(elapsed * 0.8) * 0.15; + meterLight.intensity = 0.5 + Math.sin(elapsed * 1.8) * 0.25; + + // Commit banners + const FADE_DUR = 1.5; + commitBanners.forEach(banner => { + const ud = banner.userData; + if (ud.spawnTime === null) { + if (elapsed < ud.startDelay) return; + ud.spawnTime = elapsed; + } + const age = elapsed - ud.spawnTime; + let opacity; + if (age < FADE_DUR) { + opacity = age / FADE_DUR; + } else if (age < ud.lifetime - FADE_DUR) { + opacity = 1; + } else if (age < ud.lifetime) { + opacity = (ud.lifetime - age) / FADE_DUR; + } else { + ud.spawnTime = elapsed + 3; + opacity = 0; + } + banner.material.opacity = opacity * 0.85; + banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4; + }); + + // Agent panels float + for (const sprite of agentPanelSprites) { + const ud = sprite.userData; + sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; + } + + // LoRA panel float + if (loraPanelSprite) { + const ud = loraPanelSprite.userData; + loraPanelSprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; + } + + // Bookshelves float + for (const shelf of bookshelfGroups) { + const ud = shelf.userData; + shelf.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.18; + } + + // Speech bubble + if (S.timmySpeechState) { + const age = elapsed - S.timmySpeechState.startTime; + let opacity; + if (age < SPEECH_FADE_IN) { + opacity = age / SPEECH_FADE_IN; + } else if (age < SPEECH_DURATION - SPEECH_FADE_OUT) { + opacity = 1.0; + } else if (age < SPEECH_DURATION) { + opacity = (SPEECH_DURATION - age) / SPEECH_FADE_OUT; + } else { + scene.remove(S.timmySpeechState.sprite); + if (S.timmySpeechState.sprite.material.map) S.timmySpeechState.sprite.material.map.dispose(); + S.timmySpeechState.sprite.material.dispose(); + S.timmySpeechSprite = null; + S.timmySpeechState = null; + opacity = 0; + } + if (S.timmySpeechState) { + S.timmySpeechState.sprite.material.opacity = opacity; + S.timmySpeechState.sprite.position.y = TIMMY_SPEECH_POS.y + Math.sin(elapsed * 1.1) * 0.1; + } + } + + // Tome float + tomeGroup.position.y = 5.8 + Math.sin(elapsed * 0.6) * 0.18; + tomeGroup.rotation.y = elapsed * 0.3; + tomeGlow.intensity = 0.3 + Math.sin(elapsed * 1.4) * 0.12; + if (S.oathActive) { + oathSpot.intensity = 3.8 + Math.sin(elapsed * 0.9) * 0.4; + } + + // Shockwave rings + for (let i = shockwaveRings.length - 1; i >= 0; i--) { + const ring = shockwaveRings[i]; + const age = elapsed - ring.startTime - ring.delay; + if (age < 0) continue; + const t = Math.min(age / SHOCKWAVE_DURATION, 1); + if (t >= 1) { + scene.remove(ring.mesh); + ring.mesh.geometry.dispose(); + ring.mat.dispose(); + shockwaveRings.splice(i, 1); + continue; + } + const eased = 1 - Math.pow(1 - t, 2); + ring.mesh.scale.setScalar(eased * 14 + 0.1); + ring.mat.opacity = (1 - t) * 0.9; + } + + // Fireworks + for (let i = fireworkBursts.length - 1; i >= 0; i--) { + const burst = fireworkBursts[i]; + const age = elapsed - burst.startTime; + const t = Math.min(age / FIREWORK_BURST_DURATION, 1); + if (t >= 1) { + scene.remove(burst.points); + burst.geo.dispose(); + burst.mat.dispose(); + fireworkBursts.splice(i, 1); + continue; + } + burst.mat.opacity = t < 0.6 ? 1.0 : (1.0 - t) / 0.4; + + const pos = burst.geo.attributes.position.array; + const vel = burst.velocities; + const org = burst.origins; + const halfGAge2 = 0.5 * FIREWORK_GRAVITY * age * age; + for (let j = 0; j < FIREWORK_BURST_PARTICLES; j++) { + pos[j * 3] = org[j * 3] + vel[j * 3] * age; + pos[j * 3 + 1] = org[j * 3 + 1] + vel[j * 3 + 1] * age + halfGAge2; + pos[j * 3 + 2] = org[j * 3 + 2] + vel[j * 3 + 2] * age; + } + burst.geo.attributes.position.needsUpdate = true; + } + + // Rune ring + for (const rune of runeSprites) { + const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED; + rune.sprite.position.x = Math.cos(angle) * 7.0; + rune.sprite.position.z = Math.sin(angle) * 7.0; + rune.sprite.position.y = RUNE_RING_Y + Math.sin(elapsed * 0.7 + rune.floatPhase) * 0.4; + 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; + } + + // Earth + const earthActivity = totalActivity(); + const targetEarthSpeed = 0.005 + earthActivity * 0.045; + 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; + + // Weather particles + if (rainParticles.visible) { + const rpos = rainGeo.attributes.position.array; + for (let i = 0; i < PRECIP_COUNT; i++) { + rpos[i * 3 + 1] -= rainVelocities[i]; + if (rpos[i * 3 + 1] < PRECIP_FLOOR) { + rpos[i * 3 + 1] = PRECIP_HEIGHT; + rpos[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2; + rpos[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2; + } + } + rainGeo.attributes.position.needsUpdate = true; + } + + if (snowParticles.visible) { + const spos = snowGeo.attributes.position.array; + for (let i = 0; i < PRECIP_COUNT; i++) { + spos[i * 3 + 1] -= 0.025 + Math.sin(snowDrift[i]) * 0.005; + spos[i * 3] += Math.sin(elapsed * 0.4 + snowDrift[i]) * 0.008; + if (spos[i * 3 + 1] < PRECIP_FLOOR) { + spos[i * 3 + 1] = PRECIP_HEIGHT; + spos[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2; + spos[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2; + } + } + snowGeo.attributes.position.needsUpdate = true; + } + + // Gravity anomalies + for (const gz of gravityZoneObjects) { + const pos = gz.geo.attributes.position.array; + const count = gz.zone.particleCount; + for (let i = 0; i < count; i++) { + pos[i * 3 + 1] += gz.velocities[i]; + pos[i * 3] += Math.sin(elapsed * 0.5 + gz.driftPhases[i]) * 0.003; + pos[i * 3 + 2] += Math.cos(elapsed * 0.5 + gz.driftPhases[i]) * 0.003; + if (pos[i * 3 + 1] > GRAVITY_ANOMALY_CEIL) { + const angle = Math.random() * Math.PI * 2; + const r = Math.sqrt(Math.random()) * gz.zone.radius; + pos[i * 3] = gz.zone.x + Math.cos(angle) * r; + pos[i * 3 + 1] = 0.2 + Math.random() * 2.0; + pos[i * 3 + 2] = gz.zone.z + Math.sin(angle) * r; + } + } + gz.geo.attributes.position.needsUpdate = true; + gz.ringMat.opacity = 0.3 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.15; + gz.discMat.opacity = 0.02 + Math.sin(elapsed * 1.5 + gz.zone.x) * 0.02; + } + + // Dual-brain + dualBrainSprite.position.y = dualBrainSprite.userData.baseY + + Math.sin(elapsed * dualBrainSprite.userData.floatSpeed + dualBrainSprite.userData.floatPhase) * 0.22; + dualBrainScanSprite.position.y = dualBrainSprite.position.y; + + 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.1 + Math.sin(elapsed * 0.6) * 0.05; + localOrbLight.intensity = 0.1 + Math.sin(elapsed * 0.6 + Math.PI) * 0.05; + + cloudOrb.position.y = 3.0 + Math.sin(elapsed * 0.9) * 0.15; + localOrb.position.y = 3.0 + Math.sin(elapsed * 0.9 + 1.0) * 0.15; + cloudOrbLight.position.y = cloudOrb.position.y; + localOrbLight.position.y = localOrb.position.y; + + 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; + 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]; + pos[i * 3] = startX + (endX - startX) * t; + 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; + pos[i * 3 + 2] = Math.sin(t * Math.PI * 4 + elapsed * 2 + i) * 0.12; + } + brainParticleGeo.attributes.position.needsUpdate = true; + brainParticleMat.opacity = 0.6 + Math.sin(elapsed * 2.0) * 0.2; + } + + // Scanning line + { + const W = 512, H = 512; + _scanCtx.clearRect(0, 0, W, H); + const scanY = ((elapsed * 60) % H); + _scanCtx.fillStyle = 'rgba(68, 136, 255, 0.5)'; + _scanCtx.fillRect(0, scanY, W, 2); + const grad = _scanCtx.createLinearGradient(0, scanY - 8, 0, scanY + 10); + grad.addColorStop(0, 'rgba(68, 136, 255, 0)'); + grad.addColorStop(0.4, 'rgba(68, 136, 255, 0.15)'); + grad.addColorStop(0.6, 'rgba(68, 136, 255, 0.15)'); + grad.addColorStop(1, 'rgba(68, 136, 255, 0)'); + _scanCtx.fillStyle = grad; + _scanCtx.fillRect(0, scanY - 8, W, 18); + dualBrainScanTexture.needsUpdate = true; + } + + dualBrainLight.intensity = 0.4 + Math.sin(elapsed * 1.1) * 0.2; + + // Portal collision + forwardVector.set(0, 0, -1).applyQuaternion(camera.quaternion); + raycaster.set(camera.position, forwardVector); + + const intersects = raycaster.intersectObjects(portalGroup.children); + if (intersects.length > 0) { + const intersectedPortal = intersects[0].object; + console.log(`Entered portal: ${intersectedPortal.name}`); + if (!S.isWarping) { + startWarp(intersectedPortal); + } + } + + // Warp effect if (S.isWarping) { + const warpElapsed = elapsed - S.warpStartTime; + const progress = Math.min(warpElapsed / WARP_DURATION, 1.0); warpPass.uniforms['time'].value = elapsed; + warpPass.uniforms['progress'].value = progress; + + if (!S.warpNavigated && progress >= 0.88 && S.warpDestinationUrl) { + S.warpNavigated = true; + setTimeout(() => { window.location.href = S.warpDestinationUrl; }, 180); + } + + if (progress >= 1.0) { + S.isWarping = false; + warpPass.enabled = false; + warpPass.uniforms['progress'].value = 0.0; + if (!S.warpNavigated && S.warpDestinationUrl) { + S.warpNavigated = true; + window.location.href = S.warpDestinationUrl; + } + } } + // Crystals + const activity = totalActivity(); + for (const crystal of crystals) { + crystal.mesh.position.x = crystal.basePos.x; + crystal.mesh.position.y = crystal.basePos.y + Math.sin(elapsed * 0.65 + crystal.floatPhase) * 0.35; + crystal.mesh.position.z = crystal.basePos.z; + crystal.mesh.rotation.y = elapsed * 0.4 + crystal.floatPhase; + crystal.light.position.copy(crystal.mesh.position); + const flashAge = elapsed - crystal.flashStartTime; + const flashBoost = flashAge < 0.25 ? (1.0 - flashAge / 0.25) * 2.0 : 0.0; + crystal.light.intensity = 0.2 + activity * 0.8 + Math.sin(elapsed * 2.0 + crystal.floatPhase) * 0.1 + flashBoost; + crystal.mesh.material.emissiveIntensity = 1.0 + flashBoost * 0.8; + } + + // Lightning flicker + for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) { + const meta = lightningArcMeta[i]; + if (meta.active) { + lightningArcs[i].material.opacity = meta.baseOpacity * (0.55 + Math.random() * 0.45); + } + } + + if (elapsed * 1000 - S.lastLightningRefreshTime > LIGHTNING_REFRESH_MS) { + S.lastLightningRefreshTime = elapsed * 1000; + updateLightningArcs(elapsed); + } + + // Timelapse + if (S.timelapseActive) { + const realElapsed = elapsed - S.timelapseRealStart; + S.timelapseProgress = Math.min(realElapsed / TIMELAPSE_DURATION_S, 1.0); + const span = timelapseWindow.endMs - timelapseWindow.startMs; + const virtualMs = timelapseWindow.startMs + span * S.timelapseProgress; + + while ( + S.timelapseNextCommitIdx < timelapseCommits.length && + timelapseCommits[S.timelapseNextCommitIdx].ts <= virtualMs + ) { + fireTimelapseCommit(timelapseCommits[S.timelapseNextCommitIdx]); + S.timelapseNextCommitIdx++; + } + + updateTimelapseHeatmap(virtualMs); + updateTimelapseHUD(S.timelapseProgress, virtualMs); + + if (S.timelapseProgress >= 1.0) stopTimelapse(); + } + + updateAudioListener(); composer.render(); +} + +// === START === +animate(); + +// === INIT ALL SUBSYSTEMS === +initAudioListeners(); +initDebug(); +initWebSocket(); +initSessionExport(); +initSovereigntyEasterEgg(); +initCommitBanners(); +loadPortals(); +initBookshelves(); +initOathListeners(); +initAgentBoard(); +loadLoRAStatus(); +initPortalHealthChecks(); +initWeather(); +initTimelapse(); +initBitcoin(); + +// === EVENT LISTENERS === +window.addEventListener('beforeunload', () => { + wsClient.disconnect(); }); -// === START THE SINGLE RAF LOOP === -start(); -console.log('Nexus Sovereign Node: ONLINE.'); +window.addEventListener('chat-message', (event) => { + console.log('Chat message:', event.detail); + if (typeof event.detail?.text === 'string') { + logMessage(event.detail.speaker || 'TIMMY', event.detail.text); + showTimmySpeech(event.detail.text); + if (event.detail.text.toLowerCase().includes('sovereignty')) { + triggerSovereigntyEasterEgg(); + } + if (event.detail.text.toLowerCase().includes('milestone')) { + triggerFireworks(); + } + } +}); + +window.addEventListener('milestone-complete', (event) => { + console.log('[nexus] Milestone complete:', event.detail); + triggerFireworks(); +}); + +window.addEventListener('status-update', (event) => { + console.log('[hermes] Status update:', event.detail); +}); + +window.addEventListener('pr-notification', (event) => { + console.log('[hermes] PR notification:', event.detail); + if (event.detail && event.detail.action === 'merged') { + triggerMergeFlash(); + } +}); diff --git a/modules/SovOS.js b/modules/SovOS.js deleted file mode 100644 index 81578d0b..00000000 --- a/modules/SovOS.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as THREE from 'three'; -import { THEME } from './core/theme.js'; -import { S } from './state.js'; -import { Broadcaster } from './state.js'; - -export class SovOS { - constructor(scene) { - this.scene = scene; - this.apps = new Map(); - this.init(); - } - - init() { - this.container = new THREE.Group(); - this.container.position.set(0, 3, -7.5); - this.scene.add(this.container); - } - - registerApp(id, config) { - const app = this.createWindow(id, config); - this.apps.set(id, app); - this.container.add(app.group); - } - - createWindow(id, config) { - const { x, y, rot, title, color } = config; - const w = 2.8, h = 3.8; - const group = new THREE.Group(); - group.position.set(x, y || 0, 0); - group.rotation.y = rot || 0; - - // Glassmorphism Frame - const glassMat = new THREE.MeshPhysicalMaterial({ - color: THEME.glass.color, - transparent: true, - opacity: THEME.glass.opacity, - roughness: THEME.glass.roughness, - metalness: THEME.glass.metalness, - transmission: THEME.glass.transmission, - thickness: THEME.glass.thickness, - ior: THEME.glass.ior, - side: THREE.DoubleSide - }); - const frame = new THREE.Mesh(new THREE.PlaneGeometry(w, h), glassMat); - group.add(frame); - - // Canvas UI - const canvas = document.createElement('canvas'); - canvas.width = 512; canvas.height = 700; - const ctx = canvas.getContext('2d'); - const texture = new THREE.CanvasTexture(canvas); - const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide }); - const screen = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.92, h * 0.92), mat); - screen.position.z = 0.05; - group.add(screen); - - const renderUI = (state) => { - ctx.clearRect(0, 0, 512, 700); - // Header - ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; - ctx.fillRect(0, 0, 512, 80); - ctx.fillStyle = '#' + new THREE.Color(color).getHexString(); - ctx.font = 'bold 32px "Orbitron"'; - ctx.fillText(title, 30, 50); - // Body - ctx.font = '20px "JetBrains Mono"'; - ctx.fillStyle = '#ffffff'; - config.renderBody(ctx, state); - texture.needsUpdate = true; - }; - - Broadcaster.subscribe(renderUI); - return { group, renderUI }; - } -} diff --git a/modules/audio.js b/modules/audio.js index 88e25255..1da3c9b4 100644 --- a/modules/audio.js +++ b/modules/audio.js @@ -2,7 +2,6 @@ import * as THREE from 'three'; import { camera } from './scene-setup.js'; import { S } from './state.js'; -import { fetchSoulMd } from './data/loaders.js'; const audioSources = []; const positionedPanners = []; @@ -264,10 +263,12 @@ export function initAudioListeners() { document.getElementById('podcast-toggle').addEventListener('click', () => { const btn = document.getElementById('podcast-toggle'); if (btn.textContent === '🎧') { - fetchSoulMd().then(lines => { - const text = lines.join('\n'); - return text; - }).then(text => { + fetch('SOUL.md') + .then(response => { + if (!response.ok) throw new Error('Failed to load SOUL.md'); + return response.text(); + }) + .then(text => { const paragraphs = text.split('\n\n').filter(p => p.trim()); if (!paragraphs.length) { @@ -342,5 +343,12 @@ export function initAudioListeners() { } async function loadSoulMdAudio() { - return fetchSoulMd(); + try { + const res = await fetch('SOUL.md'); + if (!res.ok) throw new Error('not found'); + const raw = await res.text(); + return raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, '')); + } catch { + return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.']; + } } diff --git a/modules/core/state.js b/modules/core/state.js deleted file mode 100644 index 6d156e98..00000000 --- a/modules/core/state.js +++ /dev/null @@ -1,35 +0,0 @@ -// modules/core/state.js — Shared reactive data bus -// Data modules write here; visual modules read from here. -// No module may call fetch() except those under modules/data/. - -export const state = { - // Commit heatmap (written by data/gitea.js) - zoneIntensity: {}, // { zoneName: [0..1], ... } - commits: [], // raw commit objects (last N) - commitHashes: [], // short hashes for matrix rain - - // Agent status (written by data/gitea.js) - agentStatus: null, // { agents: Array } | null - activeAgentCount: 0, // count of agents with status === 'working' - - // Weather (written by data/weather.js) - weather: null, // { cloud_cover, precipitation, ... } | null - - // Bitcoin (written by data/bitcoin.js) - blockHeight: 0, - lastBlockHeight: 0, - newBlockDetected: false, - starPulseIntensity: 0, - - // Portal / sovereignty / SOUL (written by data/loaders.js) - portals: [], // portal descriptor objects - sovereignty: null, // { score, label, assessment_type } | null - soulMd: '', // raw SOUL.md text - - // Computed helpers - totalActivity() { - const vals = Object.values(this.zoneIntensity); - if (vals.length === 0) return 0; - return vals.reduce((s, v) => s + v, 0) / vals.length; - }, -}; diff --git a/modules/core/ticker.js b/modules/core/ticker.js deleted file mode 100644 index ed4ae95c..00000000 --- a/modules/core/ticker.js +++ /dev/null @@ -1,31 +0,0 @@ -// modules/core/ticker.js — Global Animation Clock -// Single requestAnimationFrame loop. No module may call RAF directly. -// All modules subscribe their update(elapsed, delta) function here. -import * as THREE from 'three'; - -const _clock = new THREE.Clock(); -const _subs = []; - -/** Register an update function: fn(elapsed, delta) */ -export function subscribe(fn) { - if (!_subs.includes(fn)) _subs.push(fn); -} - -/** Remove a previously registered update function */ -export function unsubscribe(fn) { - const i = _subs.indexOf(fn); - if (i !== -1) _subs.splice(i, 1); -} - -function _tick() { - requestAnimationFrame(_tick); - const delta = _clock.getDelta(); - const elapsed = _clock.getElapsedTime(); - for (const fn of _subs) fn(elapsed, delta); -} - -/** Start the single RAF loop. Call once from app.js. */ -export function start() { - _clock.start(); - _tick(); -} diff --git a/modules/data/bitcoin.js b/modules/data/bitcoin.js deleted file mode 100644 index 611f0107..00000000 --- a/modules/data/bitcoin.js +++ /dev/null @@ -1,27 +0,0 @@ -// modules/data/bitcoin.js — Blockstream block height polling -// Writes to S: lastKnownBlockHeight, _starPulseIntensity -import { S } from '../state.js'; - -const BITCOIN_REFRESH_MS = 60 * 1000; - -export async function fetchBlockHeight() { - try { - const res = await fetch('https://blockstream.info/api/blocks/tip/height'); - if (!res.ok) return null; - const height = parseInt(await res.text(), 10); - if (isNaN(height)) return null; - - const isNew = S.lastKnownBlockHeight !== null && height > S.lastKnownBlockHeight; - S.lastKnownBlockHeight = height; - - if (isNew) { - S._starPulseIntensity = 1.0; - } - - return { height, isNewBlock: isNew }; - } catch { - return null; - } -} - -export { BITCOIN_REFRESH_MS }; diff --git a/modules/data/gitea.js b/modules/data/gitea.js deleted file mode 100644 index 1eeb4d33..00000000 --- a/modules/data/gitea.js +++ /dev/null @@ -1,163 +0,0 @@ -// modules/data/gitea.js — All Gitea API calls -// Writes to S: _activeAgentCount, _matrixCommitHashes, agentStatus -import { S } from '../state.js'; - -const GITEA_BASE = 'http://143.198.27.163:3000/api/v1'; -const GITEA_TOKEN = 'dc0517a965226b7a0c5ffdd961b1ba26521ac592'; -const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent']; -const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama']; - -const DAY_MS = 86400000; -const HOUR_MS = 3600000; -const CACHE_MS = 5 * 60 * 1000; - -let _agentStatusCache = null; -let _agentStatusCacheTime = 0; -let _commitsCache = null; -let _commitsCacheTime = 0; - -// --- Core fetchers --- - -export async function fetchNexusCommits(limit = 50) { - const now = Date.now(); - if (_commitsCache && (now - _commitsCacheTime < CACHE_MS)) return _commitsCache; - - try { - const res = await fetch( - `${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/commits?limit=${limit}`, - { headers: { 'Authorization': `token ${GITEA_TOKEN}` } } - ); - if (!res.ok) return []; - _commitsCache = await res.json(); - _commitsCacheTime = now; - return _commitsCache; - } catch { - return []; - } -} - -async function fetchRepoCommits(repo, limit = 30) { - try { - const res = await fetch( - `${GITEA_BASE}/repos/${repo}/commits?sha=main&limit=${limit}&token=${GITEA_TOKEN}` - ); - if (!res.ok) return []; - return await res.json(); - } catch { - return []; - } -} - -async function fetchOpenPRs() { - try { - const res = await fetch( - `${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=50&token=${GITEA_TOKEN}` - ); - if (res.ok) return await res.json(); - } catch { /* ignore */ } - return []; -} - -export async function fetchAgentStatus() { - const now = Date.now(); - if (_agentStatusCache && (now - _agentStatusCacheTime < CACHE_MS)) return _agentStatusCache; - - const allRepoCommits = await Promise.all(GITEA_REPOS.map(r => fetchRepoCommits(r))); - const openPRs = await fetchOpenPRs(); - - const agents = []; - 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); - } - - 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'; - } - - const agentPRs = openPRs.filter(pr => - (pr.user?.login || '').toLowerCase().includes(nameLower) || - (pr.head?.label || '').toLowerCase().includes(nameLower) - ); - - agents.push({ - name: nameLower, - status, - issue: currentWork, - prs_today: agentPRs.length, - local: nameLower === 'ollama', - }); - } - - _agentStatusCache = { agents }; - _agentStatusCacheTime = now; - return _agentStatusCache; -} - -// --- State updaters --- - -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); - return commits; -} - -export async function refreshAgentData() { - try { - const data = await fetchAgentStatus(); - S._activeAgentCount = data.agents.filter(a => a.status === 'working').length; - 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; - return fallback; - } -} - -export async function fetchMergedPRs(limit = 20) { - try { - const res = await fetch( - `${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=${limit}`, - { headers: { 'Authorization': `token ${GITEA_TOKEN}` } } - ); - if (!res.ok) return []; - const data = await res.json(); - return data - .filter(p => p.merged) - .map(p => ({ - prNum: p.number, - title: p.title - .replace(/^\[[\w\s]+\]\s*/i, '') - .replace(/\s*\(#\d+\)\s*$/, ''), - })); - } catch { - return []; - } -} - -export { GITEA_BASE, GITEA_TOKEN, GITEA_REPOS, AGENT_NAMES, CACHE_MS as AGENT_STATUS_CACHE_MS }; diff --git a/modules/data/loaders.js b/modules/data/loaders.js deleted file mode 100644 index e536c303..00000000 --- a/modules/data/loaders.js +++ /dev/null @@ -1,64 +0,0 @@ -// modules/data/loaders.js — Static file loaders (portals.json, sovereignty-status.json, SOUL.md) -// Writes to S: sovereigntyScore, sovereigntyLabel -import { S } from '../state.js'; - -// --- SOUL.md (cached) --- -let _soulMdCache = null; - -export async function fetchSoulMd() { - if (_soulMdCache) return _soulMdCache; - try { - const res = await fetch('SOUL.md'); - if (!res.ok) throw new Error('not found'); - const raw = await res.text(); - _soulMdCache = raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, '')); - return _soulMdCache; - } catch { - return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.']; - } -} - -// --- Portal health probes --- -export async function checkPortalHealth(portals) { - 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), - }); - portal.status = 'online'; - } catch { - portal.status = 'offline'; - } - } -} - -// --- portals.json --- -export async function fetchPortals() { - const res = await fetch('./portals.json'); - if (!res.ok) throw new Error('Portals not found'); - return await res.json(); -} - -// --- sovereignty-status.json --- -export async function fetchSovereigntyStatus() { - try { - const res = await fetch('./sovereignty-status.json'); - if (!res.ok) throw new Error('not found'); - const data = await res.json(); - const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85)); - const label = typeof data.label === 'string' ? data.label : ''; - const assessmentType = data.assessment_type || 'MANUAL'; - - S.sovereigntyScore = score; - S.sovereigntyLabel = label; - - return { score, label, assessmentType }; - } catch { - return { score: S.sovereigntyScore, label: S.sovereigntyLabel, assessmentType: 'MANUAL' }; - } -} diff --git a/modules/data/weather.js b/modules/data/weather.js deleted file mode 100644 index 30e05076..00000000 --- a/modules/data/weather.js +++ /dev/null @@ -1,34 +0,0 @@ -// modules/data/weather.js — Open-Meteo weather fetch -// Writes to: weatherState (returned), scene effects applied by caller - -const WEATHER_LAT = 43.2897; -const WEATHER_LON = -72.1479; -const WEATHER_REFRESH_MS = 15 * 60 * 1000; - -function weatherCodeToLabel(code) { - if (code === 0) return { condition: 'Clear', icon: '☀️' }; - if (code <= 2) return { condition: 'Partly Cloudy', icon: '⛅' }; - if (code === 3) return { condition: 'Overcast', icon: '☁️' }; - if (code >= 45 && code <= 48) return { condition: 'Fog', icon: '🌫️' }; - if (code >= 51 && code <= 57) return { condition: 'Drizzle', icon: '🌦️' }; - if (code >= 61 && code <= 67) return { condition: 'Rain', icon: '🌧️' }; - if (code >= 71 && code <= 77) return { condition: 'Snow', icon: '❄️' }; - if (code >= 80 && code <= 82) return { condition: 'Showers', icon: '🌦️' }; - if (code >= 85 && code <= 86) return { condition: 'Snow Showers', icon: '🌨️' }; - if (code >= 95 && code <= 99) return { condition: 'Thunderstorm', icon: '⛈️' }; - return { condition: 'Unknown', icon: '🌀' }; -} - -export async function fetchWeatherData() { - 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); - 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 }; -} - -export { WEATHER_REFRESH_MS }; diff --git a/modules/effects.js b/modules/effects.js index 1b903224..ee91944a 100644 --- a/modules/effects.js +++ b/modules/effects.js @@ -3,7 +3,6 @@ import * as THREE from 'three'; import { NEXUS } from './constants.js'; import { scene } from './scene-setup.js'; import { S } from './state.js'; -import { fetchSovereigntyStatus } from './data/loaders.js'; // === ENERGY BEAM === const ENERGY_BEAM_RADIUS = 0.2; @@ -103,14 +102,20 @@ sovereigntyGroup.traverse(obj => { export async function loadSovereigntyStatus() { try { - const { score, label, assessmentType } = await fetchSovereigntyStatus(); + const res = await fetch('./sovereignty-status.json'); + if (!res.ok) throw new Error('not found'); + const data = await res.json(); + const score = Math.max(0, Math.min(100, typeof data.score === 'number' ? data.score : 85)); + const label = typeof data.label === 'string' ? data.label : ''; + S.sovereigntyScore = score; + S.sovereigntyLabel = label; scoreArcMesh.geometry.dispose(); scoreArcMesh.geometry = buildScoreArcGeo(score); const col = sovereigntyHexColor(score); scoreArcMat.color.setHex(col); meterLight.color.setHex(col); if (meterSpriteMat.map) meterSpriteMat.map.dispose(); - + const assessmentType = data.assessment_type || 'MANUAL'; meterSpriteMat.map = buildMeterTexture(score, label, assessmentType); meterSpriteMat.needsUpdate = true; } catch { diff --git a/modules/extras.js b/modules/extras.js index 792546dd..151653e0 100644 --- a/modules/extras.js +++ b/modules/extras.js @@ -5,8 +5,6 @@ import { S } from './state.js'; import { clock, totalActivity } from './warp.js'; import { HEATMAP_ZONES, zoneIntensity, drawHeatmap, updateHeatmap } from './heatmap.js'; import { triggerShockwave } from './celebrations.js'; -import { fetchNexusCommits } from './data/gitea.js'; -import { fetchBlockHeight, BITCOIN_REFRESH_MS } from './data/bitcoin.js'; // === GRAVITY ANOMALY ZONES === const GRAVITY_ANOMALY_FLOOR = 0.2; @@ -188,7 +186,12 @@ const timelapseBtnEl = document.getElementById('timelapse-btn'); async function loadTimelapseData() { try { - const data = await fetchNexusCommits(); + const res = await fetch( + 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50', + { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } + ); + if (!res.ok) throw new Error('fetch failed'); + const data = await res.json(); const midnight = new Date(); midnight.setHours(0, 0, 0, 0); @@ -299,21 +302,27 @@ export function initBitcoin() { const blockHeightDisplay = document.getElementById('block-height-display'); const blockHeightValue = document.getElementById('block-height-value'); - async function pollBlockHeight() { - const result = await fetchBlockHeight(); - if (!result) return; + async function fetchBlockHeight() { + try { + const res = await fetch('https://blockstream.info/api/blocks/tip/height'); + if (!res.ok) return; + const height = parseInt(await res.text(), 10); + if (isNaN(height)) return; - if (result.isNewBlock && blockHeightDisplay) { - blockHeightDisplay.classList.remove('fresh'); - void blockHeightDisplay.offsetWidth; - blockHeightDisplay.classList.add('fresh'); - } + if (S.lastKnownBlockHeight !== null && height !== S.lastKnownBlockHeight) { + blockHeightDisplay.classList.remove('fresh'); + void blockHeightDisplay.offsetWidth; + blockHeightDisplay.classList.add('fresh'); + S._starPulseIntensity = 1.0; + } - if (blockHeightValue) { - blockHeightValue.textContent = result.height.toLocaleString(); + S.lastKnownBlockHeight = height; + blockHeightValue.textContent = height.toLocaleString(); + } catch (_) { + // Network unavailable } } - pollBlockHeight(); - setInterval(pollBlockHeight, BITCOIN_REFRESH_MS); + fetchBlockHeight(); + setInterval(fetchBlockHeight, 60000); } diff --git a/modules/heatmap.js b/modules/heatmap.js index 617e46a8..cb91095f 100644 --- a/modules/heatmap.js +++ b/modules/heatmap.js @@ -3,7 +3,6 @@ import * as THREE from 'three'; import { scene } from './scene-setup.js'; import { GLASS_RADIUS } from './platform.js'; import { S } from './state.js'; -import { refreshCommitData } from './data/gitea.js'; const HEATMAP_SIZE = 512; const HEATMAP_REFRESH_MS = 5 * 60 * 1000; @@ -95,7 +94,16 @@ export function drawHeatmap() { } export async function updateHeatmap() { - const commits = await refreshCommitData(); + let commits = []; + try { + const res = await fetch( + 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50', + { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } + ); + if (res.ok) commits = await res.json(); + } catch { /* silently use zero-activity baseline */ } + + S._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])); diff --git a/modules/nostr-panel.js b/modules/nostr-panel.js deleted file mode 100644 index 5d3eb249..00000000 --- a/modules/nostr-panel.js +++ /dev/null @@ -1,46 +0,0 @@ -// === NOSTR FEED PANEL === -import * as THREE from 'three'; -import { NEXUS } from './constants.js'; -import { NOSTR_STATE } from './nostr.js'; - -export function createNostrPanelTexture() { - const W = 512, H = 512; - const canvas = document.createElement('canvas'); - canvas.width = W; canvas.height = H; - const ctx = canvas.getContext('2d'); - - const update = () => { - ctx.clearRect(0, 0, W, H); - // Background - ctx.fillStyle = 'rgba(10, 20, 40, 0.8)'; - ctx.fillRect(0, 0, W, H); - - // Header - ctx.fillStyle = '#4488ff'; - ctx.font = 'bold 32px "Orbitron"'; - ctx.fillText('◈ NOSTR_FEED', 30, 60); - ctx.fillRect(30, 75, 452, 2); - - // Connection Status - ctx.fillStyle = NOSTR_STATE.connected ? '#00ff88' : '#ff4444'; - ctx.beginPath(); - ctx.arc(460, 48, 8, 0, Math.PI * 2); - ctx.fill(); - - // Events - ctx.font = '18px "JetBrains Mono"'; - NOSTR_STATE.events.slice(0, 10).forEach((ev, i) => { - const y = 120 + i * 38; - ctx.fillStyle = ev.kind === 9735 ? '#ffd700' : '#ffffff'; - const prefix = ev.kind === 9735 ? '⚡' : '•'; - ctx.fillText(\`\${prefix} [\${ev.pubkey}] \${ev.content}\`, 30, y); - }); - - if (NOSTR_STATE.events.length === 0) { - ctx.fillStyle = '#667788'; - ctx.fillText('> WAITING FOR EVENTS...', 30, 120); - } - }; - - return { canvas, update }; -} diff --git a/modules/nostr.js b/modules/nostr.js deleted file mode 100644 index 1148ce66..00000000 --- a/modules/nostr.js +++ /dev/null @@ -1,76 +0,0 @@ -// === NOSTR INTEGRATION — SOVEREIGN COMMUNICATION === -import { S } from './state.js'; - -export const NOSTR_RELAYS = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.snort.social' -]; - -export const NOSTR_STATE = { - events: [], - connected: false, - lastEventTime: 0 -}; - -export class NostrManager { - constructor() { - this.sockets = []; - } - - connect() { - NOSTR_RELAYS.forEach(url => { - try { - const ws = new WebSocket(url); - ws.onopen = () => { - console.log(\`[nostr] Connected to \${url}\`); - NOSTR_STATE.connected = true; - this.subscribe(ws); - }; - ws.onmessage = (e) => this.handleMessage(e.data); - ws.onerror = () => console.warn(\`[nostr] Connection error: \${url}\`); - this.sockets.push(ws); - } catch (err) { - console.error(\`[nostr] Failed to connect to \${url}\`, err); - } - }); - } - - subscribe(ws) { - const subId = 'nexus-sub-' + Math.random().toString(36).substring(7); - const filter = { kinds: [1, 7, 9735], limit: 20 }; // Notes, Reactions, Zaps - ws.send(JSON.stringify(['REQ', subId, filter])); - } - - handleMessage(data) { - try { - const msg = JSON.parse(data); - if (msg[0] === 'EVENT') { - const event = msg[2]; - this.processEvent(event); - } - } catch (err) { /* ignore parse errors */ } - } - - processEvent(event) { - const simplified = { - id: event.id.substring(0, 8), - pubkey: event.pubkey.substring(0, 8), - content: event.content.length > 60 ? event.content.substring(0, 57) + '...' : event.content, - kind: event.kind, - created_at: event.created_at - }; - - NOSTR_STATE.events.unshift(simplified); - if (NOSTR_STATE.events.length > 50) NOSTR_STATE.events.pop(); - NOSTR_STATE.lastEventTime = Date.now(); - - // Visual feedback via state pulse - if (event.kind === 9735) { // Zap! - S.energyBeamPulse = 1.0; - console.log('[nostr] ZAP RECEIVED!'); - } - } -} - -export const nostr = new NostrManager(); diff --git a/modules/oath.js b/modules/oath.js index 4a1423a3..8f6d8d94 100644 --- a/modules/oath.js +++ b/modules/oath.js @@ -53,8 +53,16 @@ scene.add(oathSpot.target); const AMBIENT_NORMAL = ambientLight.intensity; const OVERHEAD_NORMAL = overheadLight.intensity; -// loadSoulMd imported from data/loaders.js and re-exported for backward compat -export { fetchSoulMd as loadSoulMd } from './data/loaders.js'; +export async function loadSoulMd() { + try { + const res = await fetch('SOUL.md'); + if (!res.ok) throw new Error('not found'); + const raw = await res.text(); + return raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, '')); + } catch { + return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.']; + } +} function scheduleOathLines(lines, textEl) { let idx = 0; diff --git a/modules/panels.js b/modules/panels.js index 02d98fd4..82c2fad7 100644 --- a/modules/panels.js +++ b/modules/panels.js @@ -4,9 +4,90 @@ import { NEXUS } from './constants.js'; import { scene } from './scene-setup.js'; import { S } from './state.js'; import { agentPanelSprites } from './bookshelves.js'; -import { refreshAgentData, AGENT_STATUS_CACHE_MS, AGENT_NAMES } from './data/gitea.js'; // === AGENT STATUS BOARD === +let _agentStatusCache = null; +let _agentStatusCacheTime = 0; +const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000; + +const GITEA_BASE = 'http://143.198.27.163:3000/api/v1'; +const GITEA_TOKEN='81a88f...ae2d'; +const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent']; +const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama']; + +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 = []; + + 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 []; } + })); + + 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); + } + + 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'; + } + + 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' }; function createAgentPanelTexture(agent) { @@ -134,9 +215,20 @@ function rebuildAgentPanels(statusData) { }); } +async function fetchAgentStatus() { + try { + return await fetchAgentStatusFromGitea(); + } catch { + return { agents: AGENT_NAMES.map(n => ({ + name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false, + })) }; + } +} + export async function refreshAgentBoard() { - const data = await refreshAgentData(); + const data = await fetchAgentStatus(); rebuildAgentPanels(data); + S._activeAgentCount = data.agents.filter(a => a.status === 'working').length; } export function initAgentBoard() { diff --git a/modules/portals.js b/modules/portals.js index abe74539..e9a673dd 100644 --- a/modules/portals.js +++ b/modules/portals.js @@ -4,7 +4,6 @@ import { scene } from './scene-setup.js'; import { rebuildRuneRing, setPortalsRef } from './effects.js'; import { setPortalsRefAudio, startPortalHums } from './audio.js'; import { S } from './state.js'; -import { fetchPortals as fetchPortalData } from './data/loaders.js'; export const portalGroup = new THREE.Group(); scene.add(portalGroup); @@ -49,7 +48,9 @@ export function setRunPortalHealthChecksFn(fn) { _runPortalHealthChecksFn = fn; export async function loadPortals() { try { - portals = await fetchPortalData(); + const res = await fetch('./portals.json'); + if (!res.ok) throw new Error('Portals not found'); + portals = await res.json(); console.log('Loaded portals:', portals); setPortalsRef(portals); setPortalsRefAudio(portals); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..5b050711 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "the-nexus", + "version": "1.0.67", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "the-nexus", + "version": "1.0.67" + } + } +}