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
+
+
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() {