diff --git a/TASKS.md b/TASKS.md index 9d60701..349401b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -46,6 +46,13 @@ Verification: - manual mobile checks at 375px width; - EN/RU copy audit. +Progress: +- done: + - `API Keys` page now has a mobile card layout instead of relying only on desktop table presentation +- pending: + - mobile card layouts for `Secrets` and `Usage` + - remaining `Agents` and `Settings` commercial UX polish + ## Planned ### `feat/open-core-repo-boundary` diff --git a/apps/ui/html/api-keys.html b/apps/ui/html/api-keys.html index 646b193..e6b6b3e 100644 --- a/apps/ui/html/api-keys.html +++ b/apps/ui/html/api-keys.html @@ -22,6 +22,11 @@ } .scope-checkbox-name { font-size: 13px; font-weight: 500; color: var(--text-primary); } .scope-checkbox-desc { font-size: 11.5px; color: var(--text-muted); } + .keys-card-list { display: none; } + @media (max-width: 720px) { + #keys-table-wrap { display: none; } + .keys-card-list { display: grid; } + } @@ -195,6 +200,7 @@ +
diff --git a/apps/ui/js/api-keys.js b/apps/ui/js/api-keys.js index 6f31241..b3d1ccd 100644 --- a/apps/ui/js/api-keys.js +++ b/apps/ui/js/api-keys.js @@ -291,12 +291,16 @@ function renderScopes() { function renderTable(errorMessage) { var q = search.toLowerCase(); var tbody = document.getElementById('keys-tbody'); + var cardList = document.getElementById('keys-card-list'); var rows = KEYS.filter(function(key) { return !q || key.name.toLowerCase().includes(q) || key.prefix.toLowerCase().includes(q); }); var subtitle = document.getElementById('keys-summary-subtitle'); tbody.innerHTML = ''; + if (cardList) { + cardList.innerHTML = ''; + } if (subtitle) { var active = KEYS.filter(function(key) { return key.status === 'active'; }).length; @@ -314,6 +318,7 @@ function renderTable(errorMessage) { errorCell.textContent = errorMessage; errorRow.appendChild(errorCell); tbody.appendChild(errorRow); + renderKeyCards([], errorMessage); return; } @@ -327,6 +332,7 @@ function renderTable(errorMessage) { : tKey('apikeys.agent.empty_hint'); empty.appendChild(td); tbody.appendChild(empty); + renderKeyCards(rows); return; } @@ -370,6 +376,121 @@ function renderTable(errorMessage) { tbody.appendChild(node); }); + + renderKeyCards(rows); +} + +function renderKeyCards(rows, errorMessage) { + var cardList = document.getElementById('keys-card-list'); + if (!cardList) return; + + cardList.innerHTML = ''; + + if (errorMessage) { + cardList.appendChild(buildKeyCardMessage(errorMessage, true)); + return; + } + + if (!rows.length) { + cardList.appendChild( + buildKeyCardMessage( + currentAgentId + ? (KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none')) + : tKey('apikeys.agent.empty_hint'), + false + ) + ); + return; + } + + rows.forEach(function(key) { + var card = document.createElement('div'); + card.className = 'resource-card'; + + var header = document.createElement('div'); + header.className = 'resource-card-header'; + var headerMain = document.createElement('div'); + var title = document.createElement('div'); + title.className = 'resource-card-title'; + title.textContent = key.name; + var subtitle = document.createElement('div'); + subtitle.className = 'resource-card-subtitle'; + subtitle.textContent = key.prefix; + headerMain.appendChild(title); + headerMain.appendChild(subtitle); + + var actions = document.createElement('div'); + actions.className = 'resource-card-actions'; + + var statusBadge = document.createElement('span'); + statusBadge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; + statusBadge.textContent = key.status === 'active' + ? tKey('apikeys.status.active') + : tKey('apikeys.status.revoked'); + actions.appendChild(statusBadge); + header.appendChild(headerMain); + header.appendChild(actions); + card.appendChild(header); + + var metaGrid = document.createElement('div'); + metaGrid.className = 'resource-meta-grid'; + metaGrid.appendChild(buildMetaItem('apikeys.th.scopes', key.scopes.join(', ') || '—')); + metaGrid.appendChild(buildMetaItem('apikeys.th.created', key.created)); + metaGrid.appendChild(buildMetaItem('apikeys.th.last', key.lastUsed)); + card.appendChild(metaGrid); + + var actionRow = document.createElement('div'); + actionRow.className = 'resource-card-actions'; + if (key.status === 'active') { + actionRow.appendChild(buildCardAction('apikeys.action.copy_prefix', function() { + copyPrefix(key.prefix); + })); + actionRow.appendChild(buildCardAction('apikeys.action.revoke', function() { + revokeKey(key.id); + }, true)); + } else { + actionRow.appendChild(buildCardAction('apikeys.action.delete', function() { + deleteKey(key.id); + }, true)); + } + card.appendChild(actionRow); + + cardList.appendChild(card); + }); +} + +function buildKeyCardMessage(text, isError) { + var card = document.createElement('div'); + card.className = 'resource-card'; + var value = document.createElement('div'); + value.className = 'resource-meta-value'; + value.textContent = text; + value.style.color = isError ? 'var(--danger,#f85149)' : 'var(--text-muted)'; + card.appendChild(value); + return card; +} + +function buildMetaItem(labelKey, valueText) { + var item = document.createElement('div'); + item.className = 'resource-meta-item'; + var label = document.createElement('div'); + label.className = 'resource-meta-label'; + label.textContent = tKey(labelKey); + var value = document.createElement('div'); + value.className = 'resource-meta-value'; + value.textContent = valueText; + item.appendChild(label); + item.appendChild(value); + return item; +} + +function buildCardAction(labelKey, onClick, isDanger) { + var button = document.createElement('button'); + button.type = 'button'; + button.className = isDanger ? 'btn-secondary' : 'btn-secondary'; + button.textContent = tKey(labelKey); + button.addEventListener('click', onClick); + return button; } var modal = document.getElementById('modal-create');