Files
Api-Gateway/app/templates/requests.html
T
Samuel AmarandClaude Haiku 4.5 77d7a50fa9 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>
2026-07-29 14:00:22 +02:00

174 lines
7.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Requests · API Gateway{% endblock %}
{% block content %}
<h1>Requests</h1>
<div class="req-split">
<div class="card">
<form method="get" action="/admin/requests" class="row" id="filter-form">
<select name="service_id" style="max-width:160px">
<option value="">All services</option>
{% for s in services %}<option value="{{ s.id }}" {% if f.service_id == s.id %}selected{% endif %}>{{ s.name }}</option>{% endfor %}
</select>
<select name="user_id" style="max-width:140px">
<option value="">All users</option>
{% for u in users %}<option value="{{ u.id }}" {% if f.user_id == u.id %}selected{% endif %}>{{ u.username }}</option>{% endfor %}
</select>
<select name="key_id" style="max-width:150px">
<option value="">All keys</option>
{% for k in keys %}<option value="{{ k.id }}" {% if f.key_id == k.id %}selected{% endif %}>{{ k.name }}</option>{% endfor %}
</select>
<select name="status_class" style="max-width:120px">
<option value="">All statuses</option>
{% for c in ['2', '3', '4', '5'] %}
<option value="{{ c }}" {% if f.status_class == c %}selected{% endif %}>{{ c }}xx</option>
{% endfor %}
</select>
<input type="text" name="q" value="{{ f.q }}" placeholder="Path contains…" style="max-width:200px">
<span class="right hint">{{ total }} request{{ '' if total == 1 else 's' }}</span>
</form>
</div>
<div class="card req-list">
<div class="table-scroll">
<table id="req-table">
<thead><tr><th>Time (UTC)</th><th>Service</th><th>Key</th><th>Request</th><th>Status</th><th>Latency</th></tr></thead>
<tbody>
{% for log in logs %}
<tr class="req-row" data-id="{{ log.id }}" tabindex="0" role="button"
aria-label="Inspect request {{ log.id }}">
<td>{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td class="strong">{{ log.service.name if log.service else "" }}</td>
<td>{{ log.api_key.name if log.api_key else "" }}</td>
<td>{{ log.method }} {{ log.path[:60] }}{% if log.query_string %}?{{ log.query_string[:30] }}{% endif %}</td>
<td class="status-{{ log.status_code // 100 }}xx">{{ log.status_code }}</td>
<td>{{ "%.1f" | format(log.latency_ms) }} ms</td>
</tr>
{% else %}
<tr><td colspan="6">No requests match these filters.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pages > 1 %}
{% set base = '/admin/requests?service_id=' ~ (f.service_id or '') ~ '&user_id=' ~ (f.user_id or '') ~ '&key_id=' ~ (f.key_id or '') ~ '&status_class=' ~ f.status_class ~ '&q=' ~ f.q %}
<div class="row" style="margin-top:12px">
{% if page > 1 %}<a class="btn ghost" href="{{ base }}&page={{ page - 1 }}" style="padding:5px 10px">← Newer</a>{% endif %}
<span class="hint">Page {{ page }} of {{ pages }}</span>
{% if page < pages %}<a class="btn ghost" href="{{ base }}&page={{ page + 1 }}" style="padding:5px 10px">Older →</a>{% endif %}
</div>
{% endif %}
</div>
<div class="card" id="inspector" hidden>
<div class="row" style="margin-bottom:12px">
<h2 style="margin:0">Request <span id="i-id"></span></h2>
<span id="i-status" class="badge"></span>
<span class="hint" id="i-latency"></span>
<span class="hint" id="i-time"></span>
<button type="button" class="icon right" id="i-close" title="Close inspector" aria-label="Close inspector">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="inspector-body">
<table style="margin-bottom:16px">
<tbody>
<tr><td class="strong" style="width:160px">Gateway request</td><td><code id="i-gwreq"></code></td></tr>
<tr><td class="strong">Forwarded to</td><td><code id="i-fwd"></code></td></tr>
<tr><td class="strong">Service</td><td id="i-service"></td></tr>
<tr><td class="strong">Matched endpoint</td><td id="i-endpoint"></td></tr>
<tr><td class="strong">API key</td><td id="i-key"></td></tr>
<tr><td class="strong">User</td><td id="i-user"></td></tr>
<tr><td class="strong">Client IP</td><td id="i-ip"></td></tr>
</tbody>
</table>
<div class="grid cols-2">
<div>
<h2>Request payload</h2>
<pre id="i-reqbody"></pre>
</div>
<div>
<h2>Response payload</h2>
<pre id="i-resbody"></pre>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const inspector = document.getElementById('inspector');
const $ = id => document.getElementById(id);
async function inspect(id) {
const r = await fetch(`/admin/requests/${id}/data`);
if (!r.ok) return;
const d = await r.json();
$('i-id').textContent = '#' + d.id;
$('i-status').textContent = d.status;
$('i-status').className = `badge status-${Math.floor(d.status / 100)}xx`;
$('i-latency').textContent = d.latency_ms + ' ms';
$('i-time').textContent = d.time + ' UTC';
const qs = d.query_string ? '?' + d.query_string : '';
$('i-gwreq').textContent = `${d.method} /${d.slug ?? '?'}${d.path}${qs}`;
$('i-fwd').textContent = d.forwarded_to ?? '';
$('i-service').textContent = d.service ?? '';
$('i-endpoint').textContent = d.endpoint
? d.endpoint + (d.endpoint_description ? ' — ' + d.endpoint_description : '') : '';
$('i-key').textContent = d.key ? `${d.key} (${d.key_prefix}…)` : '';
$('i-user').textContent = d.user ?? '';
$('i-ip').textContent = d.client_ip || '';
$('i-reqbody').textContent = d.request_body || 'No request body.';
$('i-resbody').textContent = d.response_body || 'No response body captured.';
document.querySelectorAll('.req-row').forEach(row =>
row.classList.toggle('selected', +row.dataset.id === d.id));
inspector.hidden = false;
inspector.querySelector('.inspector-body').scrollTop = 0;
const url = new URL(location);
url.searchParams.set('inspect', d.id);
history.replaceState(null, '', url);
}
document.querySelectorAll('.req-row').forEach(row => {
row.addEventListener('click', () => inspect(+row.dataset.id));
row.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); inspect(+row.dataset.id); }
});
});
document.getElementById('i-close').addEventListener('click', () => {
inspector.hidden = true;
document.querySelectorAll('.req-row.selected').forEach(r => r.classList.remove('selected'));
const url = new URL(location);
url.searchParams.delete('inspect');
history.replaceState(null, '', url);
});
const preselected = new URLSearchParams(location.search).get('inspect');
if (preselected) inspect(+preselected);
// Filters apply immediately; empty fields are omitted from the URL.
const filterForm = document.getElementById('filter-form');
function applyFilters() {
const p = new URLSearchParams();
for (const el of filterForm.elements) {
if (el.name && el.value) p.set(el.name, el.value);
}
location.href = '/admin/requests' + (p.size ? '?' + p.toString() : '');
}
filterForm.addEventListener('submit', e => { e.preventDefault(); applyFilters(); });
filterForm.querySelectorAll('select').forEach(s => s.addEventListener('change', applyFilters));
let debounce;
filterForm.querySelector('input[name=q]').addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(applyFilters, 500);
});
</script>
{% endblock %}