diff --git a/TASKS.md b/TASKS.md index 349401b..b8385f2 100644 --- a/TASKS.md +++ b/TASKS.md @@ -49,8 +49,9 @@ Verification: 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 - pending: - - mobile card layouts for `Secrets` and `Usage` + - mobile card layout for `Usage` - remaining `Agents` and `Settings` commercial UX polish ## Planned diff --git a/apps/ui/html/secrets.html b/apps/ui/html/secrets.html index b8a71ba..defbeb3 100644 --- a/apps/ui/html/secrets.html +++ b/apps/ui/html/secrets.html @@ -39,6 +39,11 @@ color: var(--text-primary); font-weight: 600; } + .secrets-card-list { display: none; } + @media (max-width: 720px) { + #secrets-table-wrap { display: none; } + .secrets-card-list { display: grid; } + } @@ -138,6 +143,7 @@
+
@@ -152,6 +158,8 @@
+
+
diff --git a/apps/ui/js/secrets.js b/apps/ui/js/secrets.js index 8b89519..2fecacf 100644 --- a/apps/ui/js/secrets.js +++ b/apps/ui/js/secrets.js @@ -17,6 +17,7 @@ function initSecretsPage() { var modal = document.getElementById('secret-modal'); var tbody = document.getElementById('secrets-tbody'); + var cardList = document.getElementById('secrets-card-list'); var profilesList = document.getElementById('auth-profiles-list'); var summary = document.getElementById('secrets-summary'); var profilesSummary = document.getElementById('secret-profiles-summary'); @@ -248,6 +249,9 @@ function initSecretsPage() { }); window.CrankDom.clear(tbody); + if (cardList) { + window.CrankDom.clear(cardList); + } if (state.loading && state.secrets.length === 0) { var loadingRow = document.createElement('tr'); @@ -257,6 +261,7 @@ function initSecretsPage() { loadingCell.textContent = tKey('secrets.loading'); loadingRow.appendChild(loadingCell); tbody.appendChild(loadingRow); + renderSecretCards([], tKey('secrets.loading')); return; } @@ -268,6 +273,7 @@ function initSecretsPage() { errorCell.textContent = state.error; errorRow.appendChild(errorCell); tbody.appendChild(errorRow); + renderSecretCards([], state.error); return; } @@ -279,6 +285,7 @@ function initSecretsPage() { emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none'); emptyRow.appendChild(emptyCell); tbody.appendChild(emptyRow); + renderSecretCards(rows); return; } @@ -340,6 +347,106 @@ function initSecretsPage() { tbody.appendChild(tr); }); + + renderSecretCards(rows); + } + + function renderSecretCards(rows, statusText) { + if (!cardList) { + return; + } + + window.CrankDom.clear(cardList); + + if (statusText && !rows.length) { + var messageCard = document.createElement('div'); + messageCard.className = 'resource-card'; + var message = document.createElement('div'); + message.className = 'resource-meta-value'; + message.textContent = statusText; + message.style.color = state.error ? 'var(--danger,#f85149)' : 'var(--text-muted)'; + messageCard.appendChild(message); + cardList.appendChild(messageCard); + return; + } + + rows.forEach(function(secret) { + var references = state.derived.usageBySecret[secret.id] || []; + var card = document.createElement('div'); + card.className = 'resource-card'; + + var header = document.createElement('div'); + header.className = 'resource-card-header'; + var headerBody = document.createElement('div'); + var title = document.createElement('div'); + title.className = 'resource-card-title'; + title.textContent = secret.name; + var subtitle = document.createElement('div'); + subtitle.className = 'resource-card-subtitle'; + subtitle.textContent = secretKindLabel(secret.kind); + headerBody.appendChild(title); + headerBody.appendChild(subtitle); + + var actions = document.createElement('div'); + actions.className = 'resource-card-actions'; + var badge = document.createElement('span'); + badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active'); + badge.textContent = tKey('secrets.status.' + secret.status); + actions.appendChild(badge); + header.appendChild(headerBody); + header.appendChild(actions); + card.appendChild(header); + + var metaGrid = document.createElement('div'); + metaGrid.className = 'resource-meta-grid'; + metaGrid.appendChild(buildSecretMetaItem('secrets.th.version', 'v' + secret.current_version)); + metaGrid.appendChild(buildSecretMetaItem( + 'secrets.th.last_used', + secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never') + )); + metaGrid.appendChild(buildSecretMetaItem( + 'secrets.th.used_by', + window.tPlural('secrets.used_by_count', references.length, { count: references.length }) + )); + card.appendChild(metaGrid); + + var actionRow = document.createElement('div'); + actionRow.className = 'resource-card-actions'; + var rotateButton = document.createElement('button'); + rotateButton.className = 'btn-secondary'; + rotateButton.type = 'button'; + rotateButton.textContent = tKey('secrets.action.rotate'); + rotateButton.addEventListener('click', function() { + openModal('rotate', secret); + }); + actionRow.appendChild(rotateButton); + + var deleteButton = document.createElement('button'); + deleteButton.className = 'btn-secondary'; + deleteButton.type = 'button'; + deleteButton.textContent = tKey('secrets.action.delete'); + deleteButton.addEventListener('click', async function() { + await deleteSecret(secret); + }); + actionRow.appendChild(deleteButton); + card.appendChild(actionRow); + + cardList.appendChild(card); + }); + } + + function buildSecretMetaItem(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 renderProfiles() {