128 lines
4.4 KiB
HTML
128 lines
4.4 KiB
HTML
{{define "content"}}
|
|
<div hx-ext="sse" sse-connect="/api/events" hx-get="/dashboard" hx-trigger="sse:refresh" hx-target="#content" hx-swap="innerHTML">
|
|
<div class="page-header">
|
|
<h1>Dashboard</h1>
|
|
</div>
|
|
|
|
<div class="cards">
|
|
{{with .Summary.Today}}
|
|
<div class="card"><div class="label">Requests Today</div><div class="value">{{.Requests}}</div></div>
|
|
<div class="card"><div class="label">Cost Today</div><div class="value green">{{formatCost .CostUSD}}</div></div>
|
|
<div class="card"><div class="label">Tokens Today</div><div class="value blue">{{addInt .InputTokens .OutputTokens}}</div><div class="sub">{{.InputTokens}} in / {{.OutputTokens}} out</div></div>
|
|
<div class="card"><div class="label">Errors Today</div><div class="value {{if gt .Errors 0}}red{{end}}">{{.Errors}}</div></div>
|
|
<div class="card"><div class="label">Cache Hits</div><div class="value">{{.CachedHits}}</div></div>
|
|
{{end}}
|
|
{{with .Summary.Week}}
|
|
<div class="card"><div class="label">Cost (7d)</div><div class="value green">{{formatCost .CostUSD}}</div></div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="active" onclick="loadTimeseries('24h', this)">24h</button>
|
|
<button onclick="loadTimeseries('7d', this)">7d</button>
|
|
<button onclick="loadTimeseries('30d', this)">30d</button>
|
|
</div>
|
|
<div class="section">
|
|
<h2>Requests & Cost</h2>
|
|
<canvas id="chart" height="200"></canvas>
|
|
</div>
|
|
|
|
{{if .Models}}
|
|
<div class="section">
|
|
<h2>Models</h2>
|
|
<table>
|
|
<thead><tr><th>Model</th><th>Requests</th><th>Tokens (in/out)</th><th>Cost</th><th>Avg Latency</th></tr></thead>
|
|
<tbody>
|
|
{{range .Models}}
|
|
<tr>
|
|
<td>{{.Model}}</td>
|
|
<td>{{.Requests}}</td>
|
|
<td>{{.InputTokens}} / {{.OutputTokens}}</td>
|
|
<td class="green">{{formatCost .CostUSD}}</td>
|
|
<td>{{printf "%.0f" .AvgLatencyMS}}ms</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Providers}}
|
|
<div class="section">
|
|
<h2>Providers</h2>
|
|
<table>
|
|
<thead><tr><th>Provider</th><th>Requests</th><th>Success</th><th>Errors</th><th>Avg Latency</th><th>Cost</th></tr></thead>
|
|
<tbody>
|
|
{{range .Providers}}
|
|
<tr>
|
|
<td>{{.Provider}}</td>
|
|
<td>{{.Requests}}</td>
|
|
<td class="green">{{.Successes}}</td>
|
|
<td class="red">{{.Errors}}</td>
|
|
<td>{{printf "%.0f" .AvgLatencyMS}}ms</td>
|
|
<td class="green">{{formatCost .CostUSD}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .TokenStats}}
|
|
<div class="section">
|
|
<h2>API Token Usage</h2>
|
|
<table>
|
|
<thead><tr><th>Token</th><th>Requests</th><th>Tokens (in/out)</th><th>Cost</th></tr></thead>
|
|
<tbody>
|
|
{{range .TokenStats}}
|
|
<tr>
|
|
<td>{{.TokenName}}</td>
|
|
<td>{{.Requests}}</td>
|
|
<td>{{.InputTokens}} / {{.OutputTokens}}</td>
|
|
<td class="green">{{formatCost .CostUSD}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{end}}
|
|
|
|
<script>
|
|
var _chart;
|
|
function loadTimeseries(period, btn) {
|
|
document.querySelectorAll('.tabs button').forEach(function(b) { b.classList.remove('active'); });
|
|
if (btn) btn.classList.add('active');
|
|
else document.querySelector('.tabs button').classList.add('active');
|
|
fetch('/api/stats/timeseries?period=' + period, {credentials: 'same-origin'})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var labels = (data||[]).map(function(d) { return d.bucket; });
|
|
var requests = (data||[]).map(function(d) { return d.requests; });
|
|
var costs = (data||[]).map(function(d) { return d.cost_usd; });
|
|
if (_chart) _chart.destroy();
|
|
_chart = new Chart(document.getElementById('chart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{ label: 'Requests', data: requests, backgroundColor: '#3b82f680', yAxisID: 'y' },
|
|
{ label: 'Cost ($)', data: costs, type: 'line', borderColor: '#4ade80', backgroundColor: '#4ade8020', yAxisID: 'y1', tension: 0.3 }
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
interaction: { mode: 'index', intersect: false },
|
|
scales: {
|
|
y: { position: 'left', ticks: { color: '#94a3b8' }, grid: { color: '#1e293b' } },
|
|
y1: { position: 'right', ticks: { color: '#4ade80' }, grid: { display: false } },
|
|
x: { ticks: { color: '#94a3b8', maxRotation: 45 }, grid: { color: '#1e293b' } }
|
|
},
|
|
plugins: { legend: { labels: { color: '#e2e8f0' } } }
|
|
}
|
|
});
|
|
}).catch(function(){});
|
|
}
|
|
loadTimeseries('24h');
|
|
</script>
|
|
</div>
|
|
{{end}}
|