[claude] Live agent status board — 3D floating holo-panels (#199) (#218)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
This commit was merged in pull request #218.
This commit is contained in:
9
api/status.json
Normal file
9
api/status.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"agents": [
|
||||
{ "name": "claude", "status": "working", "issue": "Live agent status board (#199)", "prs_today": 3 },
|
||||
{ "name": "gemini", "status": "idle", "issue": null, "prs_today": 1 },
|
||||
{ "name": "kimi", "status": "working", "issue": "Portal system YAML registry (#5)", "prs_today": 2 },
|
||||
{ "name": "groq", "status": "idle", "issue": null, "prs_today": 0 },
|
||||
{ "name": "grok", "status": "dead", "issue": null, "prs_today": 0 }
|
||||
]
|
||||
}
|
||||
176
app.js
176
app.js
@@ -408,6 +408,12 @@ function animate() {
|
||||
banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4;
|
||||
});
|
||||
|
||||
// Animate agent status panels — gentle float
|
||||
for (const sprite of agentPanelSprites) {
|
||||
const ud = sprite.userData;
|
||||
sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
|
||||
}
|
||||
|
||||
composer.render();
|
||||
}
|
||||
|
||||
@@ -558,6 +564,10 @@ window.addEventListener('beforeunload', () => {
|
||||
// === COMMIT BANNERS ===
|
||||
const commitBanners = [];
|
||||
|
||||
// === AGENT STATUS PANELS (declared early — populated after scene is ready) ===
|
||||
/** @type {THREE.Sprite[]} */
|
||||
const agentPanelSprites = [];
|
||||
|
||||
/**
|
||||
* Creates a canvas texture for a commit banner.
|
||||
* @param {string} hash - Short commit hash
|
||||
@@ -651,3 +661,169 @@ async function initCommitBanners() {
|
||||
}
|
||||
|
||||
initCommitBanners();
|
||||
|
||||
// === AGENT STATUS BOARD ===
|
||||
|
||||
const AGENT_STATUS_STUB = {
|
||||
agents: [
|
||||
{ name: 'claude', status: 'working', issue: 'Live agent status board (#199)', prs_today: 3 },
|
||||
{ name: 'gemini', status: 'idle', issue: null, prs_today: 1 },
|
||||
{ name: 'kimi', status: 'working', issue: 'Portal system YAML registry (#5)', prs_today: 2 },
|
||||
{ name: 'groq', status: 'idle', issue: null, prs_today: 0 },
|
||||
{ name: 'grok', status: 'dead', issue: null, prs_today: 0 },
|
||||
]
|
||||
};
|
||||
|
||||
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dead: '#ff4444' };
|
||||
|
||||
/**
|
||||
* Builds a canvas texture for a single agent holo-panel.
|
||||
* @param {{ name: string, status: string, issue: string|null, prs_today: number }} agent
|
||||
* @returns {THREE.CanvasTexture}
|
||||
*/
|
||||
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';
|
||||
|
||||
// Dark background
|
||||
ctx.fillStyle = 'rgba(0, 8, 24, 0.88)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Outer border in status color
|
||||
ctx.strokeStyle = sc;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(1, 1, W - 2, H - 2);
|
||||
|
||||
// Faint inner border
|
||||
ctx.strokeStyle = sc;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.strokeRect(4, 4, W - 8, H - 8);
|
||||
ctx.globalAlpha = 1.0;
|
||||
|
||||
// Agent name
|
||||
ctx.font = 'bold 28px "Courier New", monospace';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillText(agent.name.toUpperCase(), 16, 44);
|
||||
|
||||
// Status dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(W - 30, 26, 10, 0, Math.PI * 2);
|
||||
ctx.fillStyle = sc;
|
||||
ctx.fill();
|
||||
|
||||
// Status label
|
||||
ctx.font = '13px "Courier New", monospace';
|
||||
ctx.fillStyle = sc;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(agent.status.toUpperCase(), W - 16, 60);
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
// Separator
|
||||
ctx.strokeStyle = '#1a3a6a';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(16, 70);
|
||||
ctx.lineTo(W - 16, 70);
|
||||
ctx.stroke();
|
||||
|
||||
// Current issue label
|
||||
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);
|
||||
|
||||
// Separator
|
||||
ctx.strokeStyle = '#1a3a6a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(16, 128);
|
||||
ctx.lineTo(W - 16, 128);
|
||||
ctx.stroke();
|
||||
|
||||
// PRs merged today
|
||||
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);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
}
|
||||
|
||||
/** Group holding all agent panels so they can be toggled/repositioned together. */
|
||||
const agentBoardGroup = new THREE.Group();
|
||||
scene.add(agentBoardGroup);
|
||||
|
||||
const BOARD_RADIUS = 9.5; // distance from scene origin
|
||||
const BOARD_Y = 4.2; // height above platform
|
||||
const BOARD_SPREAD = Math.PI * 0.75; // 135° total arc, centred on negative-Z axis
|
||||
|
||||
/**
|
||||
* (Re)builds the agent panel sprites from fresh status data.
|
||||
* @param {{ agents: Array<{ name: string, status: string, issue: string|null, prs_today: number }> }} statusData
|
||||
*/
|
||||
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);
|
||||
// Spread in a semi-circle: angle=PI is directly behind (negative-Z)
|
||||
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,
|
||||
};
|
||||
agentBoardGroup.add(sprite);
|
||||
agentPanelSprites.push(sprite);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches live agent status, falling back to the stub when the endpoint is unavailable.
|
||||
* @returns {Promise<typeof AGENT_STATUS_STUB>}
|
||||
*/
|
||||
async function fetchAgentStatus() {
|
||||
try {
|
||||
const res = await fetch('/api/status.json');
|
||||
if (!res.ok) throw new Error('status ' + res.status);
|
||||
return await res.json();
|
||||
} catch {
|
||||
return AGENT_STATUS_STUB;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgentBoard() {
|
||||
const data = await fetchAgentStatus();
|
||||
rebuildAgentPanels(data);
|
||||
}
|
||||
|
||||
// Initial render, then poll every 30 s
|
||||
refreshAgentBoard();
|
||||
setInterval(refreshAgentBoard, 30000);
|
||||
|
||||
Reference in New Issue
Block a user