document.addEventListener('DOMContentLoaded', function () {
var state = {
period: '7d',
workspaceId: null,
usage: null,
loading: false,
loadError: '',
};
var periodSelect = document.getElementById('period');
var exportBtn = document.querySelector('.page-header-actions .btn-secondary');
var chartWrap = document.getElementById('chart-bars');
var tableBody = document.getElementById('usage-tbody');
var chartTemplate = document.getElementById('tmpl-chart-bar');
var rowTemplate = document.getElementById('tmpl-usage-row');
var subtitle = document.querySelector('.section-card-subtitle');
var statCards = document.querySelectorAll('.stats-grid .stat-card');
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function formatCount(value) {
return Number(value || 0).toLocaleString();
}
function formatMs(value) {
if (!value) {
return '0ms';
}
if (value >= 1000) {
return (value / 1000).toFixed(1) + 's';
}
return value + 'ms';
}
function periodLabel(period) {
var labels = {
'7d': 'last 7 days',
'30d': 'last 30 days',
'90d': 'last 90 days',
'this_month': 'this month',
};
return labels[period] || period;
}
function protocolColor(protocol) {
if (protocol === 'graphql' || protocol === 'Graphql') {
return 'var(--purple)';
}
if (protocol === 'grpc' || protocol === 'Grpc') {
return 'var(--accent)';
}
return 'var(--blue)';
}
function protocolLabel(protocol) {
if (protocol === 'graphql' || protocol === 'Graphql') {
return 'GraphQL';
}
if (protocol === 'grpc' || protocol === 'Grpc') {
return 'gRPC';
}
return 'REST';
}
function chartBucketLabel(timestamp, period, index) {
var date = new Date(timestamp);
if (period === '7d') {
return date.toLocaleDateString('en-US', { weekday: 'short' });
}
if (period === '90d') {
return date.toLocaleDateString('en-US', { month: 'short' });
}
return 'Wk ' + (index + 1);
}
function renderEmpty(title, message) {
chartWrap.innerHTML =
'
' +
'
' + title + '
' +
'
' + message + '
' +
'
';
tableBody.innerHTML =
'' +
'' +
' ' + title + ' ' +
' ' + message + ' ' +
' ' +
' |
';
}
function renderStats() {
var summary = state.usage ? state.usage.summary : null;
var cards = [
{
value: formatCount(summary ? summary.rollup.calls_total : 0),
text: 'Across ' + periodLabel(state.period),
},
{
value: ((summary ? summary.success_rate : 0) || 0).toFixed(1) + '%',
text: formatCount(summary ? summary.rollup.calls_ok : 0) + ' successful calls',
},
{
value: formatMs(summary ? summary.rollup.p50_ms : 0),
text: 'Median latency for selected period',
},
{
value: formatMs(summary ? summary.rollup.p99_ms : 0),
text: formatCount(summary ? summary.rollup.calls_error : 0) + ' error calls',
}
];
cards.forEach(function (card, index) {
var node = statCards[index];
if (!node) {
return;
}
node.querySelector('.stat-value').textContent = card.value;
var delta = node.querySelector('.stat-delta');
delta.className = 'stat-delta';
delta.textContent = card.text;
});
}
function renderChart() {
var timeline = state.usage ? state.usage.timeline : [];
chartWrap.innerHTML = '';
if (!timeline.length) {
chartWrap.innerHTML =
'' +
'
No usage data yet
' +
'
Invocation metrics will appear here after tests or published tool calls in the selected period.
' +
'
';
return;
}
var maxValue = Math.max.apply(null, timeline.map(function (point) {
return point.calls_ok + point.calls_error;
}));
timeline.forEach(function (point, index) {
var node = chartTemplate.content.cloneNode(true);
var okHeight = maxValue ? Math.round((point.calls_ok / maxValue) * 160) : 0;
var errorHeight = maxValue ? Math.round((point.calls_error / maxValue) * 160) : 0;
node.querySelector('.chart-bar.success').style.height = okHeight + 'px';
node.querySelector('.chart-bar.success').dataset.tip = formatCount(point.calls_ok) + ' ok';
node.querySelector('.chart-bar.error').style.height = errorHeight + 'px';
node.querySelector('.chart-bar.error').dataset.tip = formatCount(point.calls_error) + ' errors';
node.querySelector('.chart-col-label').textContent = chartBucketLabel(point.bucket_start, state.period, index);
chartWrap.appendChild(node);
});
}
function renderTable() {
var operations = state.usage ? state.usage.operations : [];
tableBody.innerHTML = '';
if (!operations.length) {
tableBody.innerHTML =
'' +
'' +
' No operation breakdown yet ' +
' The selected period has no invocation samples to break down by operation. ' +
' ' +
' |
';
return;
}
var totalCalls = operations.reduce(function (sum, operation) {
return sum + operation.calls_total;
}, 0);
operations.forEach(function (operation) {
var node = rowTemplate.content.cloneNode(true);
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
var share = totalCalls === 0
? 0
: ((operation.calls_total / totalCalls) * 100);
node.querySelector('.col-name').textContent = operation.operation_display_name;
node.querySelector('.col-op-slug').textContent = operation.operation_name;
var proto = node.querySelector('.col-proto');
proto.textContent = protocolLabel(operation.protocol);
proto.style.color = protocolColor(operation.protocol);
node.querySelector('.col-calls').textContent = formatCount(operation.calls_total);
node.querySelector('.col-errors').textContent = formatCount(operation.calls_error);
var errorRateEl = node.querySelector('.col-err-rate');
errorRateEl.textContent = errorRate.toFixed(2) + '%';
errorRateEl.style.color = errorRate > 5 ? 'var(--red)' : (errorRate > 1 ? 'var(--amber)' : 'var(--green)');
var latencyText = node.querySelector('.col-latency-text');
latencyText.textContent =
formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms);
var base = Math.max(operation.p99_ms, 1);
node.querySelector('.latency-p50').style.width = Math.max(4, Math.round((operation.p50_ms / base) * 64)) + 'px';
node.querySelector('.latency-p95').style.width = Math.max(4, Math.round(((operation.p95_ms - operation.p50_ms) / base) * 64)) + 'px';
node.querySelector('.latency-p99').style.width = Math.max(4, Math.round(((operation.p99_ms - operation.p95_ms) / base) * 64)) + 'px';
node.querySelector('.col-quota-label').textContent = share.toFixed(1) + '% of workspace traffic';
node.querySelector('.col-quota-fill').style.width = share.toFixed(1) + '%';
tableBody.appendChild(node);
});
}
function renderUsage() {
if (state.loading && !state.usage) {
renderEmpty('Loading usageā¦', 'Aggregating invocation metrics for the selected workspace.');
return;
}
if (state.loadError) {
renderEmpty('Unable to load usage', state.loadError);
return;
}
renderStats();
renderChart();
renderTable();
if (subtitle) {
subtitle.textContent = 'Breakdown for ' + periodLabel(state.period);
}
}
async function loadUsage() {
if (!window.CrankApi) {
state.loadError = 'Admin API is not available';
renderUsage();
return;
}
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.loadError = 'Workspace is not selected';
renderUsage();
return;
}
state.loading = true;
state.loadError = '';
renderUsage();
try {
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
} catch (error) {
state.loadError = error.message || 'Failed to load usage';
} finally {
state.loading = false;
renderUsage();
}
}
function exportCsv() {
if (!state.usage) {
if (window.CrankUi) {
window.CrankUi.info('Load usage data before exporting the CSV snapshot.', 'No usage data loaded');
}
return;
}
var rows = ['Operation,Protocol,Calls,Errors,Error Rate (%),p50 (ms),p95 (ms),p99 (ms)'];
state.usage.operations.forEach(function (operation) {
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
rows.push([
'"' + operation.operation_display_name + '"',
'"' + protocolLabel(operation.protocol) + '"',
operation.calls_total,
operation.calls_error,
errorRate.toFixed(2),
operation.p50_ms,
operation.p95_ms,
operation.p99_ms,
].join(','));
});
var blob = new Blob([rows.join('\r\n')], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'crank-usage-' + state.period + '.csv';
link.click();
URL.revokeObjectURL(url);
if (window.CrankUi) {
window.CrankUi.success('The current usage snapshot was exported as CSV.', 'Usage exported');
}
}
if (periodSelect) {
periodSelect.value = state.period;
periodSelect.addEventListener('change', function () {
state.period = this.value;
loadUsage();
});
}
if (exportBtn) {
exportBtn.addEventListener('click', exportCsv);
}
window.addEventListener('crank:workspacechange', loadUsage);
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(loadUsage);
} else {
loadUsage();
}
});