- ADR-0001: Multi-Agent Coworking Architecture with LLM Gateway Orchestrator - ADR-0002: Tier Assignment Strategy for Model Selection (cost-first escalation) - ADR-0003: Confidence Gate Thresholds & Learning Cycle Intervals (6h/12h/24h cycles) - ADR-0004: External Provider Fallback Chain Ordering (Cerebras → Groq → Mistral) - Enhanced client SDK: Offline Ollama fallback, health checks, exponential backoff retry - Integration tests: claude-code-integration.test.ts (14 test cases) - PHASE_2F_DEPLOYMENT.md: Pre-deployment checklist, automated deploy, rollback plan - Post-deployment verification procedures for health, client fallback, metrics
306 lines
10 KiB
HTML
306 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LLM Gateway Dashboard</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
|
|
<style>
|
|
body { background: #f8f9fa; }
|
|
.stat-card {
|
|
background: white;
|
|
border: none;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.stat-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #2c3e50;
|
|
}
|
|
.stat-label {
|
|
font-size: 0.875rem;
|
|
color: #7f8c8d;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.chart-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.alert-item {
|
|
padding: 0.75rem;
|
|
border-left: 4px solid #dc3545;
|
|
background: #fff5f5;
|
|
margin-bottom: 0.5rem;
|
|
border-radius: 4px;
|
|
}
|
|
.loading { opacity: 0.6; pointer-events: none; }
|
|
.error { color: #dc3545; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-dark bg-dark mb-4">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">📊 LLM Gateway Dashboard</span>
|
|
<span class="navbar-text text-muted">Real-time Cost & Compression Metrics</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container-fluid">
|
|
<!-- Summary Stats -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Cost (24h)</div>
|
|
<div class="stat-value" id="totalCost">€0.00</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Saved</div>
|
|
<div class="stat-value" id="totalSaved">€0.00</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Compression Ratio</div>
|
|
<div class="stat-value" id="compressionRatio">0%</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Requests</div>
|
|
<div class="stat-value" id="requestCount">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Cost by Model</h5>
|
|
<canvas id="costByModelChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Tokens by Model</h5>
|
|
<canvas id="tokensByModelChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agent Activity -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-8">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Agent Activity</h5>
|
|
<div id="agentActivity" style="max-height: 400px; overflow-y: auto;">
|
|
<p class="text-muted">Loading agent data...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Active Alerts</h5>
|
|
<div id="alertPanel">
|
|
<p class="text-muted">Loading alerts...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cost Breakdown -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Cost by Project</h5>
|
|
<div id="costByProject">
|
|
<p class="text-muted">Loading project costs...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5 class="mb-3">Cost by Task Type</h5>
|
|
<div id="costByTaskType">
|
|
<p class="text-muted">Loading task costs...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '';
|
|
let costByModelChart = null;
|
|
let tokensByModelChart = null;
|
|
let eventSource = null;
|
|
|
|
function connectToStream() {
|
|
eventSource = new EventSource(`${API_BASE}/api/stream/costs`);
|
|
|
|
eventSource.addEventListener('connected', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
console.log('SSE connected:', data.clientId);
|
|
});
|
|
|
|
eventSource.addEventListener('cost-update', (e) => {
|
|
const update = JSON.parse(e.data);
|
|
incrementStats(update);
|
|
});
|
|
|
|
eventSource.onerror = () => {
|
|
console.error('SSE stream error, reconnecting...');
|
|
eventSource.close();
|
|
setTimeout(() => connectToStream(), 3000);
|
|
};
|
|
}
|
|
|
|
function incrementStats(update) {
|
|
const totalCostEl = document.getElementById('totalCost');
|
|
const totalSavedEl = document.getElementById('totalSaved');
|
|
const requestCountEl = document.getElementById('requestCount');
|
|
|
|
const currentCost = parseFloat(totalCostEl.textContent.replace('€', '')) || 0;
|
|
const currentSaved = parseFloat(totalSavedEl.textContent.replace('€', '')) || 0;
|
|
const currentCount = parseInt(requestCountEl.textContent) || 0;
|
|
|
|
totalCostEl.textContent = `€${(currentCost + update.costUsd).toFixed(4)}`;
|
|
totalSavedEl.textContent = `€${(currentSaved + update.costSavedUsd).toFixed(4)}`;
|
|
requestCountEl.textContent = (currentCount + 1).toString();
|
|
}
|
|
|
|
async function refreshDashboard() {
|
|
try {
|
|
const [summary, costs, tokens, agents, alerts] = await Promise.all([
|
|
fetch(`${API_BASE}/api/dashboard/summary?hours=24`).then(r => r.json()),
|
|
fetch(`${API_BASE}/api/dashboard/costs?hours=24`).then(r => r.json()),
|
|
fetch(`${API_BASE}/api/dashboard/tokens?hours=24`).then(r => r.json()),
|
|
fetch(`${API_BASE}/api/dashboard/agents?hours=24`).then(r => r.json()),
|
|
fetch(`${API_BASE}/api/dashboard/alerts`).then(r => r.json())
|
|
]);
|
|
|
|
updateSummary(summary);
|
|
updateCharts(costs, tokens);
|
|
updateAgentActivity(agents);
|
|
updateAlerts(alerts);
|
|
} catch (err) {
|
|
console.error('Failed to refresh dashboard:', err);
|
|
}
|
|
}
|
|
|
|
function updateSummary(summary) {
|
|
document.getElementById('totalCost').textContent = `€${summary.totalCost.toFixed(4)}`;
|
|
document.getElementById('totalSaved').textContent = `€${summary.totalSaved.toFixed(4)}`;
|
|
document.getElementById('compressionRatio').textContent = `${summary.compressionRatio}%`;
|
|
document.getElementById('requestCount').textContent = summary.requestCount.toString();
|
|
}
|
|
|
|
function updateCharts(costs, tokens) {
|
|
// Cost by Model Chart
|
|
const modelLabels = Object.keys(costs.byModel);
|
|
const modelCosts = Object.values(costs.byModel).map(m => m.cost);
|
|
|
|
const ctx1 = document.getElementById('costByModelChart').getContext('2d');
|
|
if (costByModelChart) costByModelChart.destroy();
|
|
costByModelChart = new Chart(ctx1, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: modelLabels,
|
|
datasets: [{
|
|
data: modelCosts,
|
|
backgroundColor: ['#6366f1', '#ec4899', '#f59e0b', '#10b981', '#06b6d4', '#8b5cf6'],
|
|
borderColor: '#fff',
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
|
|
// Tokens by Model Chart
|
|
const tokenLabels = Object.keys(tokens.byModel);
|
|
const tokenData = Object.values(tokens.byModel).map(m => m.in + m.out);
|
|
|
|
const ctx2 = document.getElementById('tokensByModelChart').getContext('2d');
|
|
if (tokensByModelChart) tokensByModelChart.destroy();
|
|
tokensByModelChart = new Chart(ctx2, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: tokenLabels,
|
|
datasets: [{
|
|
label: 'Total Tokens',
|
|
data: tokenData,
|
|
backgroundColor: '#6366f1',
|
|
borderRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
indexAxis: 'y',
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateAgentActivity(agents) {
|
|
const html = agents.length > 0
|
|
? agents.map(a => `
|
|
<div class="mb-3 pb-2 border-bottom">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<strong>${a.agent}</strong>
|
|
<span class="badge bg-primary">${a.taskCount} tasks</span>
|
|
</div>
|
|
<div class="text-muted small">
|
|
<div>Avg Cost: €${a.averageCost.toFixed(4)} | Confidence: ${(a.averageConfidence * 100).toFixed(1)}%</div>
|
|
<div>Tokens: ${a.totalTokens.toLocaleString()} | Last: ${new Date(a.lastActivity).toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
`).join('')
|
|
: '<p class="text-muted">No agent activity</p>';
|
|
document.getElementById('agentActivity').innerHTML = html;
|
|
}
|
|
|
|
function updateAlerts(alerts) {
|
|
const html = alerts.active > 0
|
|
? `<div class="alert alert-warning mb-3">
|
|
<strong>${alerts.active} Active Alerts</strong>
|
|
<div class="mt-2 small">
|
|
${Object.entries(alerts.byType).map(([type, count]) =>
|
|
`<div>• ${type}: ${count}</div>`
|
|
).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="small"><strong>Thresholds:</strong>
|
|
<div>Compression: ${alerts.thresholds.compressionBelow}%</div>
|
|
<div>Weekly Budget: €${alerts.thresholds.weeklyBudget}</div>
|
|
<div>External API: €${alerts.thresholds.externalApiCost}</div>
|
|
</div>`
|
|
: '<p class="text-muted">✓ No active alerts</p>';
|
|
document.getElementById('alertPanel').innerHTML = html;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
connectToStream();
|
|
refreshDashboard();
|
|
setInterval(() => refreshDashboard(), 30000);
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
if (eventSource) eventSource.close();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|