diff --git a/TASKS.md b/TASKS.md index b8385f2..0668479 100644 --- a/TASKS.md +++ b/TASKS.md @@ -50,8 +50,8 @@ Progress: - done: - `API Keys` page now has a mobile card layout instead of relying only on desktop table presentation - `Secrets` page now has a mobile card layout instead of relying only on desktop table presentation + - `Usage` page now has a mobile card layout instead of relying only on desktop table presentation - pending: - - mobile card layout for `Usage` - remaining `Agents` and `Settings` commercial UX polish ## Planned diff --git a/apps/ui/html/usage.html b/apps/ui/html/usage.html index 1567b29..ea86e52 100644 --- a/apps/ui/html/usage.html +++ b/apps/ui/html/usage.html @@ -10,6 +10,13 @@ + @@ -162,6 +169,7 @@
Breakdown for last 7 days
+
@@ -177,6 +185,8 @@
+
+
diff --git a/apps/ui/js/usage.js b/apps/ui/js/usage.js index 35bd7c2..a6fa3c5 100644 --- a/apps/ui/js/usage.js +++ b/apps/ui/js/usage.js @@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', function () { var exportBtn = document.querySelector('.page-header-actions .btn-secondary'); var chartWrap = document.getElementById('chart-bars'); var tableBody = document.getElementById('usage-tbody'); + var cardList = document.getElementById('usage-card-list'); var chartTemplate = document.getElementById('tmpl-chart-bar'); var rowTemplate = document.getElementById('tmpl-usage-row'); var subtitle = document.querySelector('.section-card-subtitle'); @@ -330,6 +331,9 @@ document.addEventListener('DOMContentLoaded', function () { function renderTable() { var operations = state.derived.localizedOperations; tableBody.innerHTML = ''; + if (cardList) { + cardList.innerHTML = ''; + } if (!operations.length) { var row = document.createElement('tr'); @@ -345,6 +349,7 @@ document.addEventListener('DOMContentLoaded', function () { ); row.appendChild(cell); tableBody.appendChild(row); + renderOperationCards([]); return; } @@ -387,6 +392,75 @@ document.addEventListener('DOMContentLoaded', function () { tableBody.appendChild(node); }); + + renderOperationCards(operations); + } + + function renderOperationCards(operations) { + if (!cardList) { + return; + } + + cardList.innerHTML = ''; + + if (!operations.length) { + cardList.appendChild(buildEmptyState( + tKey('usage.table.empty.title'), + tKey('usage.table.empty.sub'), + false + )); + return; + } + + operations.forEach(function(entry) { + var operation = entry.operation; + var localizedOperation = entry.localized; + var errorRate = operation.calls_total === 0 + ? 0 + : ((operation.calls_error / operation.calls_total) * 100); + var share = state.derived.totalCalls === 0 + ? 0 + : ((operation.calls_total / state.derived.totalCalls) * 100); + + var card = element('div', 'resource-card'); + var header = element('div', 'resource-card-header'); + var headerMain = element('div'); + headerMain.appendChild(element( + 'div', + 'resource-card-title', + localizedOperation.operation_display_name || operation.operation_display_name + )); + headerMain.appendChild(element('div', 'resource-card-subtitle', operation.operation_name)); + + var actions = element('div', 'resource-card-actions'); + var protocolBadge = element('span', 'badge'); + protocolBadge.textContent = protocolLabel(operation.protocol); + protocolBadge.style.color = protocolColor(operation.protocol); + actions.appendChild(protocolBadge); + header.appendChild(headerMain); + header.appendChild(actions); + card.appendChild(header); + + var metaGrid = element('div', 'resource-meta-grid'); + metaGrid.appendChild(buildUsageMetaItem('usage.table.th.calls', formatCount(operation.calls_total))); + metaGrid.appendChild(buildUsageMetaItem('usage.table.th.errors', formatCount(operation.calls_error))); + metaGrid.appendChild(buildUsageMetaItem('usage.table.th.error_rate', errorRate.toFixed(2) + '%')); + metaGrid.appendChild(buildUsageMetaItem( + 'usage.table.th.latency', + formatMs(operation.p50_ms) + ' / ' + formatMs(operation.p95_ms) + ' / ' + formatMs(operation.p99_ms) + )); + metaGrid.appendChild(buildUsageMetaItem('usage.table.th.share', tfKey('usage.table.share', { value: share.toFixed(1) }))); + card.appendChild(metaGrid); + + cardList.appendChild(card); + }); + } + + function buildUsageMetaItem(labelKey, valueText) { + var item = element('div', 'resource-meta-item'); + item.appendChild(element('div', 'resource-meta-label', tKey(labelKey))); + item.appendChild(element('div', 'resource-meta-value', valueText)); + return item; } function renderUsage() {