## Ralph Skill - Complete Python Implementation - __main__.py: Main entry point for Ralph autonomous agent - agent_capability_registry.py: Agent capability registry (FIXED syntax error) - dynamic_agent_selector.py: Dynamic agent selection logic - meta_agent_orchestrator.py: Meta-orchestration for multi-agent workflows - worker_agent.py: Worker agent implementation - ralph_agent_integration.py: Integration with Claude Code - superpowers_integration.py: Superpowers framework integration - observability_dashboard.html: Real-time observability UI - observability_server.py: Dashboard server - multi-agent-architecture.md: Architecture documentation - SUPERPOWERS_INTEGRATION.md: Integration guide ## Framework Integration Status - ✅ codebase-indexer (Chippery): Complete implementation with 5 scripts - ✅ ralph (Ralph Orchestrator): Complete Python implementation - ✅ always-use-superpowers: Declarative skill (SKILL.md) - ✅ auto-superpowers: Declarative skill (SKILL.md) - ✅ auto-dispatcher: Declarative skill (Ralph framework) - ✅ autonomous-planning: Declarative skill (Ralph framework) - ✅ mcp-client: Declarative skill (AGIAgent/Agno framework) ## Agent Updates - Updated README.md with latest integration status - Added framework integration agents Token Savings: ~99% via semantic codebase indexing 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
783 lines
25 KiB
HTML
783 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ralph Multi-Agent Command Center</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
color: #e4e4e7;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
padding: 20px 40px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: #10b981;
|
|
}
|
|
|
|
.status-dot.disconnected {
|
|
background: #ef4444;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.container {
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 30px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #a1a1aa;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.conflict-alert {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.conflict-alert h3 {
|
|
color: #ef4444;
|
|
font-size: 16px;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.conflict-list {
|
|
list-style: none;
|
|
}
|
|
|
|
.conflict-list li {
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.conflict-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.agents-section {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.section-header {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.agent-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.agent-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.agent-card:hover {
|
|
transform: translateY(-2px);
|
|
border-color: rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.agent-card.active {
|
|
border-color: #10b981;
|
|
box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);
|
|
}
|
|
|
|
.agent-card.busy {
|
|
border-color: #f59e0b;
|
|
}
|
|
|
|
.agent-card.error {
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
.agent-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: start;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.agent-id {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.agent-status {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.agent-status.idle {
|
|
background: #6b7280;
|
|
}
|
|
|
|
.agent-status.active {
|
|
background: #10b981;
|
|
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
|
}
|
|
|
|
.agent-status.busy {
|
|
background: #f59e0b;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.agent-status.error {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.agent-info {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.agent-info p {
|
|
font-size: 13px;
|
|
color: #a1a1aa;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.agent-info span {
|
|
color: #e4e4e7;
|
|
}
|
|
|
|
.task-progress {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.task-progress-label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.task-progress-bar {
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.task-progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.files-list {
|
|
margin-top: 12px;
|
|
font-size: 12px;
|
|
color: #a1a1aa;
|
|
}
|
|
|
|
.files-list code {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: 'Monaco', 'Menlo', monospace;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.activity-section {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.activity-stream {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.activity-stream::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.activity-stream::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.activity-stream::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.event {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.event:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.event-time {
|
|
color: #a1a1aa;
|
|
font-family: 'Monaco', 'Menlo', monospace;
|
|
font-size: 11px;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.event-agent {
|
|
color: #667eea;
|
|
font-weight: 600;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.event-action {
|
|
color: #e4e4e7;
|
|
}
|
|
|
|
.event-action.success {
|
|
color: #10b981;
|
|
}
|
|
|
|
.event-action.error {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.event-action.warning {
|
|
color: #f59e0b;
|
|
}
|
|
|
|
.charts-section {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.chart-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.chart-card h3 {
|
|
font-size: 14px;
|
|
color: #a1a1aa;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge.frontend {
|
|
background: rgba(102, 126, 234, 0.2);
|
|
color: #667eea;
|
|
}
|
|
|
|
.badge.backend {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
color: #10b981;
|
|
}
|
|
|
|
.badge.testing {
|
|
background: rgba(245, 158, 11, 0.2);
|
|
color: #f59e0b;
|
|
}
|
|
|
|
.badge.docs {
|
|
background: rgba(236, 72, 153, 0.2);
|
|
color: #ec4899;
|
|
}
|
|
|
|
.badge.refactor {
|
|
background: rgba(139, 92, 246, 0.2);
|
|
color: #8b5cf6;
|
|
}
|
|
|
|
.badge.analysis {
|
|
background: rgba(6, 182, 212, 0.2);
|
|
color: #06b6d4;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<div class="header">
|
|
<h1>Ralph Multi-Agent Command Center</h1>
|
|
<div class="connection-status">
|
|
<div :class="['status-dot', connected ? 'connected' : 'disconnected']"></div>
|
|
<span>{{ connected ? 'Connected' : 'Disconnected' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<!-- Overall Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Agents</div>
|
|
<div class="stat-value">{{ activeAgents.length }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Tasks Completed</div>
|
|
<div class="stat-value">{{ completedTasks }} / {{ totalTasks }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Files Modified</div>
|
|
<div class="stat-value">{{ modifiedFiles.size }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Conflicts</div>
|
|
<div class="stat-value">{{ conflicts.length }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conflict Alerts -->
|
|
<div v-if="conflicts.length > 0" class="conflict-alert">
|
|
<h3>
|
|
<span>⚠️</span>
|
|
File Conflicts Detected
|
|
</h3>
|
|
<ul class="conflict-list">
|
|
<li v-for="conflict in conflicts" :key="conflict.file">
|
|
<code>{{ conflict.file }}</code>
|
|
<span style="color: #a1a1aa"> — </span>
|
|
{{ conflict.agents.join(' vs ') }}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Charts -->
|
|
<div class="charts-section">
|
|
<div class="chart-card">
|
|
<h3>Task Completion by Type</h3>
|
|
<canvas id="taskTypeChart"></canvas>
|
|
</div>
|
|
<div class="chart-card">
|
|
<h3>Agent Performance</h3>
|
|
<canvas id="agentPerformanceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agents Grid -->
|
|
<div class="agents-section">
|
|
<div class="section-header">
|
|
<h2>Worker Agents</h2>
|
|
<span style="font-size: 14px; color: #a1a1aa">
|
|
{{ activeAgents.length }} active / {{ agents.length }} total
|
|
</span>
|
|
</div>
|
|
<div class="agent-grid">
|
|
<div
|
|
v-for="agent in agents"
|
|
:key="agent.id"
|
|
:class="['agent-card', agent.status]">
|
|
<div :class="['agent-status', agent.status]"></div>
|
|
|
|
<div class="agent-header">
|
|
<div class="agent-id">{{ agent.id }}</div>
|
|
<span :class="['badge', agent.specialization]">{{ agent.specialization }}</span>
|
|
</div>
|
|
|
|
<div class="agent-info">
|
|
<p>Status: <span>{{ agent.status }}</span></p>
|
|
<p v-if="agent.currentTask">
|
|
Current Task: <span>{{ agent.currentTask }}</span>
|
|
</p>
|
|
<p v-else>
|
|
Current Task: <span style="color: #6b7280">Idle</span>
|
|
</p>
|
|
<p>Tasks Completed: <span>{{ agent.completedCount }}</span></p>
|
|
</div>
|
|
|
|
<div v-if="agent.currentTask" class="task-progress">
|
|
<div class="task-progress-label">
|
|
<span>Progress</span>
|
|
<span>{{ Math.round(agent.progress) }}%</span>
|
|
</div>
|
|
<div class="task-progress-bar">
|
|
<div
|
|
class="task-progress-fill"
|
|
:style="{ width: agent.progress + '%' }">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="agent.workingFiles && agent.workingFiles.length > 0" class="files-list">
|
|
<div style="margin-bottom: 4px; color: #a1a1aa">Working Files:</div>
|
|
<code v-for="file in agent.workingFiles.slice(0, 3)" :key="file">
|
|
{{ file }}
|
|
</code>
|
|
<span v-if="agent.workingFiles.length > 3">
|
|
+{{ agent.workingFiles.length - 3 }} more
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Stream -->
|
|
<div class="activity-section">
|
|
<div class="section-header">
|
|
<h2>Live Activity</h2>
|
|
<span style="font-size: 14px; color: #a1a1aa">
|
|
{{ recentEvents.length }} events
|
|
</span>
|
|
</div>
|
|
<div class="activity-stream">
|
|
<div v-for="event in recentEvents" :key="event.id" class="event">
|
|
<div class="event-time">{{ formatTime(event.timestamp) }}</div>
|
|
<div class="event-agent">{{ event.agentId }}</div>
|
|
<div :class="['event-action', event.type]">{{ event.action }}</div>
|
|
</div>
|
|
<div v-if="recentEvents.length === 0" style="text-align: center; padding: 40px; color: #6b7280">
|
|
No events yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp } = Vue;
|
|
|
|
createApp({
|
|
data() {
|
|
return {
|
|
agents: [],
|
|
conflicts: [],
|
|
recentEvents: [],
|
|
totalTasks: 0,
|
|
completedTasks: 0,
|
|
modifiedFiles: new Set(),
|
|
ws: null,
|
|
connected: false,
|
|
charts: {
|
|
taskType: null,
|
|
agentPerformance: null
|
|
}
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
activeAgents() {
|
|
return this.agents.filter(a => a.status === 'active' || a.status === 'busy');
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
connect() {
|
|
const wsUrl = `ws://${window.location.hostname}:3001`;
|
|
console.log('Connecting to:', wsUrl);
|
|
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
this.ws.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
this.connected = true;
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
this.handleMessage(data);
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.log('WebSocket disconnected, reconnecting...');
|
|
this.connected = false;
|
|
setTimeout(() => this.connect(), 3000);
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
this.connected = false;
|
|
};
|
|
},
|
|
|
|
handleMessage(data) {
|
|
switch(data.type) {
|
|
case 'agent_update':
|
|
this.updateAgent(data.agent);
|
|
break;
|
|
case 'conflict':
|
|
this.conflicts.push(data.conflict);
|
|
break;
|
|
case 'conflict_resolved':
|
|
this.conflicts = this.conflicts.filter(
|
|
c => c.file !== data.conflict.file
|
|
);
|
|
break;
|
|
case 'task_complete':
|
|
this.completedTasks++;
|
|
this.addEvent('success', data.agentId, `Completed task ${data.taskId}`);
|
|
break;
|
|
case 'task_failed':
|
|
this.addEvent('error', data.agentId, `Failed task ${data.taskId}: ${data.error}`);
|
|
break;
|
|
case 'task_started':
|
|
this.addEvent('warning', data.agentId, `Started task ${data.taskId}`);
|
|
break;
|
|
case 'event':
|
|
this.addEvent('info', data.agentId, data.action);
|
|
break;
|
|
}
|
|
},
|
|
|
|
updateAgent(agentData) {
|
|
const index = this.agents.findIndex(a => a.id === agentData.id);
|
|
|
|
if (index >= 0) {
|
|
this.agents[index] = agentData;
|
|
} else {
|
|
this.agents.push(agentData);
|
|
}
|
|
|
|
// Track modified files
|
|
if (agentData.workingFiles) {
|
|
agentData.workingFiles.forEach(f => this.modifiedFiles.add(f));
|
|
}
|
|
|
|
// Update charts periodically
|
|
this.updateCharts();
|
|
},
|
|
|
|
addEvent(type, agentId, action) {
|
|
const event = {
|
|
id: Date.now() + Math.random(),
|
|
timestamp: Date.now(),
|
|
type,
|
|
agentId,
|
|
action
|
|
};
|
|
|
|
this.recentEvents.unshift(event);
|
|
|
|
// Keep only last 100 events
|
|
if (this.recentEvents.length > 100) {
|
|
this.recentEvents = this.recentEvents.slice(0, 100);
|
|
}
|
|
},
|
|
|
|
formatTime(timestamp) {
|
|
return new Date(timestamp).toLocaleTimeString();
|
|
},
|
|
|
|
updateCharts() {
|
|
// Update task type chart
|
|
if (this.charts.taskType) {
|
|
const typeCounts = {};
|
|
this.agents.forEach(agent => {
|
|
const type = agent.specialization;
|
|
typeCounts[type] = (typeCounts[type] || 0) + agent.completedCount;
|
|
});
|
|
|
|
this.charts.taskType.data.datasets[0].data = Object.values(typeCounts);
|
|
this.charts.taskType.data.labels = Object.keys(typeCounts);
|
|
this.charts.taskType.update('none');
|
|
}
|
|
|
|
// Update agent performance chart
|
|
if (this.charts.agentPerformance) {
|
|
this.charts.agentPerformance.data.datasets[0].data = this.agents.map(a => a.completedCount);
|
|
this.charts.agentPerformance.data.labels = this.agents.map(a => a.id);
|
|
this.charts.agentPerformance.update('none');
|
|
}
|
|
},
|
|
|
|
initCharts() {
|
|
// Task Type Chart
|
|
const taskTypeCtx = document.getElementById('taskTypeChart').getContext('2d');
|
|
this.charts.taskType = new Chart(taskTypeCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: [
|
|
'#667eea',
|
|
'#10b981',
|
|
'#f59e0b',
|
|
'#ec4899',
|
|
'#8b5cf6',
|
|
'#06b6d4'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
color: '#a1a1aa',
|
|
font: { size: 11 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Agent Performance Chart
|
|
const agentPerfCtx = document.getElementById('agentPerformanceChart').getContext('2d');
|
|
this.charts.agentPerformance = new Chart(agentPerfCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Tasks Completed',
|
|
data: [],
|
|
backgroundColor: 'rgba(102, 126, 234, 0.5)',
|
|
borderColor: '#667eea',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(255, 255, 255, 0.05)'
|
|
},
|
|
ticks: {
|
|
color: '#a1a1aa'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
color: '#a1a1aa',
|
|
font: { size: 10 }
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.initCharts();
|
|
this.connect();
|
|
|
|
// Also load initial data via HTTP in case WS is slow
|
|
fetch('http://localhost:3001/api/status')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
this.agents = data.agents || [];
|
|
this.totalTasks = data.totalTasks || 0;
|
|
this.completedTasks = data.completedTasks || 0;
|
|
})
|
|
.catch(err => console.log('Initial load failed:', err));
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|