feat(admin): adaptive poll rate — 1 s active, 5 s idle after 15 s no traffic
Deploy to Production / deploy (push) Successful in 3m16s
Deploy to Production / deploy (push) Successful in 3m16s
Admin sandbox drill-down polls at 1 s when traffic is flowing (live label updates feel instant), drops back to 5 s after 15 s of inactivity. Chart bucket remains fixed at 5 s so x-axis labels stay correct; deltas accumulate between buckets independently of poll rate. Tier-dependent rate reserved for future sandbox-owner dashboard. Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
@@ -131,98 +131,83 @@
|
|||||||
|
|
||||||
// ── Sandbox live rate chart (sandbox drill-down only) ──────────────────────
|
// ── Sandbox live rate chart (sandbox drill-down only) ──────────────────────
|
||||||
(function () {
|
(function () {
|
||||||
const el = document.getElementById('rate-chart');
|
var el = document.getElementById('rate-chart');
|
||||||
if (!el || !window.__D || !__D.sandboxId) return;
|
if (!el || !window.__D || !__D.sandboxId) return;
|
||||||
|
|
||||||
const INTERVAL = 5000;
|
// Admin always polls at 1 s when active; idles back to 5 s after 15 s of no traffic
|
||||||
const BUCKETS = 72; // 6 min at 5s
|
// Tier-dependent rate would live in a future sandbox-owner dashboard, not here
|
||||||
const rateLimit = __D.ratePerMinute || 1;
|
var FAST_INTERVAL = 1000;
|
||||||
const sid = __D.sandboxId;
|
var IDLE_INTERVAL = 5000;
|
||||||
|
var IDLE_AFTER_MS = 15000; // 15 s inactivity → idle
|
||||||
|
var CHART_TICK = 5000; // chart bucket always 5 s so x-axis labels stay correct
|
||||||
|
var BUCKETS = 72; // 6 min at 5 s/bucket
|
||||||
|
|
||||||
const buf = new Array(BUCKETS).fill(0);
|
var rateLimit = __D.ratePerMinute || 1;
|
||||||
let lastTotal = null;
|
var sid = __D.sandboxId;
|
||||||
|
|
||||||
const ML = 36; // left margin for Y-axis labels
|
var buf = new Array(BUCKETS).fill(0);
|
||||||
const MB = 18; // bottom margin for X-axis labels
|
var lastTotal = null;
|
||||||
const W = el.parentElement.clientWidth || 900;
|
var accDelta = 0; // accumulated since last chart flush
|
||||||
const H = 96;
|
var lastActivity = Date.now(); // time of last non-zero delta
|
||||||
const CH = H - MB; // chart height (plot area)
|
var currentInterval = FAST_INTERVAL;
|
||||||
|
|
||||||
|
var ML = 36, MB = 18;
|
||||||
|
var W = el.parentElement.clientWidth || 900;
|
||||||
|
var H = 96, CH = H - MB;
|
||||||
|
|
||||||
el.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
el.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
||||||
el.style.width = '100%';
|
el.style.width = '100%';
|
||||||
|
|
||||||
const yS = d3.scaleLinear().domain([0, rateLimit * 1.1]).range([CH, 0]);
|
var yS = d3.scaleLinear().domain([0, rateLimit * 1.1]).range([CH, 0]);
|
||||||
const xS = d3.scaleLinear().domain([0, BUCKETS - 1]).range([ML, W]);
|
var xS = d3.scaleLinear().domain([0, BUCKETS - 1]).range([ML, W]);
|
||||||
|
|
||||||
const areaFn = d3.area()
|
var areaFn = d3.area()
|
||||||
.x(function (_, i) { return xS(i); })
|
.x(function (_, i) { return xS(i); })
|
||||||
.y0(CH)
|
.y0(CH)
|
||||||
.y1(function (d) { return yS(d); })
|
.y1(function (d) { return yS(d); })
|
||||||
.curve(d3.curveCatmullRom.alpha(0.5));
|
.curve(d3.curveCatmullRom.alpha(0.5));
|
||||||
|
|
||||||
const svg = d3.select(el);
|
var svg = d3.select(el);
|
||||||
|
|
||||||
// Y-axis: 0 / 50 / 100% grid lines + labels
|
|
||||||
[0, 50, 100].forEach(function (pct) {
|
[0, 50, 100].forEach(function (pct) {
|
||||||
const y = yS(rateLimit * pct / 100);
|
var y = yS(rateLimit * pct / 100);
|
||||||
svg.append('line')
|
svg.append('line').attr('x1', ML).attr('x2', W).attr('y1', y).attr('y2', y).attr('class', 'axis-line');
|
||||||
.attr('x1', ML).attr('x2', W)
|
svg.append('text').attr('x', ML - 4).attr('y', y + 3).attr('text-anchor', 'end').attr('class', 'axis-label').text(pct + '%');
|
||||||
.attr('y1', y).attr('y2', y)
|
|
||||||
.attr('class', 'axis-line');
|
|
||||||
svg.append('text')
|
|
||||||
.attr('x', ML - 4).attr('y', y + 3)
|
|
||||||
.attr('text-anchor', 'end')
|
|
||||||
.attr('class', 'axis-label')
|
|
||||||
.text(pct + '%');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// X-axis baseline + time labels
|
svg.append('line').attr('x1', ML).attr('x2', W).attr('y1', CH).attr('y2', CH).attr('class', 'axis-line');
|
||||||
svg.append('line')
|
|
||||||
.attr('x1', ML).attr('x2', W)
|
|
||||||
.attr('y1', CH).attr('y2', CH)
|
|
||||||
.attr('class', 'axis-line');
|
|
||||||
|
|
||||||
[
|
[{bucket: 0, label: '-6m', anchor: 'start'}, {bucket: BUCKETS / 2, label: '-3m', anchor: 'middle'}, {bucket: BUCKETS - 1, label: 'now', anchor: 'end'}]
|
||||||
{ bucket: 0, label: '-6m', anchor: 'start' },
|
.forEach(function (t) {
|
||||||
{ bucket: BUCKETS / 2, label: '-3m', anchor: 'middle' },
|
svg.append('text').attr('x', xS(t.bucket)).attr('y', H - 4).attr('text-anchor', t.anchor).attr('class', 'axis-label').text(t.label);
|
||||||
{ bucket: BUCKETS - 1, label: 'now', anchor: 'end' }
|
});
|
||||||
].forEach(function (t) {
|
|
||||||
svg.append('text')
|
|
||||||
.attr('x', xS(t.bucket))
|
|
||||||
.attr('y', H - 4)
|
|
||||||
.attr('text-anchor', t.anchor)
|
|
||||||
.attr('class', 'axis-label')
|
|
||||||
.text(t.label);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 100% limit line (red dashed)
|
|
||||||
var ly = yS(rateLimit);
|
var ly = yS(rateLimit);
|
||||||
svg.append('line')
|
svg.append('line').attr('x1', ML).attr('x2', W).attr('y1', ly).attr('y2', ly).attr('class', 'rate-limit-line');
|
||||||
.attr('x1', ML).attr('x2', W)
|
|
||||||
.attr('y1', ly).attr('y2', ly)
|
|
||||||
.attr('class', 'rate-limit-line');
|
|
||||||
|
|
||||||
var path = svg.append('path').attr('class', 'rate-area');
|
var path = svg.append('path').attr('class', 'rate-area');
|
||||||
|
|
||||||
function fmt(n) {
|
function fmt(n) {
|
||||||
return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M'
|
return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k' : String(n);
|
||||||
: n >= 1e3 ? (n / 1e3).toFixed(1) + 'k'
|
|
||||||
: String(n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(rpm) {
|
function updateLabel(rpm) {
|
||||||
buf.push(rpm);
|
var pct = (rpm / rateLimit * 100).toFixed(1);
|
||||||
buf.shift();
|
|
||||||
path.attr('d', areaFn(buf));
|
|
||||||
var pct = rateLimit > 0 ? (rpm / rateLimit * 100).toFixed(1) : '0.0';
|
|
||||||
var txt = fmt(rpm) + ' req/min (' + pct + '%)';
|
|
||||||
var elNow = document.getElementById('rate-now');
|
var elNow = document.getElementById('rate-now');
|
||||||
var elLive = document.getElementById('rate-live-val');
|
var elLive = document.getElementById('rate-live-val');
|
||||||
if (elNow) elNow.textContent = fmt(rpm);
|
if (elNow) elNow.textContent = fmt(rpm);
|
||||||
if (elLive) elLive.textContent = txt;
|
if (elLive) elLive.textContent = fmt(rpm) + ' req/min (' + pct + '%)';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function poll() {
|
// Chart bucket: flush accumulated delta every 5 s regardless of poll rate
|
||||||
|
setInterval(function () {
|
||||||
|
var rpm = Math.round(accDelta * (60000 / CHART_TICK));
|
||||||
|
buf.push(rpm); buf.shift();
|
||||||
|
path.attr('d', areaFn(buf));
|
||||||
|
accDelta = 0;
|
||||||
|
}, CHART_TICK);
|
||||||
|
|
||||||
|
async function poll(elapsed) {
|
||||||
try {
|
try {
|
||||||
var r = await fetch('/sandbox/' + sid + '/poll');
|
var r = await fetch('/sandbox/' + sid + '/poll');
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
@@ -230,12 +215,21 @@
|
|||||||
var total = Number(d.totalRequests) || 0;
|
var total = Number(d.totalRequests) || 0;
|
||||||
if (lastTotal !== null) {
|
if (lastTotal !== null) {
|
||||||
var delta = Math.max(0, total - lastTotal);
|
var delta = Math.max(0, total - lastTotal);
|
||||||
render(Math.round(delta * (60000 / INTERVAL)));
|
accDelta += delta;
|
||||||
|
if (delta > 0) lastActivity = Date.now();
|
||||||
|
var rpm = elapsed > 0 ? Math.round(delta * (60000 / elapsed)) : 0;
|
||||||
|
updateLabel(rpm);
|
||||||
}
|
}
|
||||||
lastTotal = total;
|
lastTotal = total;
|
||||||
|
currentInterval = (Date.now() - lastActivity > IDLE_AFTER_MS) ? IDLE_INTERVAL : FAST_INTERVAL;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
poll();
|
function scheduleNext() {
|
||||||
setInterval(poll, INTERVAL);
|
var interval = currentInterval;
|
||||||
|
setTimeout(function () { poll(interval).then(scheduleNext); }, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
poll(0); // set lastTotal immediately, no rpm yet
|
||||||
|
scheduleNext();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -106,6 +106,6 @@ nav {
|
|||||||
<script>var __D = {statsJson.raw};</script>
|
<script>var __D = {statsJson.raw};</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></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="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
|
||||||
<script src="/admin.js?v=5"></script>
|
<script src="/admin.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -151,6 +151,6 @@ button.promote:hover { background: #2ea043; }
|
|||||||
<script>var __D = {dataJson.raw};</script>
|
<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/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="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
|
||||||
<script src="/admin.js?v=5"></script>
|
<script src="/admin.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user