refactor: modularize app.js into ES module architecture
All checks were successful
CI / validate (pull_request) Successful in 14s
CI / auto-merge (pull_request) Successful in 0s

Split the monolithic 5393-line app.js into 32 focused ES modules under
modules/ with a thin ~330-line orchestrator. No bundler required — runs
in-browser via import maps.

Module structure:
  core/     — scene, ticker, state, theme, audio
  data/     — gitea, weather, bitcoin, loaders
  terrain/  — stars, clouds, island
  effects/  — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones
  panels/   — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel
  portals/  — portal-system, commit-banners
  narrative/ — bookshelves, oath, chat
  utils/    — perlin

All files pass node --check. No new dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 18:12:53 +00:00
parent 677a9e5ae8
commit 675b61d65e
33 changed files with 3786 additions and 5340 deletions

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

@@ -0,0 +1,34 @@
// modules/data/bitcoin.js — Bitcoin block height polling
import { state } from '../core/state.js';
const blockHeightDisplay = document.getElementById('block-height-display');
const blockHeightValue = document.getElementById('block-height-value');
export 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 (state.lastBlockHeight !== 0 && height !== state.lastBlockHeight) {
if (blockHeightDisplay) {
blockHeightDisplay.classList.remove('fresh');
void blockHeightDisplay.offsetWidth;
blockHeightDisplay.classList.add('fresh');
}
state.starPulseIntensity = 1.0;
}
state.lastBlockHeight = height;
state.blockHeight = height;
if (blockHeightValue) blockHeightValue.textContent = height.toLocaleString();
} catch (_) {
// Network unavailable — keep last known value
}
}
export function startBlockPolling() {
fetchBlockHeight();
setInterval(fetchBlockHeight, 60000);
}

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

@@ -0,0 +1,201 @@
// modules/data/gitea.js — All Gitea API calls
import { state } from '../core/state.js';
const GITEA_BASE = 'http://143.198.27.163:3000/api/v1';
const GITEA_TOKEN = '81a88f46684e398abe081f5786a11ae9532aae2d';
const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent'];
const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama'];
const HEATMAP_DECAY_MS = 24 * 60 * 60 * 1000;
export const HEATMAP_ZONES = [
{ name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 },
{ name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 },
{ name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 },
{ name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 },
];
export async function fetchCommits() {
let commits = [];
try {
const res = await fetch(
`${GITEA_BASE}/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 */ }
state.commitHashes = commits.slice(0, 20).map(c => (c.sha || '').slice(0, 7)).filter(h => h.length > 0);
state.commits = commits;
const now = Date.now();
const rawWeights = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
for (const commit of commits) {
const author = commit.commit?.author?.name || commit.author?.login || '';
const ts = new Date(commit.commit?.author?.date || 0).getTime();
const age = now - ts;
if (age > HEATMAP_DECAY_MS) continue;
const weight = 1 - age / HEATMAP_DECAY_MS;
for (const zone of HEATMAP_ZONES) {
if (zone.authorMatch.test(author)) {
rawWeights[zone.name] += weight;
break;
}
}
}
const MAX_WEIGHT = 8;
for (const zone of HEATMAP_ZONES) {
state.zoneIntensity[zone.name] = Math.min(rawWeights[zone.name] / MAX_WEIGHT, 1.0);
}
}
let _agentStatusCache = null;
let _agentStatusCacheTime = 0;
const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000;
export async function fetchAgentStatus() {
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;
state.agentStatus = _agentStatusCache;
state.activeAgentCount = agents.filter(a => a.status === 'working').length;
return _agentStatusCache;
}
export async function fetchRecentCommitsForBanners() {
try {
const res = await fetch(
`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/commits?limit=5`,
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (!res.ok) throw new Error('fetch failed');
const data = await res.json();
return data.map(c => ({
hash: c.sha.slice(0, 7),
message: c.commit.message.split('\n')[0],
}));
} catch {
return [
{ hash: 'a1b2c3d', message: 'feat: depth of field effect on distant objects' },
{ hash: 'e4f5g6h', message: 'feat: photo mode with orbit controls' },
{ hash: 'i7j8k9l', message: 'feat: sovereignty easter egg animation' },
{ hash: 'm0n1o2p', message: 'feat: overview mode bird\'s-eye view' },
{ hash: 'q3r4s5t', message: 'feat: star field and constellation lines' },
];
}
}
export async function fetchClosedPRsForBookshelf() {
try {
const res = await fetch(
`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=20`,
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
);
if (!res.ok) throw new Error('fetch failed');
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 [
{ prNum: 324, title: 'Model training status — LoRA adapters' },
{ prNum: 323, title: 'The Oath — interactive SOUL.md reading' },
{ prNum: 320, title: 'Hermes session save/load' },
{ prNum: 304, title: 'Session export as markdown' },
{ prNum: 303, title: 'Procedural Web Audio ambient soundtrack' },
{ prNum: 301, title: 'Warp tunnel effect for portals' },
{ prNum: 296, title: 'Procedural terrain for floating island' },
{ prNum: 294, title: 'Northern lights flash on PR merge' },
];
}
}
export async function fetchTimelapseCommits() {
try {
const res = await fetch(
`${GITEA_BASE}/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);
return data
.map(c => ({
ts: new Date(c.commit?.author?.date || 0).getTime(),
author: c.commit?.author?.name || c.author?.login || 'unknown',
message: (c.commit?.message || '').split('\n')[0],
hash: (c.sha || '').slice(0, 7),
}))
.filter(c => c.ts >= midnight.getTime())
.sort((a, b) => a.ts - b.ts);
} catch {
return [];
}
}

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

