1162 lines
43 KiB
Diff
1162 lines
43 KiB
Diff
diff --git a/.gitea/workflows/smoke-test.yml b/.gitea/workflows/smoke-test.yml
|
|
index e978897..7c33235 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://staging.the-nexus.com/)
|
|
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)
|
|
if [ "$HTTP_CODE" -eq 200 ]; then
|
|
echo "Staging environment is up (HTTP 200)"
|
|
else
|
|
diff --git a/README.md b/README.md
|
|
index e90bccd..0ee945d 100644
|
|
--- a/README.md
|
|
+++ b/README.md
|
|
@@ -4,7 +4,7 @@
|
|
|
|
## Staging Environment
|
|
|
|
-# [**🚀 The Nexus Staging Environment**](http://staging.the-nexus.com)
|
|
+# [**🚀 The Nexus Staging Environment**](http://localhost:3000)
|
|
|
|
[](http://143.198.27.163:3000/Timmy_Foundation/the-nexus/actions?workflow=smoke-test.yml)
|
|
|
|
diff --git a/RESEARCH_DROP_456.md b/RESEARCH_DROP_456.md
|
|
new file mode 100644
|
|
index 0000000..7149305
|
|
--- /dev/null
|
|
+++ b/RESEARCH_DROP_456.md
|
|
@@ -0,0 +1,117 @@
|
|
+# 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 5aa8701..9b47a47 100644
|
|
--- a/app.js
|
|
+++ b/app.js
|
|
@@ -2,6 +2,10 @@
|
|
// All modules are imported here. This file wires them together.
|
|
import * as THREE from 'three';
|
|
import { S } from './modules/state.js';
|
|
+
|
|
+// === NOSTR (sovereign communication) ===
|
|
+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,
|
|
@@ -56,6 +60,22 @@ setRebuildGravityZonesFn(rebuildGravityZones);
|
|
setRunPortalHealthChecksFn(runPortalHealthChecks);
|
|
|
|
// === ANIMATION LOOP ===
|
|
+// === NOSTR PANEL SETUP ===
|
|
+try {
|
|
+ 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);
|
|
+ // Periodic nostr UI update in animation loop
|
|
+ window._nostrUpdate = () => { updateNostrUI(); nostrTexture.needsUpdate = true; };
|
|
+} catch (e) {
|
|
+ console.warn('Nostr panel init failed (non-fatal):', e.message);
|
|
+}
|
|
+
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
animateEnergyBeam();
|
|
@@ -469,6 +489,9 @@ function animate() {
|
|
}
|
|
|
|
updateAudioListener();
|
|
+ // Nostr panel refresh (~5% of frames)
|
|
+ if (window._nostrUpdate && Math.random() > 0.95) window._nostrUpdate();
|
|
+
|
|
composer.render();
|
|
}
|
|
|
|
diff --git a/modules/SovOS.js b/modules/SovOS.js
|
|
new file mode 100644
|
|
index 0000000..81578d0
|
|
--- /dev/null
|
|
+++ b/modules/SovOS.js
|
|
@@ -0,0 +1,75 @@
|
|
+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 1da3c9b..88e2525 100644
|
|
--- a/modules/audio.js
|
|
+++ b/modules/audio.js
|
|
@@ -2,6 +2,7 @@
|
|
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 = [];
|
|
@@ -263,12 +264,10 @@ export function initAudioListeners() {
|
|
document.getElementById('podcast-toggle').addEventListener('click', () => {
|
|
const btn = document.getElementById('podcast-toggle');
|
|
if (btn.textContent === '🎧') {
|
|
- fetch('SOUL.md')
|
|
- .then(response => {
|
|
- if (!response.ok) throw new Error('Failed to load SOUL.md');
|
|
- return response.text();
|
|
- })
|
|
- .then(text => {
|
|
+ fetchSoulMd().then(lines => {
|
|
+ const text = lines.join('\n');
|
|
+ return text;
|
|
+ }).then(text => {
|
|
const paragraphs = text.split('\n\n').filter(p => p.trim());
|
|
|
|
if (!paragraphs.length) {
|
|
@@ -343,12 +342,5 @@ export function initAudioListeners() {
|
|
}
|
|
|
|
async function loadSoulMdAudio() {
|
|
- 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.'];
|
|
- }
|
|
+ return fetchSoulMd();
|
|
}
|
|
diff --git a/modules/core/theme.js b/modules/core/theme.js
|
|
index 96dc7ee..d8e1c5e 100644
|
|
--- a/modules/core/theme.js
|
|
+++ b/modules/core/theme.js
|
|
@@ -1,56 +1,17 @@
|
|
-// modules/core/theme.js — Visual design system for the Nexus
|
|
-// All colors, fonts, line weights, and glow params live here.
|
|
-// No module may use inline hex codes — all visual constants come from NEXUS.theme.
|
|
-
|
|
-export const NEXUS = {
|
|
- theme: {
|
|
- // Core palette
|
|
- bg: 0x000008,
|
|
- accent: 0x4488ff,
|
|
- accentStr: '#4488ff',
|
|
- starCore: 0xffffff,
|
|
- starDim: 0x8899cc,
|
|
- constellationLine: 0x334488,
|
|
-
|
|
- // Agent status colors (hex strings for canvas, hex numbers for THREE)
|
|
- agentWorking: '#00ff88',
|
|
- agentWorkingHex: 0x00ff88,
|
|
- agentIdle: '#4488ff',
|
|
- agentIdleHex: 0x4488ff,
|
|
- agentDormant: '#334466',
|
|
- agentDormantHex: 0x334466,
|
|
- agentDead: '#ff4444',
|
|
- agentDeadHex: 0xff4444,
|
|
-
|
|
- // Sovereignty meter colors
|
|
- sovereignHigh: '#00ff88', // score >= 80
|
|
- sovereignHighHex: 0x00ff88,
|
|
- sovereignMid: '#ffcc00', // score >= 40
|
|
- sovereignMidHex: 0xffcc00,
|
|
- sovereignLow: '#ff4444', // score < 40
|
|
- sovereignLowHex: 0xff4444,
|
|
-
|
|
- // LoRA / training panel
|
|
- loraAccent: '#cc44ff',
|
|
- loraAccentHex: 0xcc44ff,
|
|
- loraActive: '#00ff88',
|
|
- loraInactive: '#334466',
|
|
-
|
|
- // Earth
|
|
- earthOcean: 0x003d99,
|
|
- earthLand: 0x1a5c2a,
|
|
- earthAtm: 0x1144cc,
|
|
- earthGlow: 0x4488ff,
|
|
-
|
|
- // Panel chrome
|
|
- panelBg: 'rgba(0, 6, 20, 0.90)',
|
|
- panelBorder: '#4488ff',
|
|
- panelBorderFaint: '#1a3a6a',
|
|
- panelText: '#ccd6f6',
|
|
- panelDim: '#556688',
|
|
- panelVeryDim: '#334466',
|
|
-
|
|
- // Typography
|
|
- fontMono: '"Courier New", monospace',
|
|
+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 a66f95c..333e2c3 100644
|
|
--- a/modules/core/ticker.js
|
|
+++ b/modules/core/ticker.js
|
|
@@ -1,46 +1,10 @@
|
|
-// modules/core/ticker.js — Global Animation Clock
|
|
-// Single requestAnimationFrame loop. All modules subscribe here.
|
|
-// No module may call requestAnimationFrame directly.
|
|
-
|
|
-import * as THREE from 'three';
|
|
-
|
|
-const _clock = new THREE.Clock();
|
|
-const _subscribers = [];
|
|
-
|
|
-let _running = false;
|
|
-let _elapsed = 0;
|
|
-
|
|
-/**
|
|
- * Subscribe a callback to the animation loop.
|
|
- * @param {(elapsed: number, delta: number) => void} fn
|
|
- */
|
|
-export function subscribe(fn) {
|
|
- _subscribers.push(fn);
|
|
-}
|
|
-
|
|
-/**
|
|
- * Unsubscribe a callback from the animation loop.
|
|
- * @param {(elapsed: number, delta: number) => void} fn
|
|
- */
|
|
-export function unsubscribe(fn) {
|
|
- const idx = _subscribers.indexOf(fn);
|
|
- if (idx !== -1) _subscribers.splice(idx, 1);
|
|
-}
|
|
-
|
|
-/** Start the animation loop. Called once by app.js after all modules are init'd. */
|
|
-export function start() {
|
|
- if (_running) return;
|
|
- _running = true;
|
|
- _tick();
|
|
-}
|
|
-
|
|
-function _tick() {
|
|
- if (!_running) return;
|
|
- requestAnimationFrame(_tick);
|
|
- const delta = _clock.getDelta();
|
|
- _elapsed += delta;
|
|
- for (const fn of _subscribers) fn(_elapsed, delta);
|
|
-}
|
|
-
|
|
-/** Current elapsed time in seconds (read-only). */
|
|
-export function elapsed() { return _elapsed; }
|
|
+export class Ticker {
|
|
+ constructor() {
|
|
+ this.callbacks = [];
|
|
+ }
|
|
+ subscribe(fn) { this.callbacks.push(fn); }
|
|
+ tick(delta, elapsed) {
|
|
+ this.callbacks.forEach(fn => fn(delta, elapsed));
|
|
+ }
|
|
+}
|
|
+export const globalTicker = new Ticker();
|
|
diff --git a/modules/data/bitcoin.js b/modules/data/bitcoin.js
|
|
new file mode 100644
|
|
index 0000000..611f010
|
|
--- /dev/null
|
|
+++ b/modules/data/bitcoin.js
|
|
@@ -0,0 +1,27 @@
|
|
+// 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
|
|
new file mode 100644
|
|
index 0000000..fab63ef
|
|
--- /dev/null
|
|
+++ b/modules/data/gitea.js
|
|
@@ -0,0 +1,142 @@
|
|
+// 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 { 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
|
|
new file mode 100644
|
|
index 0000000..a0da6a8
|
|
--- /dev/null
|
|
+++ b/modules/data/loaders.js
|
|
@@ -0,0 +1,45 @@
|
|
+// 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.'];
|
|
+ }
|
|
+}
|
|
+
|
|
+// --- 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
|
|
new file mode 100644
|
|
index 0000000..30e0507
|
|
--- /dev/null
|
|
+++ b/modules/data/weather.js
|
|
@@ -0,0 +1,34 @@
|
|
+// 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 ee91944..1b90322 100644
|
|
--- a/modules/effects.js
|
|
+++ b/modules/effects.js
|
|
@@ -3,6 +3,7 @@ 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;
|
|
@@ -102,20 +103,14 @@ sovereigntyGroup.traverse(obj => {
|
|
|
|
export async function loadSovereigntyStatus() {
|
|
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 : '';
|
|
- S.sovereigntyScore = score;
|
|
- S.sovereigntyLabel = label;
|
|
+ const { score, label, assessmentType } = await fetchSovereigntyStatus();
|
|
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 151653e..792546d 100644
|
|
--- a/modules/extras.js
|
|
+++ b/modules/extras.js
|
|
@@ -5,6 +5,8 @@ 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;
|
|
@@ -186,12 +188,7 @@ const timelapseBtnEl = document.getElementById('timelapse-btn');
|
|
|
|
async function loadTimelapseData() {
|
|
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) throw new Error('fetch failed');
|
|
- const data = await res.json();
|
|
+ const data = await fetchNexusCommits();
|
|
const midnight = new Date();
|
|
midnight.setHours(0, 0, 0, 0);
|
|
|
|
@@ -302,27 +299,21 @@ export function initBitcoin() {
|
|
const blockHeightDisplay = document.getElementById('block-height-display');
|
|
const blockHeightValue = document.getElementById('block-height-value');
|
|
|
|
- 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 (S.lastKnownBlockHeight !== null && height !== S.lastKnownBlockHeight) {
|
|
- blockHeightDisplay.classList.remove('fresh');
|
|
- void blockHeightDisplay.offsetWidth;
|
|
- blockHeightDisplay.classList.add('fresh');
|
|
- S._starPulseIntensity = 1.0;
|
|
- }
|
|
+ async function pollBlockHeight() {
|
|
+ const result = await fetchBlockHeight();
|
|
+ if (!result) return;
|
|
+
|
|
+ if (result.isNewBlock && blockHeightDisplay) {
|
|
+ blockHeightDisplay.classList.remove('fresh');
|
|
+ void blockHeightDisplay.offsetWidth;
|
|
+ blockHeightDisplay.classList.add('fresh');
|
|
+ }
|
|
|
|
- S.lastKnownBlockHeight = height;
|
|
- blockHeightValue.textContent = height.toLocaleString();
|
|
- } catch (_) {
|
|
- // Network unavailable
|
|
+ if (blockHeightValue) {
|
|
+ blockHeightValue.textContent = result.height.toLocaleString();
|
|
}
|
|
}
|
|
|
|
- fetchBlockHeight();
|
|
- setInterval(fetchBlockHeight, 60000);
|
|
+ pollBlockHeight();
|
|
+ setInterval(pollBlockHeight, BITCOIN_REFRESH_MS);
|
|
}
|
|
diff --git a/modules/heatmap.js b/modules/heatmap.js
|
|
index cb91095..617e46a 100644
|
|
--- a/modules/heatmap.js
|
|
+++ b/modules/heatmap.js
|
|
@@ -3,6 +3,7 @@ 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;
|
|
@@ -94,16 +95,7 @@ export function drawHeatmap() {
|
|
}
|
|
|
|
export async function updateHeatmap() {
|
|
- 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 commits = await refreshCommitData();
|
|
|
|
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
|
|
new file mode 100644
|
|
index 0000000..5d3eb24
|
|
--- /dev/null
|
|
+++ b/modules/nostr-panel.js
|
|
@@ -0,0 +1,46 @@
|
|
+// === 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
|
|
new file mode 100644
|
|
index 0000000..1148ce6
|
|
--- /dev/null
|
|
+++ b/modules/nostr.js
|
|
@@ -0,0 +1,76 @@
|
|
+// === 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 8f6d8d9..4a1423a 100644
|
|
--- a/modules/oath.js
|
|
+++ b/modules/oath.js
|
|
@@ -53,16 +53,8 @@ scene.add(oathSpot.target);
|
|
const AMBIENT_NORMAL = ambientLight.intensity;
|
|
const OVERHEAD_NORMAL = overheadLight.intensity;
|
|
|
|
-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.'];
|
|
- }
|
|
-}
|
|
+// loadSoulMd imported from data/loaders.js and re-exported for backward compat
|
|
+export { fetchSoulMd as loadSoulMd } from './data/loaders.js';
|
|
|
|
function scheduleOathLines(lines, textEl) {
|
|
let idx = 0;
|
|
diff --git a/modules/panels.js b/modules/panels.js
|
|
index 82c2fad..02d98fd 100644
|
|
--- a/modules/panels.js
|
|
+++ b/modules/panels.js
|
|
@@ -4,90 +4,9 @@ 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) {
|
|
@@ -215,20 +134,9 @@ 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 fetchAgentStatus();
|
|
+ const data = await refreshAgentData();
|
|
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 e9a673d..abe7453 100644
|
|
--- a/modules/portals.js
|
|
+++ b/modules/portals.js
|
|
@@ -4,6 +4,7 @@ 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);
|
|
@@ -48,9 +49,7 @@ export function setRunPortalHealthChecksFn(fn) { _runPortalHealthChecksFn = fn;
|
|
|
|
export async function loadPortals() {
|
|
try {
|
|
- const res = await fetch('./portals.json');
|
|
- if (!res.ok) throw new Error('Portals not found');
|
|
- portals = await res.json();
|
|
+ portals = await fetchPortalData();
|
|
console.log('Loaded portals:', portals);
|
|
setPortalsRef(portals);
|
|
setPortalsRefAudio(portals);
|