Initial commit: API gateway with admin console

- 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>
This commit is contained in:
Samuel Amar
2026-07-29 14:00:22 +02:00
co-authored by Claude Haiku 4.5
commit 77d7a50fa9
31 changed files with 2927 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
{% 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/&lt;service&gt;/&lt;path&gt;</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 %}