- FastAPI async gateway with httpx proxying to multiple upstreams - SQLite database with SQLAlchemy ORM - Admin console: manage services, users, API keys, endpoint access - Per-key, per-endpoint granular access control - OpenAPI document sync and caching (5-minute TTL) - Request/response logging with full transaction inspection - In-memory rate limiting (per-key, fixed-window) - Tiered log retention (7d payloads, 90d rows, incremental vacuum) - TLS verification toggle per service (for self-signed certificates) - Service connectivity validation with automatic endpoint refresh - Request browser with filters and deep-link inspection - Docker setup with persistent volume - Modal forms for create/edit flows Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
96 lines
3.7 KiB
HTML
96 lines
3.7 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Dashboard · API Gateway{% endblock %}
|
||
{% block content %}
|
||
<h1>Dashboard</h1>
|
||
|
||
<div class="tiles" id="tiles">
|
||
<div class="tile"><div class="label">Requests (24h)</div><div class="value" id="t-total">–</div></div>
|
||
<div class="tile"><div class="label">Error rate (24h)</div><div class="value" id="t-errors">–</div></div>
|
||
<div class="tile"><div class="label">Avg latency (24h)</div><div class="value" id="t-latency">–</div></div>
|
||
<div class="tile"><div class="label">Active services</div><div class="value" id="t-services">–</div></div>
|
||
<div class="tile"><div class="label">Active API keys</div><div class="value" id="t-keys">–</div></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Traffic — last 24 hours</h2>
|
||
<div class="chart-box"><canvas id="chart-traffic"></canvas></div>
|
||
</div>
|
||
|
||
<div class="grid cols-2">
|
||
<div class="card">
|
||
<h2>Requests by service (24h)</h2>
|
||
<div class="chart-box"><canvas id="chart-services"></canvas></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Recent requests</h2>
|
||
<table>
|
||
<thead><tr><th>Time (UTC)</th><th>Service</th><th>Path</th><th>Status</th><th>ms</th></tr></thead>
|
||
<tbody>
|
||
{% for log in recent %}
|
||
<tr>
|
||
<td>{{ log.timestamp.strftime("%H:%M:%S") }}</td>
|
||
<td class="strong">{{ log.service.name if log.service else "–" }}</td>
|
||
<td>{{ log.method }} {{ log.path[:40] }}</td>
|
||
<td class="status-{{ log.status_code // 100 }}xx">{{ log.status_code }}</td>
|
||
<td>{{ "%.0f" | format(log.latency_ms) }}</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="5">No traffic yet. Send a request through <code>/gw/<service>/<path></code>.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(async () => {
|
||
const s = await GW.fetchJSON('/admin/api/stats/summary?hours=24');
|
||
document.getElementById('t-total').textContent = s.total_requests.toLocaleString();
|
||
document.getElementById('t-errors').textContent = s.error_rate + '%';
|
||
document.getElementById('t-latency').innerHTML = s.avg_latency_ms + '<small> ms</small>';
|
||
document.getElementById('t-services').textContent = s.active_services;
|
||
document.getElementById('t-keys').textContent = s.active_keys;
|
||
|
||
const ts = await GW.fetchJSON('/admin/api/stats/timeseries?hours=24');
|
||
new Chart(document.getElementById('chart-traffic'), {
|
||
type: 'line',
|
||
data: {
|
||
labels: ts.labels.map(l => l.slice(11)),
|
||
datasets: [
|
||
{ label: 'Succeeded', data: ts.ok, borderColor: GW.blue, backgroundColor: GW.blue,
|
||
borderWidth: 2, pointRadius: 0, pointHoverRadius: 4, tension: 0.3 },
|
||
{ label: 'Errors (5xx)', data: ts.errors, borderColor: GW.red, backgroundColor: GW.red,
|
||
borderWidth: 2, pointRadius: 0, pointHoverRadius: 4, tension: 0.3 },
|
||
],
|
||
},
|
||
options: {
|
||
maintainAspectRatio: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
scales: {
|
||
x: { grid: { display: false }, ticks: { maxTicksLimit: 12 } },
|
||
y: { beginAtZero: true, ticks: { precision: 0 } },
|
||
},
|
||
},
|
||
});
|
||
|
||
const bs = await GW.fetchJSON('/admin/api/stats/by-service?hours=24');
|
||
new Chart(document.getElementById('chart-services'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: bs.labels,
|
||
datasets: [{ label: 'Requests', data: bs.counts, backgroundColor: GW.blue,
|
||
borderRadius: 4, maxBarThickness: 26 }],
|
||
},
|
||
options: {
|
||
maintainAspectRatio: false,
|
||
indexAxis: 'y',
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, ticks: { precision: 0 } }, y: { grid: { display: false } } },
|
||
},
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|