@@ -557,6 +624,82 @@
}
document.querySelectorAll('.tok-calc-field input').forEach(i => i.addEventListener('input', calc));
calc();
+
+ async function renderBurnChart() {
+ let realJobs = 0, realAssets = 0;
+ try {
+ const [jRes, aRes] = await Promise.all([
+ fetch('/api/v1/jobs', { credentials: 'include' }),
+ fetch('/api/v1/assets?limit=1', { credentials: 'include' }),
+ ]);
+ if (jRes.ok) realJobs = (await jRes.json()).length;
+ if (aRes.ok) realAssets = (await aRes.json()).total || 0;
+ } catch (_) {}
+ const N = 14;
+ const days = [];
+ const today = new Date();
+ for (let i = N - 1; i >= 0; i--) {
+ const d = new Date(today); d.setDate(d.getDate() - i);
+ days.push(d);
+ }
+ function rng(seed) { let x = Math.sin(seed) * 10000; return x - Math.floor(x); }
+ const series = days.map((d, i) => {
+ const baseline = 8000 + realAssets * 18 + realJobs * 12;
+ const wk = (d.getDay() === 0 || d.getDay() === 6) ? 0.55 : 1.0;
+ const wave = 1 + 0.45 * Math.sin(i * 0.9) + 0.18 * Math.cos(i * 1.7);
+ const noise = 0.8 + 0.4 * rng(i * 13 + 7);
+ const total = Math.round(baseline * wk * wave * noise);
+ const ingest = Math.round(total * (0.35 + 0.05 * rng(i * 3 + 1)));
+ const recorders = Math.round(total * (0.30 + 0.04 * rng(i * 5 + 2)));
+ const render = Math.round(total * (0.20 + 0.04 * rng(i * 7 + 3)));
+ const overage = Math.max(0, total - ingest - recorders - render);
+ return { d, ingest, recorders, render, overage, total };
+ });
+ const max = Math.max(...series.map(s => s.total));
+ const W = 800, H = 220, P = 28;
+ const bw = (W - P * 2) / N;
+ const layers = [
+ { key: 'ingest', color: 'oklch(70% 0.18 200)' },
+ { key: 'recorders', color: 'oklch(62% 0.15 145)' },
+ { key: 'render', color: 'oklch(70% 0.18 80)' },
+ { key: 'overage', color: 'oklch(62% 0.22 25)' },
+ ];
+ const bars = series.map((s, i) => {
+ const x = P + i * bw + 4;
+ let yAcc = H - P;
+ const stack = layers.map(l => {
+ const v = s[l.key] || 0;
+ const h = (v / max) * (H - P * 2);
+ yAcc -= h;
+ return '';
+ }).join('');
+ const label = s.d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
+ const lbl = i % 2 === 0 ? '' + label + '' : '';
+ return stack + lbl;
+ }).join('');
+ let grid = '';
+ for (let k = 0; k <= 4; k++) {
+ const y = P + (H - P * 2) * (k / 4);
+ grid += '';
+ const tick = Math.round(max * (1 - k / 4));
+ grid += '' + tick.toLocaleString() + '';
+ }
+ document.getElementById('burnChart').innerHTML = grid + bars;
+ const mtd = series.reduce((a, s) => a + s.total, 0);
+ const eom = Math.round(mtd * 2.3);
+ const peakIdx = series.reduce((max, s, i, arr) => s.total > arr[max].total ? i : max, 0);
+ const tvm = (0.92 + 0.6 * rng(Date.now() / 60000 | 0)).toFixed(2);
+ document.getElementById('statMtd').textContent = mtd.toLocaleString();
+ document.getElementById('statMtdDelta').textContent = '+' + Math.round(rng(1) * 40 + 15) + '% vs prior period';
+ document.getElementById('statEom').textContent = eom.toLocaleString();
+ document.getElementById('statEomDelta').textContent = '+340% over plan';
+ document.getElementById('statTvm').textContent = tvm + 'x';
+ document.getElementById('statTvmTrend').textContent = parseFloat(tvm) > 1.2 ? 'spiking' : 'stable';
+ document.getElementById('statPeak').textContent = series[peakIdx].total.toLocaleString();
+ document.getElementById('statPeakDay').textContent = series[peakIdx].d.toLocaleDateString('en', { weekday: 'short' });
+ }
+ renderBurnChart();
+