fix(admin): blinks + live rate chart on sandbox drill-down
Deploy to Production / deploy (push) Successful in 3m14s
Deploy to Production / deploy (push) Successful in 3m14s
Blink bug: agent visits were gated behind registrarLat != null — sandboxes
without a declared location showed no blinks at all. Now always enters the
sandbox branch when __D.sandboxId is present; registrar star still conditional.
Rate chart: rolling D3 area chart (72 buckets x 5s = 6min window) polls
GET /sandbox/{uuid}/poll every 5s, computes delta→rpm, renders with
CatmullRom curve. Red dashed limit line. Live value shown in stat box and
chart header. Poll endpoint added to DashboardResource (JSON, no HTML).
Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import io.quarkus.qute.TemplateInstance;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import java.util.Map;
|
||||
import org.botstandards.apix.common.AdminStatsResponse;
|
||||
import org.botstandards.apix.common.SandboxDashboardResponse;
|
||||
import org.botstandards.apix.admin.client.AdminRegistryClient;
|
||||
@@ -47,6 +48,24 @@ public class DashboardResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/sandbox/{uuid}/poll")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response sandboxPoll(@PathParam("uuid") String uuid) {
|
||||
try {
|
||||
SandboxDashboardResponse d = registry.sandboxDetail(adminKey, uuid);
|
||||
long total = d.usage() == null ? 0L :
|
||||
d.usage().values().stream().mapToLong(Long::longValue).sum();
|
||||
return Response.ok(Map.of(
|
||||
"totalRequests", total,
|
||||
"ratePerMinute", d.ratePerMinute()
|
||||
)).build();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Poll failed for sandbox %s", uuid);
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/sandbox/{uuid}")
|
||||
public TemplateInstance sandboxDashboard(@PathParam("uuid") String uuid) {
|
||||
|
||||
@@ -62,10 +62,12 @@
|
||||
points = __D.registrarPoints.map(p => ({
|
||||
lat: p.lat, lon: p.lon, label: p.name, tier: p.tier, type: 'registrar'
|
||||
}));
|
||||
} else if (__D.registrarLat != null) {
|
||||
// sandbox drill-down — registrar star
|
||||
} else if (__D.sandboxId) {
|
||||
// sandbox drill-down
|
||||
if (__D.registrarLat != null) {
|
||||
points.push({ lat: __D.registrarLat, lon: __D.registrarLon, label: __D.name, tier: __D.tier, type: 'registrar' });
|
||||
// agent visits
|
||||
}
|
||||
// agent blinks — always, even when sandbox has no declared location
|
||||
(__D.recentVisits || []).forEach(v => {
|
||||
points.push({ lat: v.lat, lon: v.lon, type: 'agent' });
|
||||
});
|
||||
|
||||
@@ -63,6 +63,15 @@ button.promote:hover { background: #2ea043; }
|
||||
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 1rem; 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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -93,7 +102,7 @@ button.promote:hover { background: #2ea043; }
|
||||
<div class="stat">
|
||||
<div class="stat-label">Rate limit</div>
|
||||
<div class="stat-value">{dashboard.ratePerMinute}</div>
|
||||
<div class="stat-sub">req / min</div>
|
||||
<div class="stat-sub">now: <span id="rate-now">—</span> req/min</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Expires</div>
|
||||
@@ -109,6 +118,15 @@ button.promote:hover { background: #2ea043; }
|
||||
{/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="64"></svg>
|
||||
</div>
|
||||
|
||||
<div id="map-wrap"><svg id="world-map"></svg></div>
|
||||
|
||||
<div class="actions">
|
||||
@@ -132,5 +150,71 @@ button.promote:hover { background: #2ea043; }
|
||||
<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"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const INTERVAL = 5000;
|
||||
const BUCKETS = 72; // 6 min at 5s
|
||||
const rateLimit = __D.ratePerMinute || 1;
|
||||
const sid = __D.sandboxId;
|
||||
|
||||
const buf = new Array(BUCKETS).fill(0);
|
||||
let lastTotal = null;
|
||||
|
||||
const el = document.getElementById('rate-chart');
|
||||
const W = el.parentElement.clientWidth || 900;
|
||||
const H = 64;
|
||||
el.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
||||
el.style.width = '100%';
|
||||
|
||||
const xS = d3.scaleLinear().domain([0, BUCKETS - 1]).range([0, W]);
|
||||
const yS = d3.scaleLinear().domain([0, Math.max(rateLimit * 1.1, 1)]).range([H, 0]);
|
||||
|
||||
const areaFn = d3.area()
|
||||
.x(function(_, i) { return xS(i); })
|
||||
.y0(H)
|
||||
.y1(function(d) { return yS(d); })
|
||||
.curve(d3.curveCatmullRom.alpha(0.5));
|
||||
|
||||
const svg = d3.select(el);
|
||||
const path = svg.append('path').attr('class', 'rate-area');
|
||||
|
||||
// Red dashed limit line
|
||||
const ly = yS(rateLimit);
|
||||
svg.append('line')
|
||||
.attr('x1', 0).attr('x2', W)
|
||||
.attr('y1', ly).attr('y2', ly)
|
||||
.attr('class', 'rate-limit-line');
|
||||
|
||||
function fmt(n) {
|
||||
return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'k' : String(n);
|
||||
}
|
||||
|
||||
function render(rpm) {
|
||||
buf.push(rpm);
|
||||
buf.shift();
|
||||
path.attr('d', areaFn(buf));
|
||||
const txt = fmt(rpm) + ' req/min';
|
||||
document.getElementById('rate-now').textContent = fmt(rpm);
|
||||
document.getElementById('rate-live-val').textContent = txt;
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch('/sandbox/' + sid + '/poll');
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
const total = Number(d.totalRequests) || 0;
|
||||
if (lastTotal !== null) {
|
||||
const delta = Math.max(0, total - lastTotal);
|
||||
render(Math.round(delta * (60000 / INTERVAL)));
|
||||
}
|
||||
lastTotal = total;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user