Include SVG favicon showing hexagon network topology with nodes and connections, integrated into admin console and /docs portal for consistent branding. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
92 lines
3.4 KiB
HTML
92 lines
3.4 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{% block title %}API Gateway{% endblock %}</title>
|
||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
</head>
|
||
<body>
|
||
<div class="layout">
|
||
<nav class="sidebar">
|
||
<div class="brand">API <span>Gateway</span></div>
|
||
<a class="nav-item {% if active == 'dashboard' %}active{% endif %}" href="/admin">Dashboard</a>
|
||
<a class="nav-item {% if active == 'services' %}active{% endif %}" href="/admin/services">Services</a>
|
||
<a class="nav-item {% if active == 'users' %}active{% endif %}" href="/admin/users">Users</a>
|
||
<a class="nav-item {% if active == 'keys' %}active{% endif %}" href="/admin/keys">API Keys</a>
|
||
<a class="nav-item {% if active == 'requests' %}active{% endif %}" href="/admin/requests">Requests</a>
|
||
<a class="nav-item {% if active == 'monitoring' %}active{% endif %}" href="/admin/monitoring">Monitoring</a>
|
||
<div class="spacer"></div>
|
||
{% if user %}
|
||
<div class="whoami">{{ user.username }}</div>
|
||
<a class="nav-item" href="/admin/logout">Log out</a>
|
||
{% endif %}
|
||
</nav>
|
||
<main class="main">
|
||
{% block content %}{% endblock %}
|
||
</main>
|
||
</div>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
if (window.Chart) {
|
||
Chart.defaults.color = '#898781';
|
||
Chart.defaults.borderColor = '#2c2c2a';
|
||
Chart.defaults.font.family = 'system-ui, -apple-system, "Segoe UI", sans-serif';
|
||
Chart.defaults.plugins.legend.labels.boxWidth = 12;
|
||
Chart.defaults.plugins.legend.labels.boxHeight = 12;
|
||
Chart.defaults.animation.duration = 300;
|
||
}
|
||
const GW = {
|
||
blue: '#3987e5',
|
||
red: '#e66767',
|
||
ink2: '#c3c2b7',
|
||
grid: '#2c2c2a',
|
||
async fetchJSON(url) { const r = await fetch(url); return r.json(); },
|
||
};
|
||
|
||
// Destructive forms carry data-confirm. First click arms the button
|
||
// ("Confirm?"), a second click within 5s submits. No native dialogs —
|
||
// confirm() is unreliable in embedded browsers.
|
||
document.addEventListener('submit', e => {
|
||
const form = e.target;
|
||
if (!form.dataset.confirm) return;
|
||
if (form.dataset.armed === '1') return; // second click: let it through
|
||
e.preventDefault();
|
||
const btn = form.querySelector('button');
|
||
form.dataset.armed = '1';
|
||
if (!btn.dataset.orig) btn.dataset.orig = btn.innerHTML;
|
||
btn.innerHTML = 'Confirm?';
|
||
btn.classList.add('armed');
|
||
btn.title = form.dataset.confirm;
|
||
setTimeout(() => {
|
||
form.dataset.armed = '';
|
||
btn.innerHTML = btn.dataset.orig;
|
||
btn.classList.remove('armed');
|
||
}, 5000);
|
||
}, true);
|
||
|
||
// Modal overlays: [data-modal] opens the overlay with that id; the × button,
|
||
// a click on the backdrop, or Escape closes it.
|
||
document.addEventListener('click', e => {
|
||
const opener = e.target.closest('[data-modal]');
|
||
if (opener) {
|
||
e.preventDefault();
|
||
document.getElementById(opener.dataset.modal).classList.add('open');
|
||
return;
|
||
}
|
||
if (e.target.closest('.modal-close')) {
|
||
e.target.closest('.modal-overlay').classList.remove('open');
|
||
return;
|
||
}
|
||
if (e.target.classList.contains('modal-overlay')) e.target.classList.remove('open');
|
||
});
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape')
|
||
document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open'));
|
||
});
|
||
</script>
|
||
{% block scripts %}{% endblock %}
|
||
</body>
|
||
</html>
|