feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+110 -32
View File
@@ -11,6 +11,8 @@ document.addEventListener('DOMContentLoaded', function () {
},
loading: false,
loadError: '',
usageEpoch: 0,
exporting: false,
};
var periodSelect = document.getElementById('period');
@@ -20,6 +22,7 @@ document.addEventListener('DOMContentLoaded', function () {
var cardList = document.getElementById('usage-card-list');
var chartTemplate = document.getElementById('tmpl-chart-bar');
var rowTemplate = document.getElementById('tmpl-usage-row');
var outcomeList = document.getElementById('usage-outcome-list');
var subtitle = document.querySelector('.section-card-subtitle');
var statCards = document.querySelectorAll('.stats-grid .stat-card');
@@ -73,6 +76,12 @@ document.addEventListener('DOMContentLoaded', function () {
return 'REST';
}
function outcomeLabel(outcome) {
var key = 'usage.outcome.' + outcome;
var translated = tKey(key);
return translated === key ? outcome : translated;
}
function localizedUsageOperation(operation) {
if (!window.localizeDemoOperation) {
return operation;
@@ -439,6 +448,43 @@ document.addEventListener('DOMContentLoaded', function () {
return item;
}
function renderOutcomes() {
if (!outcomeList) {
return;
}
outcomeList.innerHTML = '';
var outcomes = state.usage ? (state.usage.outcomes || []) : [];
if (!outcomes.length) {
outcomeList.appendChild(buildEmptyState(
tKey('usage.outcomes.empty.title'),
tKey('usage.outcomes.empty.sub'),
true
));
return;
}
outcomes.forEach(function (outcome) {
var card = element('div', 'resource-card');
var header = element('div', 'resource-card-header');
var headerMain = element('div');
headerMain.appendChild(element('div', 'resource-card-title', outcomeLabel(outcome.group)));
headerMain.appendChild(element(
'div',
'resource-card-subtitle',
outcome.execution_error_code || tKey('usage.outcome.no_error_code')
));
header.appendChild(headerMain);
header.appendChild(element('span', 'badge', formatCount(outcome.calls_total)));
card.appendChild(header);
var metaGrid = element('div', 'resource-meta-grid');
metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(outcome.calls_total)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p50', formatMs(outcome.p50_ms)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p95', formatMs(outcome.p95_ms)));
metaGrid.appendChild(buildUsageMetaItem('usage.outcomes.p99', formatMs(outcome.p99_ms)));
card.appendChild(metaGrid);
outcomeList.appendChild(card);
});
}
function renderUsage() {
if (state.loading && !state.usage) {
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
@@ -452,6 +498,7 @@ document.addEventListener('DOMContentLoaded', function () {
renderStats();
renderChart();
renderOutcomes();
renderTable();
if (subtitle) {
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
@@ -472,24 +519,46 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var workspaceId = state.workspaceId;
var epoch = ++state.usageEpoch;
state.loading = true;
state.loadError = '';
renderUsage();
try {
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
var usage = await window.CrankApi.getUsageOverview(workspaceId, { period: state.period });
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
state.usage = usage;
recomputeDerivedState();
} catch (error) {
if (epoch !== state.usageEpoch) {
return;
}
state.loadError = error.message || tKey('usage.error.load');
state.usage = null;
recomputeDerivedState();
} finally {
state.loading = false;
renderUsage();
if (epoch === state.usageEpoch) {
state.loading = false;
renderUsage();
}
}
}
function exportCsv() {
function csvCell(value) {
var text = String(value === null || value === undefined ? '' : value);
if (/^[=+\-@\t\r\n]/.test(text)) {
text = "'" + text;
}
return '"' + text.replace(/"/g, '""') + '"';
}
async function exportCsv() {
if (!window.CrankApi || state.exporting) {
return;
}
if (!state.usage) {
if (window.CrankUi) {
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
@@ -497,33 +566,39 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var rows = [tKey('usage.csv.header')];
state.usage.operations.forEach(function (operation) {
var localizedOperation = localizedUsageOperation(operation);
var errorRate = operation.calls_total === 0
? 0
: ((operation.calls_error / operation.calls_total) * 100);
rows.push([
'"' + (localizedOperation.operation_display_name || 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(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
var workspaceId = currentWorkspaceId();
if (!workspaceId) {
return;
}
var epoch = state.usageEpoch;
state.exporting = true;
if (exportBtn) {
exportBtn.disabled = true;
}
try {
var csv = await window.CrankApi.exportUsageCsv(workspaceId, { period: state.period });
if (epoch !== state.usageEpoch || workspaceId !== currentWorkspaceId()) {
return;
}
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
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(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('usage.export.error.body'), tKey('usage.export.error.title'));
}
} finally {
state.exporting = false;
if (exportBtn) {
exportBtn.disabled = false;
}
}
}
@@ -539,7 +614,10 @@ document.addEventListener('DOMContentLoaded', function () {
exportBtn.addEventListener('click', exportCsv);
}
window.addEventListener('crank:workspacechange', loadUsage);
window.addEventListener('crank:workspacechange', function () {
state.usageEpoch += 1;
loadUsage();
});
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(loadUsage);