[modularization] Phase 2: Extract data layer — gitea, weather, bitcoin, loaders
Some checks failed
CI / validate (pull_request) Failing after 14s
CI / auto-merge (pull_request) Has been skipped

Created modules/data/ with 4 data modules:
- data/gitea.js: Centralized Gitea API (commits, PRs, agent status)
- data/weather.js: Open-Meteo weather data fetch
- data/bitcoin.js: Blockstream block height polling
- data/loaders.js: Static file loaders (portals.json, sovereignty-status.json, SOUL.md)

Updated consumers to use data modules instead of direct fetch():
- panels.js: Uses data/gitea.js for agent status
- heatmap.js: Uses data/gitea.js for commit data
- extras.js: Uses data/gitea.js for timelapse, data/bitcoin.js for block height
- portals.js: Uses data/loaders.js for portals.json
- effects.js: Uses data/loaders.js for sovereignty-status.json
- oath.js: Uses data/loaders.js for SOUL.md
- audio.js: Uses data/loaders.js for SOUL.md

Deduplication wins:
- Gitea token centralized (was hardcoded in 3 files)
- SOUL.md now cached (was fetched 3x independently)
- Nexus commits shared cache (was fetched 2x independently)

Refs #421
This commit is contained in:
Alexander Whitestone
2026-03-24 17:25:51 -04:00
parent 24e71396cc
commit a1eb0ebb90
11 changed files with 280 additions and 163 deletions

27
modules/data/bitcoin.js Normal file
View File

@@ -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 };

142
modules/data/gitea.js Normal file
View File

@@ -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 };

45
modules/data/loaders.js Normal file
View File

@@ -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' };
}
}

34
modules/data/weather.js Normal file
View File

@@ -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}&current=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 };