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/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; + async function pollBlockHeight() { + const result = await fetchBlockHeight(); + if (!result) return; - if (S.lastKnownBlockHeight !== null && height !== S.lastKnownBlockHeight) { - blockHeightDisplay.classList.remove('fresh'); - void blockHeightDisplay.offsetWidth; - blockHeightDisplay.classList.add('fresh'); - S._starPulseIntensity = 1.0; - } + 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/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);