fix(admin): move rate chart out of Qute template into admin.js
Deploy to Production / deploy (push) Successful in 3m26s

Qute processes all {...} in .html files including <script> blocks.
JS function bodies like function(d) { return y(d); } were silently
eaten, causing a syntax error that killed the entire chart script.

Moving the rate chart IIFE into admin.js (static, Qute-never-touches)
fixes the root cause. Axes (Y: 0/50/100%, X: -6m/-3m/now) and
live % label now render correctly.

Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
Carsten Rehfeld
2026-05-17 00:29:16 +02:00
parent 00e4731bde
commit 6bc4059922
3 changed files with 113 additions and 113 deletions
@@ -128,3 +128,114 @@
});
}
})();
// ── Sandbox live rate chart (sandbox drill-down only) ──────────────────────
(function () {
const 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;
const buf = new Array(BUCKETS).fill(0);
let lastTotal = null;
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)
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]);
const 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);
// 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 + '%');
});
// X-axis baseline + time labels
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);
});
// 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');
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);
}
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 + '%)';
var elNow = document.getElementById('rate-now');
var elLive = document.getElementById('rate-live-val');
if (elNow) elNow.textContent = fmt(rpm);
if (elLive) elLive.textContent = txt;
}
async function poll() {
try {
var r = await fetch('/sandbox/' + sid + '/poll');
if (!r.ok) return;
var d = await r.json();
var total = Number(d.totalRequests) || 0;
if (lastTotal !== null) {
var delta = Math.max(0, total - lastTotal);
render(Math.round(delta * (60000 / INTERVAL)));
}
lastTotal = total;
} catch (_) {}
}
poll();
setInterval(poll, INTERVAL);
})();
@@ -106,6 +106,6 @@ nav {
<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/topojson-client@3/dist/topojson-client.min.js"></script>
<script src="/admin.js?v=3"></script>
<script src="/admin.js?v=5"></script>
</body>
</html>
@@ -151,117 +151,6 @@ button.promote:hover { background: #2ea043; }
<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=4"></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;
let tickCount = 0;
const el = document.getElementById('rate-chart');
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 CW = W - ML; // chart width
const CH = H - MB; // chart height
el.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
el.style.width = '100%';
// Y scale: 0110% of rate limit, mapped to pixel height
const yS = d3.scaleLinear().domain([0, rateLimit * 1.1]).range([CH, 0]);
// X scale: bucket index to pixel x (within chart area)
const xS = d3.scaleLinear().domain([0, BUCKETS - 1]).range([ML, W]);
const 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);
// Y-axis grid lines + % labels (0%, 50%, 100%)
[0, 50, 100].forEach(function(pct) {
const rpm = rateLimit * pct / 100;
const y = yS(rpm);
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
svg.append('line')
.attr('x1', ML).attr('x2', W)
.attr('y1', CH).attr('y2', CH)
.attr('class', 'axis-line');
// X-axis time labels: -6m, -3m, now
[
{ bucket: 0, label: '-6m' },
{ bucket: BUCKETS / 2, label: '-3m' },
{ bucket: BUCKETS - 1, label: 'now' }
].forEach(function(t) {
svg.append('text')
.attr('x', xS(t.bucket))
.attr('y', H - 4)
.attr('text-anchor', t.bucket === 0 ? 'start' : t.bucket === BUCKETS - 1 ? 'end' : 'middle')
.attr('class', 'axis-label')
.text(t.label);
});
// Red dashed limit line (100%)
const ly = yS(rateLimit);
svg.append('line')
.attr('x1', ML).attr('x2', W)
.attr('y1', ly).attr('y2', ly)
.attr('class', 'rate-limit-line');
const 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);
}
function render(rpm) {
buf.push(rpm);
buf.shift();
path.attr('d', areaFn(buf));
const pct = rateLimit > 0 ? (rpm / rateLimit * 100).toFixed(1) : '0.0';
const txt = fmt(rpm) + ' req/min (' + pct + '%)';
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;
tickCount++;
} catch (_) {}
}
poll();
setInterval(poll, INTERVAL);
})();
</script>
<script src="/admin.js?v=5"></script>
</body>
</html>