document.addEventListener('DOMContentLoaded', function () { // ── Period datasets ─────────────────────────────────────────────────────── var CHART_DATA = { '7d': [ {d:'Mon', s:3838, e:18}, {d:'Tue', s:4242, e:32}, {d:'Wed', s:3988, e:28}, {d:'Thu', s:4147, e:45}, {d:'Fri', s:3810, e:22}, {d:'Sat', s:2915, e:15}, {d:'Sun', s:1569, e:8} ], '30d': [ {d:'Wk 1', s:24500, e:120}, {d:'Wk 2', s:26800, e:145}, {d:'Wk 3', s:25100, e:98}, {d:'Wk 4', s:27400, e:178} ], '90d': [ {d:'Jan', s:92000, e:420}, {d:'Feb', s:108000, e:510}, {d:'Mar', s:98400, e:458} ], 'this_month': [ {d:'Wk 1', s:24500, e:120}, {d:'Wk 2', s:26800, e:145}, {d:'Wk 3', s:25100, e:98}, {d:'Wk 4', s:27400, e:178} ] }; // Base 7-day operation data; other periods scale from this. var OPS_7D = [ { name: 'create_crm_lead', display: 'Create CRM Lead', proto: 'REST', calls: 9840, errors: 42, p50: 138, p95: 390, p99: 820 }, { name: 'search_products', display: 'Search Products', proto: 'REST', calls: 7123, errors: 8, p50: 91, p95: 210, p99: 440 }, { name: 'fetch_invoice', display: 'Fetch Invoice', proto: 'REST', calls: 5210, errors: 214, p50: 104, p95: 540, p99: 8200 }, { name: 'update_contact', display: 'Update Contact', proto: 'REST', calls: 3882, errors: 28, p50: 192, p95: 460, p99: 910 }, { name: 'send_email', display: 'Send Email', proto: 'REST', calls: 1440, errors: 96, p50: 310, p95: 2100, p99: 30100 }, { name: 'list_orders', display: 'List Orders (GraphQL)', proto: 'GraphQL', calls: 670, errors: 2, p50: 66, p95: 180, p99: 320 }, { name: 'delete_record', display: 'Delete Record', proto: 'REST', calls: 176, errors: 6, p50: 285, p95: 690, p99: 1400 } ]; function scaleOps(factor) { return OPS_7D.map(function (op) { return { name: op.name, display: op.display, proto: op.proto, calls: Math.round(op.calls * factor), errors: Math.round(op.errors * factor), p50: op.p50, p95: op.p95, p99: op.p99 }; }); } var OPS_DATA = { '7d': OPS_7D, '30d': scaleOps(4.2), '90d': scaleOps(12), 'this_month': scaleOps(4.2) }; // ── Stat card data ──────────────────────────────────────────────────────── var STATS = { '7d': { invocations: '28,341', invDelta: { dir: 'up', text: '+12% vs prior period' }, success: '98.6%', sucDelta: { dir: 'up', text: '+0.3pp vs prior period' }, p50: '124ms', p50Delta: { dir: 'up', text: '-18ms vs prior period' }, p99: '2.1s', p99Delta: { dir: 'down', text: '+340ms vs prior period' } }, '30d': { invocations: '118,623', invDelta: { dir: 'up', text: '+8% vs prior period' }, success: '98.4%', sucDelta: { dir: 'down', text: '-0.2pp vs prior period' }, p50: '128ms', p50Delta: { dir: 'down', text: '+4ms vs prior period' }, p99: '2.3s', p99Delta: { dir: 'down', text: '+200ms vs prior period' } }, '90d': { invocations: '342,804', invDelta: { dir: 'up', text: '+15% vs prior period' }, success: '98.7%', sucDelta: { dir: 'up', text: '+0.1pp vs prior period' }, p50: '121ms', p50Delta: { dir: 'up', text: '-7ms vs prior period' }, p99: '2.0s', p99Delta: { dir: 'up', text: '-100ms vs prior period' } }, 'this_month': { invocations: '118,623', invDelta: { dir: 'up', text: '+8% vs prior period' }, success: '98.4%', sucDelta: { dir: 'down', text: '-0.2pp vs prior period' }, p50: '128ms', p50Delta: { dir: 'down', text: '+4ms vs prior period' }, p99: '2.3s', p99Delta: { dir: 'down', text: '+200ms vs prior period' } } }; // ── Helpers ─────────────────────────────────────────────────────────────── var COLORS = { REST: 'var(--blue)', GraphQL: 'var(--purple)', gRPC: 'var(--accent)' }; var QUOTA = 50000; function fmtMs(ms) { return ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : ms + 'ms'; } function errRateClass(rate) { if (rate > 5) return 'color:var(--red)'; if (rate > 1) return 'color:var(--amber)'; return 'color:var(--green)'; } var ARROW_UP = ''; var ARROW_DOWN = ''; // ── Render functions ────────────────────────────────────────────────────── function renderChart(period) { var data = CHART_DATA[period]; var maxVal = Math.max.apply(null, data.map(function (d) { return d.s + d.e; })); var wrap = document.getElementById('chart-bars'); var tmpl = document.getElementById('tmpl-chart-bar'); wrap.innerHTML = ''; data.forEach(function (d) { var hS = Math.round((d.s / maxVal) * 160); var hE = Math.round((d.e / maxVal) * 160); var node = tmpl.content.cloneNode(true); node.querySelector('.chart-bar.error').style.height = hE + 'px'; node.querySelector('.chart-bar.error').dataset.tip = d.e + ' errors'; node.querySelector('.chart-bar.success').style.height = hS + 'px'; node.querySelector('.chart-bar.success').dataset.tip = d.s.toLocaleString() + ' ok'; node.querySelector('.chart-col-label').textContent = d.d; wrap.appendChild(node); }); } function renderTable(period) { var ops = OPS_DATA[period]; var tbody = document.getElementById('usage-tbody'); var tmpl = document.getElementById('tmpl-usage-row'); tbody.innerHTML = ''; ops.forEach(function (op) { var errRate = (op.errors / op.calls * 100).toFixed(2); var quotaW = Math.min(op.calls / QUOTA * 100, 100).toFixed(1); var node = tmpl.content.cloneNode(true); node.querySelector('.col-name').textContent = op.display; node.querySelector('.col-op-slug').textContent = op.name; node.querySelector('.col-proto').textContent = op.proto; node.querySelector('.col-calls').textContent = op.calls.toLocaleString(); node.querySelector('.col-errors').textContent = op.errors.toLocaleString(); var errEl = node.querySelector('.col-err-rate'); errEl.innerHTML = '' + errRate + '%'; var p50w = (op.p50 / op.p99 * 64).toFixed(1); var p95w = ((op.p95 - op.p50) / op.p99 * 64).toFixed(1); var p99w = ((op.p99 - op.p95) / op.p99 * 64).toFixed(1); node.querySelector('.latency-p50').style.width = p50w + 'px'; node.querySelector('.latency-p50').title = 'p50: ' + fmtMs(op.p50); node.querySelector('.latency-p95').style.width = p95w + 'px'; node.querySelector('.latency-p95').title = 'p95: ' + fmtMs(op.p95); node.querySelector('.latency-p99').style.width = p99w + 'px'; node.querySelector('.latency-p99').title = 'p99: ' + fmtMs(op.p99); node.querySelector('.col-latency-text').textContent = fmtMs(op.p50) + ' / ' + fmtMs(op.p95) + ' / ' + fmtMs(op.p99); node.querySelector('.col-quota-label').textContent = op.calls.toLocaleString() + ' / ' + QUOTA.toLocaleString(); node.querySelector('.col-quota-fill').style.width = quotaW + '%'; tbody.appendChild(node); }); } function renderStats(period) { var s = STATS[period]; var cards = document.querySelectorAll('.stats-grid .stat-card'); var data = [ { value: s.invocations, delta: s.invDelta }, { value: s.success, delta: s.sucDelta }, { value: s.p50, delta: s.p50Delta }, { value: s.p99, delta: s.p99Delta } ]; for (var i = 0; i < cards.length; i++) { var card = cards[i]; var d = data[i]; card.querySelector('.stat-value').textContent = d.value; var deltaEl = card.querySelector('.stat-delta'); deltaEl.className = 'stat-delta ' + d.delta.dir; var arrow = d.delta.dir === 'up' ? ARROW_UP : ARROW_DOWN; deltaEl.innerHTML = arrow + ' ' + d.delta.text; } } function updateSubtitle(period) { var labels = { '7d': 'last 7 days', '30d': 'last 30 days', '90d': 'last 90 days', 'this_month': 'this month' }; var el = document.querySelector('.section-card-subtitle'); if (el) el.textContent = 'Breakdown for ' + (labels[period] || period); } // ── CSV export ──────────────────────────────────────────────────────────── function exportCSV(period) { var ops = OPS_DATA[period]; var rows = ['Operation,Protocol,Calls,Errors,Error Rate (%),p50 (ms),p95 (ms),p99 (ms)']; ops.forEach(function (op) { var errRate = (op.errors / op.calls * 100).toFixed(2); rows.push([ '"' + op.display + '"', op.proto, op.calls, op.errors, errRate, op.p50, op.p95, op.p99 ].join(',')); }); var csv = rows.join('\r\n'); var blob = new Blob([csv], {type: 'text/csv'}); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'crank-usage-' + period + '.csv'; a.click(); URL.revokeObjectURL(url); } // ── Period selector ─────────────────────────────────────────────────────── var currentPeriod = '7d'; var periodSelect = document.getElementById('period'); if (periodSelect) { periodSelect.value = currentPeriod; periodSelect.addEventListener('change', function () { currentPeriod = this.value; renderChart(currentPeriod); renderTable(currentPeriod); renderStats(currentPeriod); updateSubtitle(currentPeriod); }); } // Export CSV button — first .btn-secondary inside .page-header-actions var exportBtn = document.querySelector('.page-header-actions .btn-secondary'); if (exportBtn) { exportBtn.addEventListener('click', function () { exportCSV(currentPeriod); }); } // ── Initial render ──────────────────────────────────────────────────────── renderChart(currentPeriod); renderTable(currentPeriod); renderStats(currentPeriod); updateSubtitle(currentPeriod); });