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');