Files
the-nexus/modules/panels.js
Alexander Whitestone 979c7cf96b
Some checks failed
Deploy Nexus / deploy (push) Failing after 5s
Staging Smoke Test / smoke-test (push) Successful in 1s
revert: strip Manus damage (nostr, SovOS, gutted app.js) — restore clean split
Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24

Refs #418, #452, #454
2026-03-24 18:29:57 -04:00

369 lines
11 KiB
JavaScript

// === AGENT STATUS BOARD + LORA PANEL ===
import * as THREE from 'three';
import { NEXUS } from './constants.js';
import { scene } from './scene-setup.js';
import { S } from './state.js';
import { agentPanelSprites } from './bookshelves.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) {
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);
}
const agentBoardGroup = new THREE.Group();
scene.add(agentBoardGroup);
const BOARD_RADIUS = 9.5;
const BOARD_Y = 4.2;
const BOARD_SPREAD = Math.PI * 0.75;
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);
});
}
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();
rebuildAgentPanels(data);
S._activeAgentCount = data.agents.filter(a => a.status === 'working').length;
}
export function initAgentBoard() {
refreshAgentBoard();
setInterval(refreshAgentBoard, AGENT_STATUS_CACHE_MS);
}
// === LORA ADAPTER STATUS PANEL ===
const LORA_ACTIVE_COLOR = '#00ff88';
const LORA_INACTIVE_COLOR = '#334466';
function createLoRAPanelTexture(data) {
const W = 420, H = 260;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(0, 6, 20, 0.90)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#cc44ff';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = '#cc44ff';
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
ctx.font = 'bold 14px "Courier New", monospace';
ctx.fillStyle = '#cc44ff';
ctx.textAlign = 'left';
ctx.fillText('MODEL TRAINING', 14, 24);
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#664488';
ctx.fillText('LoRA ADAPTERS', 14, 38);
ctx.strokeStyle = '#2a1a44';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(14, 46);
ctx.lineTo(W - 14, 46);
ctx.stroke();
if (!data || !data.adapters || data.adapters.length === 0) {
ctx.font = 'bold 18px "Courier New", monospace';
ctx.fillStyle = '#334466';
ctx.textAlign = 'center';
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
ctx.font = '11px "Courier New", monospace';
ctx.fillStyle = '#223344';
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
ctx.textAlign = 'left';
return new THREE.CanvasTexture(canvas);
}
const activeCount = data.adapters.filter(a => a.active).length;
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = LORA_ACTIVE_COLOR;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${data.adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
const ROW_H = 44;
data.adapters.forEach((adapter, i) => {
const rowY = 50 + i * ROW_H;
const col = adapter.active ? LORA_ACTIVE_COLOR : LORA_INACTIVE_COLOR;
ctx.beginPath();
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
ctx.font = 'bold 13px "Courier New", monospace';
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
ctx.fillText(adapter.name, 36, rowY + 16);
ctx.font = '10px "Courier New", monospace';
ctx.fillStyle = '#556688';
ctx.textAlign = 'right';
ctx.fillText(adapter.base, W - 14, rowY + 16);
ctx.textAlign = 'left';
if (adapter.active) {
const BAR_X = 36, BAR_W = W - 80, BAR_Y = rowY + 22, BAR_H = 5;
ctx.fillStyle = '#0a1428';
ctx.fillRect(BAR_X, BAR_Y, BAR_W, BAR_H);
ctx.fillStyle = col;
ctx.globalAlpha = 0.7;
ctx.fillRect(BAR_X, BAR_Y, BAR_W * adapter.strength, BAR_H);
ctx.globalAlpha = 1.0;
}
if (i < data.adapters.length - 1) {
ctx.strokeStyle = '#1a0a2a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(14, rowY + ROW_H - 2);
ctx.lineTo(W - 14, rowY + ROW_H - 2);
ctx.stroke();
}
});
return new THREE.CanvasTexture(canvas);
}
const loraGroup = new THREE.Group();
scene.add(loraGroup);
const LORA_PANEL_POS = new THREE.Vector3(-10.5, 4.5, 2.5);
export let loraPanelSprite = null;
function rebuildLoRAPanel(data) {
if (loraPanelSprite) {
loraGroup.remove(loraPanelSprite);
if (loraPanelSprite.material.map) loraPanelSprite.material.map.dispose();
loraPanelSprite.material.dispose();
loraPanelSprite = null;
}
const texture = createLoRAPanelTexture(data);
const material = new THREE.SpriteMaterial({
map: texture, transparent: true, opacity: 0.93, depthWrite: false,
});
loraPanelSprite = new THREE.Sprite(material);
loraPanelSprite.scale.set(6.0, 3.6, 1);
loraPanelSprite.position.copy(LORA_PANEL_POS);
loraPanelSprite.userData = {
baseY: LORA_PANEL_POS.y,
floatPhase: 1.1,
floatSpeed: 0.14,
zoomLabel: 'Model Training — LoRA Adapters',
};
loraGroup.add(loraPanelSprite);
}
export function loadLoRAStatus() {
rebuildLoRAPanel({ adapters: [] });
}