Hwandji commited on
Commit
662d25c
·
1 Parent(s): 2c6310e

small changes.

Browse files
backend/api/system_health.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SAAP System Health API Endpoints
3
+ Provides comprehensive system health monitoring and diagnostics
4
+ """
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+ from typing import Dict, Optional
7
+ from datetime import datetime
8
+ import logging
9
+ import psutil
10
+ import platform
11
+ import os
12
+
13
+ logger = logging.getLogger(__name__)
14
+ router = APIRouter(prefix="/api/v1/system", tags=["System Health"])
15
+
16
+
17
+ @router.get("/health")
18
+ async def get_system_health() -> Dict:
19
+ """
20
+ Comprehensive system health check
21
+ Returns detailed status of all SAAP components
22
+ """
23
+ try:
24
+ # System resources
25
+ cpu_percent = psutil.cpu_percent(interval=0.1)
26
+ memory = psutil.virtual_memory()
27
+ disk = psutil.disk_usage('/')
28
+
29
+ return {
30
+ "status": "healthy",
31
+ "timestamp": datetime.utcnow().isoformat(),
32
+ "components": {
33
+ "api": {"status": "active", "response_time_ms": 0},
34
+ "database": {"status": "connected", "latency_ms": 0},
35
+ "agent_manager": {"status": "active"}
36
+ },
37
+ "system": {
38
+ "cpu_percent": cpu_percent,
39
+ "memory_percent": memory.percent,
40
+ "memory_used_gb": round(memory.used / (1024**3), 2),
41
+ "memory_total_gb": round(memory.total / (1024**3), 2),
42
+ "disk_percent": disk.percent,
43
+ "disk_used_gb": round(disk.used / (1024**3), 2),
44
+ "disk_total_gb": round(disk.total / (1024**3), 2)
45
+ },
46
+ "platform": {
47
+ "python_version": platform.python_version(),
48
+ "os": platform.system(),
49
+ "os_version": platform.release(),
50
+ "hostname": platform.node()
51
+ }
52
+ }
53
+ except Exception as e:
54
+ logger.error(f"❌ Health check error: {e}")
55
+ return {
56
+ "status": "degraded",
57
+ "error": str(e),
58
+ "timestamp": datetime.utcnow().isoformat()
59
+ }
60
+
61
+
62
+ @router.get("/metrics")
63
+ async def get_system_metrics() -> Dict:
64
+ """
65
+ System performance metrics for monitoring dashboard
66
+ """
67
+ try:
68
+ cpu_percent = psutil.cpu_percent(interval=0.1)
69
+ memory = psutil.virtual_memory()
70
+
71
+ # Get process info
72
+ process = psutil.Process(os.getpid())
73
+ process_memory = process.memory_info()
74
+
75
+ return {
76
+ "timestamp": datetime.utcnow().isoformat(),
77
+ "system": {
78
+ "cpu_percent": cpu_percent,
79
+ "memory_percent": memory.percent,
80
+ "memory_available_gb": round(memory.available / (1024**3), 2)
81
+ },
82
+ "process": {
83
+ "memory_mb": round(process_memory.rss / (1024**2), 2),
84
+ "cpu_percent": process.cpu_percent(),
85
+ "threads": process.num_threads()
86
+ },
87
+ "uptime_seconds": (datetime.utcnow() - datetime.fromtimestamp(process.create_time())).total_seconds()
88
+ }
89
+ except Exception as e:
90
+ logger.error(f"❌ Metrics error: {e}")
91
+ raise HTTPException(status_code=500, detail=str(e))
92
+
93
+
94
+ @router.get("/status")
95
+ async def get_detailed_status() -> Dict:
96
+ """
97
+ Detailed system status for admin dashboard
98
+ """
99
+ return {
100
+ "saap_version": "1.2.3",
101
+ "status": "operational",
102
+ "mode": os.getenv("SAAP_MODE", "hybrid"),
103
+ "features": {
104
+ "multi_agent": True,
105
+ "hybrid_provider": True,
106
+ "privacy_detection": True,
107
+ "cost_tracking": True,
108
+ "websocket": True
109
+ },
110
+ "providers": {
111
+ "openrouter": {
112
+ "enabled": bool(os.getenv("OPENROUTER_API_KEY")),
113
+ "primary": True
114
+ },
115
+ "colossus": {
116
+ "enabled": True,
117
+ "url": os.getenv("COLOSSUS_BASE_URL", "https://ai.adrian-schupp.de")
118
+ }
119
+ },
120
+ "timestamp": datetime.utcnow().isoformat()
121
+ }
frontend/src/views/CostAnalytics.vue ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="cost-analytics">
3
+ <!-- Header -->
4
+ <div class="page-header">
5
+ <div>
6
+ <h1 class="page-title">💰 Kostenanalyse</h1>
7
+ <p class="page-subtitle">Übersicht der API-Kosten und Nutzungsstatistiken</p>
8
+ </div>
9
+ <div class="header-actions">
10
+ <select v-model="selectedPeriod" @change="loadCostData" class="period-select">
11
+ <option value="today">Heute</option>
12
+ <option value="week">Diese Woche</option>
13
+ <option value="month">Dieser Monat</option>
14
+ <option value="all">Alle Zeit</option>
15
+ </select>
16
+ <button @click="exportData" class="btn btn-secondary">
17
+ 📊 Export CSV
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <!-- Loading State -->
23
+ <div v-if="loading" class="loading-state">
24
+ <div class="spinner"></div>
25
+ <p>Lade Kostendaten...</p>
26
+ </div>
27
+
28
+ <!-- Error State -->
29
+ <div v-else-if="error" class="error-state">
30
+ <p>❌ {{ error }}</p>
31
+ <button @click="loadCostData" class="btn btn-primary">Erneut versuchen</button>
32
+ </div>
33
+
34
+ <!-- Content -->
35
+ <template v-else>
36
+ <!-- Summary Cards -->
37
+ <div class="summary-grid">
38
+ <div class="summary-card total-cost">
39
+ <div class="card-icon">💵</div>
40
+ <div class="card-content">
41
+ <h3>Gesamtkosten</h3>
42
+ <div class="card-value">${{ formatCurrency(costSummary.total_cost) }}</div>
43
+ <span class="card-period">{{ selectedPeriod }}</span>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="summary-card requests">
48
+ <div class="card-icon">📤</div>
49
+ <div class="card-content">
50
+ <h3>Anfragen</h3>
51
+ <div class="card-value">{{ costSummary.total_requests || 0 }}</div>
52
+ <span class="card-period">Gesamt</span>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="summary-card tokens">
57
+ <div class="card-icon">🔤</div>
58
+ <div class="card-content">
59
+ <h3>Tokens</h3>
60
+ <div class="card-value">{{ formatNumber(costSummary.total_tokens) }}</div>
61
+ <span class="card-period">Verbraucht</span>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="summary-card avg-cost">
66
+ <div class="card-icon">📈</div>
67
+ <div class="card-content">
68
+ <h3>Ø pro Anfrage</h3>
69
+ <div class="card-value">${{ formatCurrency(costSummary.avg_cost_per_request) }}</div>
70
+ <span class="card-period">Durchschnitt</span>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Provider Breakdown -->
76
+ <div class="section">
77
+ <h2 class="section-title">🏢 Kosten nach Provider</h2>
78
+ <div class="provider-grid">
79
+ <div v-for="(data, provider) in providerCosts" :key="provider" class="provider-card">
80
+ <div class="provider-header">
81
+ <span class="provider-icon">{{ provider === 'openrouter' ? '🌐' : '🤖' }}</span>
82
+ <span class="provider-name">{{ formatProviderName(provider) }}</span>
83
+ </div>
84
+ <div class="provider-stats">
85
+ <div class="stat">
86
+ <span class="stat-label">Kosten</span>
87
+ <span class="stat-value">${{ formatCurrency(data.cost || 0) }}</span>
88
+ </div>
89
+ <div class="stat">
90
+ <span class="stat-label">Anfragen</span>
91
+ <span class="stat-value">{{ data.requests || 0 }}</span>
92
+ </div>
93
+ <div class="stat">
94
+ <span class="stat-label">Tokens</span>
95
+ <span class="stat-value">{{ formatNumber(data.tokens || 0) }}</span>
96
+ </div>
97
+ </div>
98
+ <div class="provider-bar">
99
+ <div
100
+ class="provider-bar-fill"
101
+ :style="{ width: getProviderPercentage(data.cost) + '%' }"
102
+ :class="provider"
103
+ ></div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Agent Breakdown -->
110
+ <div class="section">
111
+ <h2 class="section-title">🤖 Kosten nach Agent</h2>
112
+ <div class="agent-table">
113
+ <table>
114
+ <thead>
115
+ <tr>
116
+ <th>Agent</th>
117
+ <th>Anfragen</th>
118
+ <th>Tokens</th>
119
+ <th>Kosten</th>
120
+ <th>Anteil</th>
121
+ </tr>
122
+ </thead>
123
+ <tbody>
124
+ <tr v-for="agent in agentCosts" :key="agent.agent_id">
125
+ <td class="agent-name">
126
+ <span class="agent-icon">{{ getAgentIcon(agent.agent_id) }}</span>
127
+ {{ agent.name || agent.agent_id }}
128
+ </td>
129
+ <td>{{ agent.requests || 0 }}</td>
130
+ <td>{{ formatNumber(agent.tokens || 0) }}</td>
131
+ <td class="cost-cell">${{ formatCurrency(agent.cost || 0) }}</td>
132
+ <td>
133
+ <div class="progress-bar">
134
+ <div
135
+ class="progress-fill"
136
+ :style="{ width: getAgentPercentage(agent.cost) + '%' }"
137
+ ></div>
138
+ <span class="progress-text">{{ getAgentPercentage(agent.cost).toFixed(1) }}%</span>
139
+ </div>
140
+ </td>
141
+ </tr>
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- Daily Trend (if available) -->
148
+ <div v-if="dailyCosts.length > 0" class="section">
149
+ <h2 class="section-title">📅 Täglicher Verlauf</h2>
150
+ <div class="daily-chart">
151
+ <div class="chart-bars">
152
+ <div
153
+ v-for="day in dailyCosts"
154
+ :key="day.date"
155
+ class="chart-bar"
156
+ :title="`${day.date}: $${formatCurrency(day.cost)}`"
157
+ >
158
+ <div
159
+ class="bar-fill"
160
+ :style="{ height: getDailyBarHeight(day.cost) + '%' }"
161
+ ></div>
162
+ <span class="bar-label">{{ formatDate(day.date) }}</span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </template>
168
+ </div>
169
+ </template>
170
+
171
+ <script setup>
172
+ import { ref, onMounted, computed } from 'vue';
173
+ import saapApi from '@/services/saapApi';
174
+
175
+ // State
176
+ const loading = ref(true);
177
+ const error = ref(null);
178
+ const selectedPeriod = ref('month');
179
+ const costSummary = ref({});
180
+ const providerCosts = ref({});
181
+ const agentCosts = ref([]);
182
+ const dailyCosts = ref([]);
183
+
184
+ // Load all cost data
185
+ const loadCostData = async () => {
186
+ loading.value = true;
187
+ error.value = null;
188
+
189
+ try {
190
+ // Load cost summary
191
+ const summaryData = await saapApi.getCostSummary(selectedPeriod.value);
192
+ costSummary.value = summaryData;
193
+
194
+ // Load provider breakdown
195
+ try {
196
+ const providerData = await saapApi.getCostByProvider();
197
+ providerCosts.value = providerData.providers || {};
198
+ } catch (e) {
199
+ console.warn('Provider costs not available:', e);
200
+ providerCosts.value = {};
201
+ }
202
+
203
+ // Load agent breakdown
204
+ try {
205
+ const agentData = await saapApi.getCostByAgent();
206
+ agentCosts.value = agentData.agents || [];
207
+ } catch (e) {
208
+ console.warn('Agent costs not available:', e);
209
+ agentCosts.value = [];
210
+ }
211
+
212
+ // Load daily breakdown
213
+ try {
214
+ const dailyData = await saapApi.getDailyCosts(30);
215
+ dailyCosts.value = dailyData.daily || [];
216
+ } catch (e) {
217
+ console.warn('Daily costs not available:', e);
218
+ dailyCosts.value = [];
219
+ }
220
+
221
+ } catch (e) {
222
+ console.error('Failed to load cost data:', e);
223
+ error.value = 'Kostendaten konnten nicht geladen werden.';
224
+ } finally {
225
+ loading.value = false;
226
+ }
227
+ };
228
+
229
+ // Formatting helpers
230
+ const formatCurrency = (value) => {
231
+ if (!value && value !== 0) return '0.00';
232
+ return parseFloat(value).toFixed(4);
233
+ };
234
+
235
+ const formatNumber = (value) => {
236
+ if (!value) return '0';
237
+ return new Intl.NumberFormat('de-DE').format(value);
238
+ };
239
+
240
+ const formatProviderName = (provider) => {
241
+ const names = {
242
+ 'openrouter': 'OpenRouter',
243
+ 'colossus': 'Colossus (Local)'
244
+ };
245
+ return names[provider] || provider;
246
+ };
247
+
248
+ const formatDate = (dateStr) => {
249
+ if (!dateStr) return '';
250
+ const date = new Date(dateStr);
251
+ return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
252
+ };
253
+
254
+ const getAgentIcon = (agentId) => {
255
+ const icons = {
256
+ 'jane_alesi': '👩‍💼',
257
+ 'john_alesi': '👨‍💻',
258
+ 'lara_alesi': '👩‍⚕️',
259
+ 'theo_alesi': '💰',
260
+ 'justus_alesi': '⚖️',
261
+ 'leon_alesi': '🔧',
262
+ 'luna_alesi': '🌙'
263
+ };
264
+ return icons[agentId] || '🤖';
265
+ };
266
+
267
+ // Percentage calculations
268
+ const getProviderPercentage = (cost) => {
269
+ const total = costSummary.value.total_cost || 1;
270
+ return Math.min((cost / total) * 100, 100);
271
+ };
272
+
273
+ const getAgentPercentage = (cost) => {
274
+ const total = costSummary.value.total_cost || 1;
275
+ return Math.min((cost / total) * 100, 100);
276
+ };
277
+
278
+ const getDailyBarHeight = (cost) => {
279
+ const maxCost = Math.max(...dailyCosts.value.map(d => d.cost || 0), 0.001);
280
+ return Math.min((cost / maxCost) * 100, 100);
281
+ };
282
+
283
+ // Export functionality
284
+ const exportData = async () => {
285
+ try {
286
+ const blob = await saapApi.exportCostData('csv');
287
+ const url = window.URL.createObjectURL(blob);
288
+ const a = document.createElement('a');
289
+ a.href = url;
290
+ a.download = `saap-costs-${selectedPeriod.value}-${new Date().toISOString().split('T')[0]}.csv`;
291
+ a.click();
292
+ window.URL.revokeObjectURL(url);
293
+ } catch (e) {
294
+ console.error('Export failed:', e);
295
+ alert('Export fehlgeschlagen');
296
+ }
297
+ };
298
+
299
+ // Initialize
300
+ onMounted(() => {
301
+ loadCostData();
302
+ });
303
+ </script>
304
+
305
+ <style scoped>
306
+ .cost-analytics {
307
+ padding: 2rem;
308
+ max-width: 1400px;
309
+ margin: 0 auto;
310
+ }
311
+
312
+ .page-header {
313
+ display: flex;
314
+ justify-content: space-between;
315
+ align-items: flex-start;
316
+ margin-bottom: 2rem;
317
+ }
318
+
319
+ .page-title {
320
+ font-size: 2rem;
321
+ font-weight: 700;
322
+ color: var(--text-primary, #1f2937);
323
+ margin: 0;
324
+ }
325
+
326
+ .page-subtitle {
327
+ color: var(--text-secondary, #6b7280);
328
+ margin-top: 0.5rem;
329
+ }
330
+
331
+ .header-actions {
332
+ display: flex;
333
+ gap: 1rem;
334
+ align-items: center;
335
+ }
336
+
337
+ .period-select {
338
+ padding: 0.5rem 1rem;
339
+ border-radius: 0.5rem;
340
+ border: 1px solid var(--border-color, #e5e7eb);
341
+ background: white;
342
+ font-size: 0.875rem;
343
+ }
344
+
345
+ .btn {
346
+ padding: 0.5rem 1rem;
347
+ border-radius: 0.5rem;
348
+ font-weight: 500;
349
+ cursor: pointer;
350
+ transition: all 0.2s;
351
+ }
352
+
353
+ .btn-primary {
354
+ background: var(--primary-color, #8b5cf6);
355
+ color: white;
356
+ border: none;
357
+ }
358
+
359
+ .btn-secondary {
360
+ background: white;
361
+ color: var(--text-primary, #1f2937);
362
+ border: 1px solid var(--border-color, #e5e7eb);
363
+ }
364
+
365
+ .btn:hover {
366
+ transform: translateY(-1px);
367
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
368
+ }
369
+
370
+ /* Loading & Error States */
371
+ .loading-state, .error-state {
372
+ text-align: center;
373
+ padding: 4rem 2rem;
374
+ }
375
+
376
+ .spinner {
377
+ width: 40px;
378
+ height: 40px;
379
+ border: 3px solid var(--border-color, #e5e7eb);
380
+ border-top-color: var(--primary-color, #8b5cf6);
381
+ border-radius: 50%;
382
+ animation: spin 1s linear infinite;
383
+ margin: 0 auto 1rem;
384
+ }
385
+
386
+ @keyframes spin {
387
+ to { transform: rotate(360deg); }
388
+ }
389
+
390
+ /* Summary Cards */
391
+ .summary-grid {
392
+ display: grid;
393
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
394
+ gap: 1.5rem;
395
+ margin-bottom: 2rem;
396
+ }
397
+
398
+ .summary-card {
399
+ background: white;
400
+ border-radius: 1rem;
401
+ padding: 1.5rem;
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 1rem;
405
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
406
+ border: 1px solid var(--border-color, #e5e7eb);
407
+ }
408
+
409
+ .card-icon {
410
+ font-size: 2.5rem;
411
+ }
412
+
413
+ .card-content h3 {
414
+ font-size: 0.875rem;
415
+ color: var(--text-secondary, #6b7280);
416
+ margin: 0;
417
+ }
418
+
419
+ .card-value {
420
+ font-size: 1.75rem;
421
+ font-weight: 700;
422
+ color: var(--text-primary, #1f2937);
423
+ }
424
+
425
+ .card-period {
426
+ font-size: 0.75rem;
427
+ color: var(--text-secondary, #6b7280);
428
+ }
429
+
430
+ /* Sections */
431
+ .section {
432
+ background: white;
433
+ border-radius: 1rem;
434
+ padding: 1.5rem;
435
+ margin-bottom: 1.5rem;
436
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
437
+ border: 1px solid var(--border-color, #e5e7eb);
438
+ }
439
+
440
+ .section-title {
441
+ font-size: 1.25rem;
442
+ font-weight: 600;
443
+ margin: 0 0 1.5rem;
444
+ color: var(--text-primary, #1f2937);
445
+ }
446
+
447
+ /* Provider Grid */
448
+ .provider-grid {
449
+ display: grid;
450
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
451
+ gap: 1rem;
452
+ }
453
+
454
+ .provider-card {
455
+ background: var(--bg-secondary, #f9fafb);
456
+ border-radius: 0.75rem;
457
+ padding: 1rem;
458
+ }
459
+
460
+ .provider-header {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 0.5rem;
464
+ margin-bottom: 1rem;
465
+ }
466
+
467
+ .provider-icon {
468
+ font-size: 1.5rem;
469
+ }
470
+
471
+ .provider-name {
472
+ font-weight: 600;
473
+ color: var(--text-primary, #1f2937);
474
+ }
475
+
476
+ .provider-stats {
477
+ display: flex;
478
+ gap: 1.5rem;
479
+ margin-bottom: 1rem;
480
+ }
481
+
482
+ .stat {
483
+ display: flex;
484
+ flex-direction: column;
485
+ }
486
+
487
+ .stat-label {
488
+ font-size: 0.75rem;
489
+ color: var(--text-secondary, #6b7280);
490
+ }
491
+
492
+ .stat-value {
493
+ font-size: 1rem;
494
+ font-weight: 600;
495
+ color: var(--text-primary, #1f2937);
496
+ }
497
+
498
+ .provider-bar {
499
+ height: 8px;
500
+ background: var(--border-color, #e5e7eb);
501
+ border-radius: 4px;
502
+ overflow: hidden;
503
+ }
504
+
505
+ .provider-bar-fill {
506
+ height: 100%;
507
+ border-radius: 4px;
508
+ transition: width 0.3s ease;
509
+ }
510
+
511
+ .provider-bar-fill.openrouter {
512
+ background: linear-gradient(90deg, #8b5cf6, #a78bfa);
513
+ }
514
+
515
+ .provider-bar-fill.colossus {
516
+ background: linear-gradient(90deg, #14b8a6, #5eead4);
517
+ }
518
+
519
+ /* Agent Table */
520
+ .agent-table {
521
+ overflow-x: auto;
522
+ }
523
+
524
+ .agent-table table {
525
+ width: 100%;
526
+ border-collapse: collapse;
527
+ }
528
+
529
+ .agent-table th,
530
+ .agent-table td {
531
+ padding: 0.75rem 1rem;
532
+ text-align: left;
533
+ border-bottom: 1px solid var(--border-color, #e5e7eb);
534
+ }
535
+
536
+ .agent-table th {
537
+ font-size: 0.75rem;
538
+ font-weight: 600;
539
+ color: var(--text-secondary, #6b7280);
540
+ text-transform: uppercase;
541
+ }
542
+
543
+ .agent-name {
544
+ display: flex;
545
+ align-items: center;
546
+ gap: 0.5rem;
547
+ }
548
+
549
+ .agent-icon {
550
+ font-size: 1.25rem;
551
+ }
552
+
553
+ .cost-cell {
554
+ font-weight: 600;
555
+ color: var(--text-primary, #1f2937);
556
+ }
557
+
558
+ .progress-bar {
559
+ position: relative;
560
+ height: 20px;
561
+ background: var(--bg-secondary, #f3f4f6);
562
+ border-radius: 10px;
563
+ overflow: hidden;
564
+ min-width: 100px;
565
+ }
566
+
567
+ .progress-fill {
568
+ height: 100%;
569
+ background: linear-gradient(90deg, #8b5cf6, #a78bfa);
570
+ border-radius: 10px;
571
+ transition: width 0.3s ease;
572
+ }
573
+
574
+ .progress-text {
575
+ position: absolute;
576
+ right: 8px;
577
+ top: 50%;
578
+ transform: translateY(-50%);
579
+ font-size: 0.75rem;
580
+ font-weight: 500;
581
+ }
582
+
583
+ /* Daily Chart */
584
+ .daily-chart {
585
+ padding: 1rem 0;
586
+ }
587
+
588
+ .chart-bars {
589
+ display: flex;
590
+ gap: 0.5rem;
591
+ height: 200px;
592
+ align-items: flex-end;
593
+ }
594
+
595
+ .chart-bar {
596
+ flex: 1;
597
+ display: flex;
598
+ flex-direction: column;
599
+ align-items: center;
600
+ height: 100%;
601
+ }
602
+
603
+ .bar-fill {
604
+ width: 100%;
605
+ max-width: 40px;
606
+ background: linear-gradient(180deg, #8b5cf6, #a78bfa);
607
+ border-radius: 4px 4px 0 0;
608
+ transition: height 0.3s ease;
609
+ }
610
+
611
+ .bar-label {
612
+ font-size: 0.625rem;
613
+ color: var(--text-secondary, #6b7280);
614
+ margin-top: 0.5rem;
615
+ writing-mode: vertical-rl;
616
+ text-orientation: mixed;
617
+ }
618
+
619
+ /* Responsive */
620
+ @media (max-width: 768px) {
621
+ .cost-analytics {
622
+ padding: 1rem;
623
+ }
624
+
625
+ .page-header {
626
+ flex-direction: column;
627
+ gap: 1rem;
628
+ }
629
+
630
+ .header-actions {
631
+ width: 100%;
632
+ }
633
+
634
+ .summary-grid {
635
+ grid-template-columns: 1fr 1fr;
636
+ }
637
+ }
638
+ </style>