Rene Fichtmueller 2ca77d0aee feat: Phase 2F — Multi-Agent Integration (ADRs + Client Fallback + Tests)
- 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
2026-04-19 21:39:44 +02:00

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>