diff --git a/apix-admin/src/main/resources/META-INF/resources/admin.js b/apix-admin/src/main/resources/META-INF/resources/admin.js
index e69d8a9..bc1b514 100644
--- a/apix-admin/src/main/resources/META-INF/resources/admin.js
+++ b/apix-admin/src/main/resources/META-INF/resources/admin.js
@@ -131,98 +131,83 @@
// ── Sandbox live rate chart (sandbox drill-down only) ──────────────────────
(function () {
- const el = document.getElementById('rate-chart');
+ var el = document.getElementById('rate-chart');
if (!el || !window.__D || !__D.sandboxId) return;
- const INTERVAL = 5000;
- const BUCKETS = 72; // 6 min at 5s
- const rateLimit = __D.ratePerMinute || 1;
- const sid = __D.sandboxId;
+ // Admin always polls at 1 s when active; idles back to 5 s after 15 s of no traffic
+ // Tier-dependent rate would live in a future sandbox-owner dashboard, not here
+ var FAST_INTERVAL = 1000;
+ 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);
- let lastTotal = null;
+ var rateLimit = __D.ratePerMinute || 1;
+ var sid = __D.sandboxId;
- const ML = 36; // left margin for Y-axis labels
- const MB = 18; // bottom margin for X-axis labels
- const W = el.parentElement.clientWidth || 900;
- const H = 96;
- const CH = H - MB; // chart height (plot area)
+ var buf = new Array(BUCKETS).fill(0);
+ var lastTotal = null;
+ var accDelta = 0; // accumulated since last chart flush
+ var lastActivity = Date.now(); // time of last non-zero delta
+ 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.style.width = '100%';
- const yS = d3.scaleLinear().domain([0, rateLimit * 1.1]).range([CH, 0]);
- const xS = d3.scaleLinear().domain([0, BUCKETS - 1]).range([ML, W]);
+ var yS = d3.scaleLinear().domain([0, rateLimit * 1.1]).range([CH, 0]);
+ 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); })
.y0(CH)
.y1(function (d) { return yS(d); })
.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) {
- const y = yS(rateLimit * pct / 100);
- svg.append('line')
- .attr('x1', ML).attr('x2', W)
- .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 + '%');
+ var y = yS(rateLimit * pct / 100);
+ svg.append('line').attr('x1', ML).attr('x2', W).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' }
- ].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);
- });
+ [{bucket: 0, label: '-6m', anchor: 'start'}, {bucket: BUCKETS / 2, label: '-3m', anchor: 'middle'}, {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);
- svg.append('line')
- .attr('x1', ML).attr('x2', W)
- .attr('y1', ly).attr('y2', ly)
- .attr('class', 'rate-limit-line');
+ svg.append('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');
function fmt(n) {
- return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M'
- : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k'
- : String(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));
- var pct = rateLimit > 0 ? (rpm / rateLimit * 100).toFixed(1) : '0.0';
- var txt = fmt(rpm) + ' req/min (' + pct + '%)';
+ function updateLabel(rpm) {
+ var pct = (rpm / rateLimit * 100).toFixed(1);
var elNow = document.getElementById('rate-now');
var elLive = document.getElementById('rate-live-val');
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 {
var r = await fetch('/sandbox/' + sid + '/poll');
if (!r.ok) return;
@@ -230,12 +215,21 @@
var total = Number(d.totalRequests) || 0;
if (lastTotal !== null) {
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;
+ currentInterval = (Date.now() - lastActivity > IDLE_AFTER_MS) ? IDLE_INTERVAL : FAST_INTERVAL;
} catch (_) {}
}
- poll();
- setInterval(poll, INTERVAL);
+ function scheduleNext() {
+ var interval = currentInterval;
+ setTimeout(function () { poll(interval).then(scheduleNext); }, interval);
+ }
+
+ poll(0); // set lastTotal immediately, no rpm yet
+ scheduleNext();
})();
diff --git a/apix-admin/src/main/resources/templates/DashboardResource/global.html b/apix-admin/src/main/resources/templates/DashboardResource/global.html
index bf4cfbd..71877df 100644
--- a/apix-admin/src/main/resources/templates/DashboardResource/global.html
+++ b/apix-admin/src/main/resources/templates/DashboardResource/global.html
@@ -106,6 +106,6 @@ nav {
-
+