@@ -0,0 +1,39 @@
// modules/data/loaders.js — JSON/file loaders
import { state } from '../core/state.js';
export async function loadPortals() {
try {
const res = await fetch('./portals.json');
if (!res.ok) throw new Error('Portals not found');
state.portals = await res.json();
return state.portals;
} catch (error) {
console.error('Failed to load portals:', error);
return [];
}
}
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();
state.sovereignty = data;
return data;
} catch {
return { score: 85, label: 'Mostly Sovereign', assessment_type: 'MANUAL' };
}
}
export async function loadSoulMd() {
try {
const res = await fetch('SOUL.md');
if (!res.ok) throw new Error('not found');
const raw = await res.text();
const lines = raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
state.soulMd = raw;
return lines;
} catch {
return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.'];
}
}

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

@@ -0,0 +1,155 @@
// modules/data/weather.js — Weather fetch and scene effects
import * as THREE from 'three';
import { state } from '../core/state.js';
const WEATHER_LAT = 43.2897;
const WEATHER_LON = -72.1479;
const WEATHER_REFRESH_MS = 15 * 60 * 1000;
const PRECIP_COUNT = 1200;
const PRECIP_AREA = 18;
const PRECIP_HEIGHT = 20;
const PRECIP_FLOOR = -5;
// Rain geometry
const rainGeo = new THREE.BufferGeometry();
const rainPositions = new Float32Array(PRECIP_COUNT * 3);
const rainVelocities = new Float32Array(PRECIP_COUNT);
for (let i = 0; i < PRECIP_COUNT; i++) {
rainPositions[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2;
rainPositions[i * 3 + 1] = Math.random() * (PRECIP_HEIGHT - PRECIP_FLOOR) + PRECIP_FLOOR;
rainPositions[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2;
rainVelocities[i] = 0.18 + Math.random() * 0.12;
}
rainGeo.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3));
const rainMat = new THREE.PointsMaterial({
color: 0x88aaff, size: 0.05, sizeAttenuation: true, transparent: true, opacity: 0.55,
});
export const rainParticles = new THREE.Points(rainGeo, rainMat);
rainParticles.visible = false;
// Snow geometry
const snowGeo = new THREE.BufferGeometry();
const snowPositions = new Float32Array(PRECIP_COUNT * 3);
const snowDrift = new Float32Array(PRECIP_COUNT);
for (let i = 0; i < PRECIP_COUNT; i++) {
snowPositions[i * 3] = (Math.random() - 0.5) * PRECIP_AREA * 2;
snowPositions[i * 3 + 1] = Math.random() * (PRECIP_HEIGHT - PRECIP_FLOOR) + PRECIP_FLOOR;
snowPositions[i * 3 + 2] = (Math.random() - 0.5) * PRECIP_AREA * 2;
snowDrift[i] = Math.random() * Math.PI * 2;
}
snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3));
const snowMat = new THREE.PointsMaterial({
color: 0xddeeff, size: 0.12, sizeAttenuation: true, transparent: true, opacity: 0.75,
});
export const snowParticles = new THREE.Points(snowGeo, snowMat);
snowParticles.visible = false;
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: '🌀' };
}
function applyWeatherToScene(wx, ambientLight) {
const code = wx.code;
const isRain = (code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 95 && code <= 99);
const isSnow = (code >= 71 && code <= 77) || (code >= 85 && code <= 86);
rainParticles.visible = isRain;
snowParticles.visible = isSnow;
if (isSnow) {
ambientLight.color.setHex(0x1a2a40);
ambientLight.intensity = 1.8;
} else if (isRain) {
ambientLight.color.setHex(0x0a1428);
ambientLight.intensity = 1.2;
} else if (code === 3 || (code >= 45 && code <= 48)) {
ambientLight.color.setHex(0x0c1220);
ambientLight.intensity = 1.1;
} else {
ambientLight.color.setHex(0x0a1428);
ambientLight.intensity = 1.4;
}
}
function updateWeatherHUD(wx) {
const iconEl = document.getElementById('weather-icon');
const tempEl = document.getElementById('weather-temp');
const descEl = document.getElementById('weather-desc');
if (iconEl) iconEl.textContent = wx.icon;
if (tempEl) tempEl.textContent = `${Math.round(wx.temp)}°F`;
if (descEl) descEl.textContent = wx.condition;
}
export async function fetchWeather(ambientLight, cloudMaterial) {
try {
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;
state.weather = { code, temp: cur.temperature_2m, wind: cur.wind_speed_10m, condition, icon, cloudcover };
applyWeatherToScene(state.weather, ambientLight);
if (cloudMaterial) {
cloudMaterial.uniforms.uDensity.value = 0.3 + (cloudcover / 100) * 0.7;
cloudMaterial.opacity = 0.05 + (cloudcover / 100) * 0.55;
}
updateWeatherHUD(state.weather);
} catch {
const descEl = document.getElementById('weather-desc');
if (descEl) descEl.textContent = 'Lempster NH';
}
}
export function startWeatherPolling(ambientLight, cloudMaterial) {
fetchWeather(ambientLight, cloudMaterial);
setInterval(() => fetchWeather(ambientLight, cloudMaterial), WEATHER_REFRESH_MS);
}
export function updateWeatherParticles(elapsed) {
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;
}
}