435e1bda42
Deploy to Production / deploy (push) Successful in 3m16s
With 1 s polls and batch traffic, delta * 60 flickers between 0 and 300. Fix: live label shows accDelta / elapsed_in_window * 60000 — the rate builds up continuously within each 5 s bucket. First 200 ms after flush carries the previous window rate to avoid a zero jump. Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
157 lines
6.8 KiB
HTML
157 lines
6.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{dashboard.name} · APIX Admin</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { background: #0d1117; color: #c9d1d9; font-family: 'SF Mono','Consolas','Fira Code',monospace; font-size: 14px; }
|
|
a { color: #58a6ff; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
nav {
|
|
display: flex; align-items: center; gap: 1.5rem;
|
|
padding: 0.75rem 1.5rem; border-bottom: 1px solid #21262d; background: #161b22;
|
|
}
|
|
.nav-logo { color: #8b949e; font-size: 0.75rem; }
|
|
.nav-logo span { color: #e6edf3; font-weight: 600; font-size: 0.9rem; }
|
|
.nav-link { font-size: 0.8rem; color: #8b949e; }
|
|
.nav-link:hover { color: #e6edf3; }
|
|
.nav-right { margin-left: auto; }
|
|
|
|
.header { padding: 1rem 1.5rem; border-bottom: 1px solid #21262d; display: flex; align-items: center; gap: 1rem; }
|
|
.sb-name { font-size: 1rem; color: #e6edf3; font-weight: 600; }
|
|
.tier-badge {
|
|
background: #161b22; border: 1px solid #30363d;
|
|
color: #8b949e; padding: 0.1rem 0.5rem; border-radius: 12px;
|
|
font-size: 0.7rem; letter-spacing: 0.05em;
|
|
}
|
|
.sb-id { font-size: 0.75rem; color: #484f58; }
|
|
|
|
.stats-grid {
|
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr));
|
|
gap: 1px; background: #21262d; border-bottom: 1px solid #21262d;
|
|
}
|
|
.stat { background: #0d1117; padding: 1rem 1.5rem; }
|
|
.stat-label { font-size: 0.65rem; color: #484f58; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.3rem; }
|
|
.stat-value { font-size: 1.4rem; color: #e6edf3; font-weight: 600; }
|
|
.stat-sub { font-size: 0.7rem; color: #484f58; margin-top: 0.2rem; }
|
|
|
|
#map-wrap { background: #060d18; }
|
|
#world-map { display: block; width: 100%; }
|
|
|
|
.actions { padding: 1.5rem; border-top: 1px solid #21262d; display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
.actions-title { width: 100%; font-size: 0.7rem; color: #484f58; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.5rem; }
|
|
form { display: flex; gap: 0.5rem; align-items: center; }
|
|
select, button {
|
|
background: #161b22; border: 1px solid #30363d; color: #c9d1d9;
|
|
font-family: inherit; font-size: 0.8rem;
|
|
border-radius: 4px; padding: 0.4rem 0.7rem; cursor: pointer;
|
|
}
|
|
select:focus, button:focus { outline: none; border-color: #58a6ff; }
|
|
button.promote { background: #238636; border-color: #238636; color: #fff; font-weight: 600; }
|
|
button.promote:hover { background: #2ea043; }
|
|
|
|
.footer { padding: 0.75rem 1.5rem; border-top: 1px solid #21262d; font-size: 0.7rem; color: #484f58; }
|
|
|
|
@keyframes star-glow {
|
|
0%,100% { filter: drop-shadow(0 0 3px #ffd700) drop-shadow(0 0 6px #ffd70066); }
|
|
50% { filter: drop-shadow(0 0 8px #ffd700) drop-shadow(0 0 18px #ffd700aa); }
|
|
}
|
|
.registrar-star { font-size: 14px; fill: #ffd700; dominant-baseline: central; text-anchor: middle; animation: star-glow 2.4s ease-in-out infinite; }
|
|
@keyframes blink-pulse {
|
|
0% { opacity:0; r:2; } 25% { opacity:0.9; r:5; } 65% { opacity:0.4; r:4; } 100% { opacity:0; r:2; }
|
|
}
|
|
.agent-blink { fill: #3d8bfd; opacity:0; animation: blink-pulse 2.8s ease-in-out infinite; }
|
|
|
|
#rate-wrap { background: #060d18; padding: 0 1.5rem 0.5rem; border-bottom: 1px solid #21262d; }
|
|
.rate-header { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.6rem 0 0.4rem; }
|
|
.rate-title { font-size: 0.65rem; color: #484f58; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
.rate-live { font-size: 0.75rem; color: #388bfd; }
|
|
.rate-limit-label { font-size: 0.65rem; color: #f8514966; margin-left: auto; }
|
|
#rate-chart { display: block; width: 100%; overflow: visible; }
|
|
.rate-area { fill: #1f6feb33; stroke: #388bfd; stroke-width: 1.5; }
|
|
.rate-limit-line { stroke: #f85149; stroke-width: 0.8; stroke-dasharray: 4 3; opacity: 0.5; }
|
|
.axis-label { font-size: 9px; fill: #484f58; font-family: 'SF Mono','Consolas','Fira Code',monospace; }
|
|
.axis-line { stroke: #21262d; stroke-width: 0.5; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<div class="nav-logo">APIX · <span>Admin</span></div>
|
|
<a class="nav-link" href="/">Dashboard</a>
|
|
<a class="nav-link" href="/sandboxes">Sandboxes</a>
|
|
<div class="nav-right"><a class="nav-link" href="/login/logout">Sign out</a></div>
|
|
</nav>
|
|
|
|
<div class="header">
|
|
<span class="sb-name">{dashboard.name}</span>
|
|
<span class="tier-badge">{dashboard.tier}</span>
|
|
<span class="sb-id">{dashboard.sandboxId}</span>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat">
|
|
<div class="stat-label">Services</div>
|
|
<div class="stat-value">{#if dashboard.usage.get('SERVICE_REGISTERED') != null}{dashboard.usage.get('SERVICE_REGISTERED')}{#else}0{/if}</div>
|
|
<div class="stat-sub">of {#if dashboard.maxServices != null}{dashboard.maxServices}{#else}∞{/if} allowed</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">Searches</div>
|
|
<div class="stat-value">{#if dashboard.usage.get('SERVICE_SEARCHED') != null}{dashboard.usage.get('SERVICE_SEARCHED')}{#else}0{/if}</div>
|
|
<div class="stat-sub">capability discovery calls</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">Rate limit</div>
|
|
<div class="stat-value">{dashboard.ratePerMinute}</div>
|
|
<div class="stat-sub">now: <span id="rate-now">—</span> req/min</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">Expires</div>
|
|
<div class="stat-value" id="expires-val">—</div>
|
|
<div class="stat-sub" id="expires-raw">{dashboard.expiresAt}</div>
|
|
</div>
|
|
{#if dashboard.registrarLocation != null}
|
|
<div class="stat">
|
|
<div class="stat-label">Location</div>
|
|
<div class="stat-value" style="font-size:1rem">{dashboard.registrarLocation}</div>
|
|
<div class="stat-sub">owner-declared</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div id="rate-wrap">
|
|
<div class="rate-header">
|
|
<span class="rate-title">Live rate</span>
|
|
<span class="rate-live" id="rate-live-val">— req/min</span>
|
|
<span class="rate-limit-label">limit: {dashboard.ratePerMinute}/min ──</span>
|
|
</div>
|
|
<svg id="rate-chart" height="96"></svg>
|
|
</div>
|
|
|
|
<div id="map-wrap"><svg id="world-map"></svg></div>
|
|
|
|
<div class="actions">
|
|
<div class="actions-title">Admin actions</div>
|
|
<form method="POST" action="/sandboxes/{dashboard.sandboxId}/promote">
|
|
<select name="tier">
|
|
<option value="FREE">FREE</option>
|
|
<option value="STANDARD">STANDARD</option>
|
|
<option value="PROFESSIONAL">PROFESSIONAL</option>
|
|
<option value="COMMUNITY">COMMUNITY</option>
|
|
<option value="FOUNDER">FOUNDER</option>
|
|
<option value="DEMO">DEMO</option>
|
|
</select>
|
|
<button type="submit" class="promote">Promote tier</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="footer">sandbox id: {dashboard.sandboxId} · created {dashboard.createdAt}</div>
|
|
|
|
<script>var __D = {dataJson.raw};</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
|
|
<script src="/admin.js?v=8"></script>
|
|
</body>
|
|
</html>
|