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 { - + diff --git a/apix-admin/src/main/resources/templates/DashboardResource/sandbox.html b/apix-admin/src/main/resources/templates/DashboardResource/sandbox.html index d423b0f..3245ae5 100644 --- a/apix-admin/src/main/resources/templates/DashboardResource/sandbox.html +++ b/apix-admin/src/main/resources/templates/DashboardResource/sandbox.html @@ -151,6 +151,6 @@ button.promote:hover { background: #2ea043; } - +