| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AskBookie API</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background: #000; |
| color: #fafafa; |
| min-height: 100vh; |
| padding: 2rem; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| .header { |
| margin-bottom: 2rem; |
| padding-bottom: 1rem; |
| border-bottom: 1px solid #27272a; |
| } |
| |
| .header h1 { |
| font-size: 1.5rem; |
| font-weight: 600; |
| margin-bottom: 0.25rem; |
| } |
| |
| .header p { |
| font-size: 0.875rem; |
| color: #71717a; |
| } |
| |
| .status { |
| display: inline-block; |
| width: 8px; |
| height: 8px; |
| background: #22c55e; |
| border-radius: 50%; |
| margin-right: 0.5rem; |
| } |
| |
| .grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 1rem; |
| margin-bottom: 2rem; |
| } |
| |
| .card { |
| background: #0a0a0a; |
| border: 1px solid #27272a; |
| border-radius: 8px; |
| padding: 1.25rem; |
| } |
| |
| .card:hover { |
| border-color: #3f3f46; |
| } |
| |
| .card-label { |
| font-size: 0.75rem; |
| color: #71717a; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| margin-bottom: 0.5rem; |
| } |
| |
| .card-value { |
| font-size: 2rem; |
| font-weight: 600; |
| margin-bottom: 0.25rem; |
| } |
| |
| .card-desc { |
| font-size: 0.875rem; |
| color: #a1a1aa; |
| } |
| |
| .section { |
| background: #0a0a0a; |
| border: 1px solid #27272a; |
| border-radius: 8px; |
| padding: 1.5rem; |
| margin-bottom: 1rem; |
| } |
| |
| .section-title { |
| font-size: 1rem; |
| font-weight: 600; |
| margin-bottom: 1rem; |
| } |
| |
| .user-card { |
| background: #000; |
| border: 1px solid #27272a; |
| border-radius: 6px; |
| padding: 1rem; |
| margin-bottom: 0.75rem; |
| } |
| |
| .user-card:hover { |
| border-color: #3f3f46; |
| } |
| |
| .user-header { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| margin-bottom: 1rem; |
| padding-bottom: 0.75rem; |
| border-bottom: 1px solid #27272a; |
| } |
| |
| .user-name { |
| font-size: 0.875rem; |
| font-weight: 500; |
| } |
| |
| .badge { |
| font-size: 0.625rem; |
| padding: 0.125rem 0.5rem; |
| background: #27272a; |
| border-radius: 4px; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| color: #a1a1aa; |
| } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); |
| gap: 0.75rem; |
| } |
| |
| .stat { |
| text-align: center; |
| } |
| |
| .stat-value { |
| font-size: 1.25rem; |
| font-weight: 600; |
| margin-bottom: 0.125rem; |
| } |
| |
| .stat-label { |
| font-size: 0.75rem; |
| color: #71717a; |
| } |
| |
| .btn { |
| position: fixed; |
| bottom: 3rem; |
| right: 2rem; |
| background: #fafafa; |
| color: #000; |
| border: none; |
| padding: 0.75rem 1.5rem; |
| border-radius: 6px; |
| font-size: 0.875rem; |
| font-weight: 500; |
| cursor: pointer; |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .btn:hover { |
| background: #e5e5e5; |
| } |
| |
| .btn:active { |
| background: #d4d4d4; |
| } |
| |
| .footer { |
| text-align: center; |
| font-size: 0.75rem; |
| color: #52525b; |
| margin-top: 2rem; |
| } |
| |
| @media (max-width: 768px) { |
| body { |
| padding: 1rem; |
| } |
| |
| .grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>AskBookie API</h1> |
| <p><span class="status"></span>Live Dashboard</p> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <div class="card-label">Uptime</div> |
| <div class="card-value" id="uptime">--</div> |
| <div class="card-desc" id="uptime-desc"></div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-label">API Calls</div> |
| <div class="card-value" id="total-calls">--</div> |
| <div class="card-desc">total</div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-label">Questions</div> |
| <div class="card-value" id="total-questions">--</div> |
| <div class="card-desc">asked</div> |
| </div> |
|
|
|
|
| <div class="card"> |
| <div class="card-label">Memory</div> |
| <div class="card-value" id="memory">--</div> |
| <div class="card-desc">MB</div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-label">Active Model</div> |
| <div class="card-value" id="model-name" style="font-size: 1rem;">--</div> |
| <div class="card-desc" id="model-desc">--</div> |
| </div> |
| </div> |
|
|
| <div class="section"> |
| <div class="section-title">Per-User Analytics</div> |
| <div id="user-stats"></div> |
| </div> |
| </div> |
|
|
| <button class="btn" onclick="loadMetrics()"> |
| <span id="refresh-text">Refresh</span> |
| </button> |
|
|
| <div class="footer" id="last-update"></div> |
|
|
| <script> |
| async function loadMetrics() { |
| const refreshText = document.getElementById('refresh-text'); |
| const originalText = refreshText.textContent; |
| refreshText.textContent = 'Loading...'; |
| |
| try { |
| const response = await fetch('/health'); |
| const health = await response.json(); |
| |
| |
| const hours = health.uptime_hours || 0; |
| const days = Math.floor(hours / 24); |
| const hrs = Math.floor(hours % 24); |
| const mins = Math.floor((hours * 60) % 60); |
| |
| if (days > 0) { |
| document.getElementById('uptime').textContent = days; |
| document.getElementById('uptime-desc').textContent = `days, ${hrs}h ${mins}m`; |
| } else if (hrs > 0) { |
| document.getElementById('uptime').textContent = hrs; |
| document.getElementById('uptime-desc').textContent = `hours, ${mins}m`; |
| } else { |
| document.getElementById('uptime').textContent = mins; |
| document.getElementById('uptime-desc').textContent = 'minutes'; |
| } |
| |
| document.getElementById('total-calls').textContent = health.total_api_calls || 0; |
| document.getElementById('total-questions').textContent = health.total_questions || 0; |
| document.getElementById('memory').textContent = (health.memory_mb || 0).toFixed(0); |
| |
| if (health.current_model) { |
| document.getElementById('model-name').textContent = health.current_model.name || '--'; |
| document.getElementById('model-desc').textContent = health.current_model.description || '--'; |
| } |
| |
| renderUserStats(health.per_user || {}); |
| |
| document.getElementById('last-update').textContent = |
| `Updated ${new Date().toLocaleTimeString()}`; |
| |
| } catch (error) { |
| console.error('Failed to load metrics:', error); |
| } finally { |
| refreshText.textContent = originalText; |
| } |
| } |
| |
| function renderUserStats(users) { |
| const container = document.getElementById('user-stats'); |
| container.innerHTML = ''; |
| |
| for (const [userName, stats] of Object.entries(users)) { |
| const userCard = document.createElement('div'); |
| userCard.className = 'user-card'; |
| |
| userCard.innerHTML = ` |
| <div class="user-header"> |
| <span class="user-name">${userName}</span> |
| <span class="badge">${stats.role || 'user'}</span> |
| </div> |
| <div class="stats-grid"> |
| <div class="stat"> |
| <div class="stat-value">${stats.api_calls || 0}</div> |
| <div class="stat-label">calls</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-value">${stats.questions_asked || 0}</div> |
| <div class="stat-label">questions</div> |
| </div> |
| |
| <div class="stat"> |
| <div class="stat-value" style="color: ${stats.success_rate < 100 ? '#ef4444' : '#22c55e'}">${stats.success_rate || 100}%</div> |
| <div class="stat-label">success</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-value">${stats.average_latency_seconds || 0}s</div> |
| <div class="stat-label">latency</div> |
| </div> |
| </div> |
| ${(stats.ask_fails > 0) ? ` |
| <div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #27272a; font-size: 0.75rem; color: #ef4444;"> |
| Failures: ${stats.ask_fails} ask |
| </div> |
| ` : ''} |
| `; |
| |
| container.appendChild(userCard); |
| } |
| } |
| |
| setInterval(loadMetrics, 5000); |
| loadMetrics(); |
| </script> |
| </body> |
|
|
| </html> |