Files
the-nexus/modules/panels/agent-board.js
Perplexity Computer 675b61d65e
All checks were successful
CI / validate (pull_request) Successful in 14s
CI / auto-merge (pull_request) Successful in 0s
refactor: modularize app.js into ES module architecture
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>
2026-03-24 18:12:53 +00:00

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