[modularization] Phase 2: Extract data layer — gitea, weather, bitcoin, loaders (#460)
This commit was merged in pull request #460.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
27
modules/data/bitcoin.js
Normal file
27
modules/data/bitcoin.js
Normal 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
142
modules/data/gitea.js
Normal 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
45
modules/data/loaders.js
Normal 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
34
modules/data/weather.js
Normal 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}¤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 };
|
||||
11
modules/effects.js
vendored
11
modules/effects.js
vendored
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user