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>
93 lines
4.5 KiB
JavaScript
93 lines
4.5 KiB
JavaScript
// modules/panels/agent-board.js — Agent status board with canvas textures
|
|
import * as THREE from 'three';
|
|
import { fetchAgentStatus } from '../data/gitea.js';
|
|
import { state } from '../core/state.js';
|
|
|
|
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dormant: '#334466', dead: '#ff4444', unreachable: '#ff4444' };
|
|
const BOARD_RADIUS = 9.5;
|
|
const BOARD_Y = 4.2;
|
|
const BOARD_SPREAD = Math.PI * 0.75;
|
|
const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000;
|
|
|
|
let agentBoardGroup;
|
|
const agentPanelSprites = [];
|
|
|
|
function createAgentPanelTexture(agent) {
|
|
const W = 400, H = 200;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W; canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
const sc = AGENT_STATUS_COLORS[agent.status] || '#4488ff';
|
|
|
|
ctx.fillStyle = 'rgba(0, 8, 24, 0.88)'; ctx.fillRect(0, 0, W, H);
|
|
ctx.strokeStyle = sc; ctx.lineWidth = 2; ctx.strokeRect(1, 1, W - 2, H - 2);
|
|
ctx.strokeStyle = sc; ctx.lineWidth = 1; ctx.globalAlpha = 0.3; ctx.strokeRect(4, 4, W - 8, H - 8); ctx.globalAlpha = 1.0;
|
|
ctx.font = 'bold 28px "Courier New", monospace'; ctx.fillStyle = '#ffffff'; ctx.fillText(agent.name.toUpperCase(), 16, 44);
|
|
ctx.beginPath(); ctx.arc(W - 30, 26, 10, 0, Math.PI * 2); ctx.fillStyle = sc; ctx.fill();
|
|
ctx.font = '13px "Courier New", monospace'; ctx.fillStyle = sc; ctx.textAlign = 'right'; ctx.fillText(agent.status.toUpperCase(), W - 16, 60); ctx.textAlign = 'left';
|
|
ctx.strokeStyle = '#1a3a6a'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(16, 70); ctx.lineTo(W - 16, 70); ctx.stroke();
|
|
ctx.font = '10px "Courier New", monospace'; ctx.fillStyle = '#556688'; ctx.fillText('CURRENT ISSUE', 16, 90);
|
|
ctx.font = '13px "Courier New", monospace'; ctx.fillStyle = '#ccd6f6';
|
|
const issueText = agent.issue || '\u2014 none \u2014';
|
|
const displayIssue = issueText.length > 40 ? issueText.slice(0, 40) + '\u2026' : issueText;
|
|
ctx.fillText(displayIssue, 16, 110);
|
|
ctx.strokeStyle = '#1a3a6a'; ctx.beginPath(); ctx.moveTo(16, 128); ctx.lineTo(W - 16, 128); ctx.stroke();
|
|
ctx.font = '10px "Courier New", monospace'; ctx.fillStyle = '#556688'; ctx.fillText('PRs MERGED TODAY', 16, 148);
|
|
ctx.font = 'bold 28px "Courier New", monospace'; ctx.fillStyle = '#4488ff'; ctx.fillText(String(agent.prs_today), 16, 182);
|
|
const isLocal = agent.local === true;
|
|
const indicatorColor = isLocal ? '#00ff88' : '#ff4444';
|
|
const indicatorLabel = isLocal ? 'LOCAL' : 'CLOUD';
|
|
ctx.font = '10px "Courier New", monospace'; ctx.fillStyle = '#556688'; ctx.textAlign = 'right'; ctx.fillText('RUNTIME', W - 16, 148);
|
|
ctx.font = 'bold 13px "Courier New", monospace'; ctx.fillStyle = indicatorColor; ctx.fillText(indicatorLabel, W - 28, 172); ctx.textAlign = 'left';
|
|
ctx.beginPath(); ctx.arc(W - 16, 167, 6, 0, Math.PI * 2); ctx.fillStyle = indicatorColor; ctx.fill();
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
function rebuildAgentPanels(statusData) {
|
|
while (agentBoardGroup.children.length) agentBoardGroup.remove(agentBoardGroup.children[0]);
|
|
agentPanelSprites.length = 0;
|
|
const n = statusData.agents.length;
|
|
statusData.agents.forEach((agent, i) => {
|
|
const t = n === 1 ? 0.5 : i / (n - 1);
|
|
const angle = Math.PI + (t - 0.5) * BOARD_SPREAD;
|
|
const x = Math.cos(angle) * BOARD_RADIUS;
|
|
const z = Math.sin(angle) * BOARD_RADIUS;
|
|
const texture = createAgentPanelTexture(agent);
|
|
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.scale.set(6.4, 3.2, 1);
|
|
sprite.position.set(x, BOARD_Y, z);
|
|
sprite.userData = { baseY: BOARD_Y, floatPhase: (i / n) * Math.PI * 2, floatSpeed: 0.18 + i * 0.04, zoomLabel: `Agent: ${agent.name}` };
|
|
agentBoardGroup.add(sprite);
|
|
agentPanelSprites.push(sprite);
|
|
});
|
|
}
|
|
|
|
export function init(scene) {
|
|
agentBoardGroup = new THREE.Group();
|
|
scene.add(agentBoardGroup);
|
|
refreshAgentBoard();
|
|
setInterval(refreshAgentBoard, AGENT_STATUS_CACHE_MS);
|
|
}
|
|
|
|
async function refreshAgentBoard() {
|
|
let data;
|
|
try {
|
|
data = await fetchAgentStatus();
|
|
} catch {
|
|
data = { agents: ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama'].map(n => ({
|
|
name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false,
|
|
})) };
|
|
}
|
|
rebuildAgentPanels(data);
|
|
state.activeAgentCount = data.agents.filter(a => a.status === 'working').length;
|
|
}
|
|
|
|
export function update(elapsed) {
|
|
for (const sprite of agentPanelSprites) {
|
|
const ud = sprite.userData;
|
|
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
|
|
}
|
|
}
|