From 047df6eb721be064bde12cfefc75a8a475d7a541 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 30 May 2026 01:47:50 -0400 Subject: [PATCH] =?UTF-8?q?feat(extension):=20devices=20pane=20revamp=20?= =?UTF-8?q?=E2=80=94=20fingerprint=20+=20added-by=20+=20inline=20two-step?= =?UTF-8?q?=20revoke?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/src/popup/components/devices.ts | 94 ++++++++++++++++------- extension/src/popup/styles.css | 78 +++++++------------ extension/src/shared/types.ts | 1 + extension/src/vault/vault.css | 78 +++++++------------ 4 files changed, 123 insertions(+), 128 deletions(-) diff --git a/extension/src/popup/components/devices.ts b/extension/src/popup/components/devices.ts index 5b7c0d4..1c83394 100644 --- a/extension/src/popup/components/devices.ts +++ b/extension/src/popup/components/devices.ts @@ -3,6 +3,8 @@ import { setState, sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { Device } from '../../shared/types'; import { relativeTime } from '../../shared/relative-time'; +import { sshFingerprint } from '../../shared/ssh-fingerprint'; +import { GLYPH_REVOKE } from '../../shared/glyphs'; interface RevokedEntry { name: string; @@ -52,37 +54,47 @@ export async function renderDevices(app: HTMLElement): Promise { const isRegistered = currentDeviceName && devices.some((d) => d.name === currentDeviceName); + // Precompute fingerprints for all active devices + const fingerprints = new Map(); + await Promise.all(devices.map(async (d) => { + const fp = await sshFingerprint(d.public_key); + fingerprints.set(d.name, fp ?? '(unknown)'); + })); + const activeDevicesHtml = devices.length === 0 ? `

No devices registered

` : devices.map((d) => { const isCurrentDevice = d.name === currentDeviceName; + const fp = fingerprints.get(d.name) ?? '(unknown)'; + const addedBy = d.added_by && d.added_by !== 'unknown' ? ` · by ${escapeHtml(d.added_by)}` : ''; return ` -
-
- ${escapeHtml(d.name)}${isCurrentDevice ? ' ← you' : ''} - added ${relativeTime(d.added_at)} +
+
+ ${escapeHtml(d.name)} + ${isCurrentDevice + ? '← you' + : ``}
- ${isCurrentDevice ? '' : ``} +
${escapeHtml(fp)}
+
added ${escapeHtml(relativeTime(d.added_at))}${addedBy}
+
`; }).join(''); const revokedSectionHtml = revokedDevices.length === 0 ? '' : ` -
- - ${revokedDevices.length} revoked device${revokedDevices.length !== 1 ? 's' : ''} - -
+
+ ▸ show ${revokedDevices.length} revoked device${revokedDevices.length !== 1 ? 's' : ''} +
${revokedDevices.map((r) => `
-
- +
+ ${escapeHtml(r.name)} - - revoked ${relativeTime(r.revoked_at)} - ${r.revoked_by !== 'unknown' ? ` by ${escapeHtml(r.revoked_by)}` : ''} - +
+
+ revoked ${escapeHtml(relativeTime(r.revoked_at))}${r.revoked_by !== 'unknown' ? ` · by ${escapeHtml(r.revoked_by)}` : ''}
`).join('')} @@ -98,11 +110,14 @@ export async function renderDevices(app: HTMLElement): Promise {
${!isRegistered ? `
- ⚠ This device is not registered +
This device isn't registered.
+

Registering generates an ed25519 keypair and adds the public key to .relicario/devices.json on the remote.

` : ''} + ${devices.length > 0 ? `
ACTIVE · ${devices.length}
` : ''} ${activeDevicesHtml} + ${revokedDevices.length > 0 ? `
REVOKED · ${revokedDevices.length}
` : ''} ${revokedSectionHtml}
`; @@ -151,20 +166,43 @@ export async function renderDevices(app: HTMLElement): Promise { }); document.querySelectorAll('[data-revoke]').forEach((btn) => { - btn.addEventListener('click', async () => { + btn.addEventListener('click', () => { const name = btn.dataset.revoke; if (!name) return; - if (!confirm(`Revoke ${name}? This device will no longer be authorized.`)) return; - + const panel = document.querySelector(`[data-confirm-for="${CSS.escape(name)}"]`); + if (!panel) return; + panel.hidden = false; + panel.innerHTML = ` +

+ Revoke this device? It won't be able to sign commits or push changes after revocation. +

+
+ + +
+ `; btn.disabled = true; - btn.textContent = '...'; - const result = await sendMessage({ type: 'revoke_device', name }); - if (result.ok) { - await sendMessage({ type: 'sync' }); - renderDevices(app); - } else { - setState({ error: result.error }); - } + + panel.querySelector('[data-revoke-cancel]')?.addEventListener('click', () => { + panel.hidden = true; + panel.innerHTML = ''; + btn.disabled = false; + }); + + panel.querySelector('[data-revoke-confirm]')?.addEventListener('click', async () => { + const confirmBtn = panel.querySelector('[data-revoke-confirm]')!; + confirmBtn.disabled = true; + confirmBtn.textContent = '...'; + const result = await sendMessage({ type: 'revoke_device', name }); + if (result.ok) { + await sendMessage({ type: 'sync' }); + renderDevices(app); + } else { + setState({ error: result.error }); + confirmBtn.disabled = false; + confirmBtn.textContent = 'revoke'; + } + }); }); }); } diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 5527f4a..54fed12 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1169,69 +1169,47 @@ textarea { margin-bottom: 12px; } -.device-banner { +.device-row { + padding: 10px 0; + border-bottom: 1px solid var(--border-subtle); +} +.device-row__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; - padding: 10px; - background: #3d1f00; - border: 1px solid #9e6a03; - border-radius: 4px; - margin-bottom: 12px; - font-size: 12px; - color: #f0c674; + margin-bottom: 2px; } - -.device-row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; -} - -.device-row__info { - flex: 1; - min-width: 0; -} - -.device-row__name { - display: block; - font-size: 13px; - color: #c9d1d9; -} - +.device-row__name { color: var(--text); } .device-row__you { font-size: 11px; - color: #58a6ff; + color: var(--text-muted); + margin-left: 8px; } - .device-row__meta { font-size: 11px; - color: #8b949e; + color: var(--text-muted); + margin-top: 2px; } +.device-row__confirm { + margin-top: 8px; + padding: 10px; + border: 1px solid var(--border-subtle); + border-radius: 3px; + background: var(--bg-input); +} +.device-row__confirm-text { margin: 0 0 8px 0; color: var(--text); } +.device-row__confirm-actions { display: flex; gap: 8px; justify-content: flex-end; } -.device-row__revoke { - font-size: 11px; - padding: 4px 8px; - background: #da3633; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.device-row__revoke:hover { - background: #f85149; -} - -.device-row__revoke:disabled { - opacity: 0.5; - cursor: default; +.device-banner { + padding: 12px; + border: 1px solid var(--border-subtle); + border-radius: 3px; + background: var(--bg-pane); + margin-bottom: 16px; } +.device-banner__title { margin-bottom: 4px; } +.device-banner__body { font-size: 12px; margin: 0 0 10px 0; } /* --- Field history view --- */ diff --git a/extension/src/shared/types.ts b/extension/src/shared/types.ts index c54e954..565afcf 100644 --- a/extension/src/shared/types.ts +++ b/extension/src/shared/types.ts @@ -150,6 +150,7 @@ export interface Device { name: string; public_key: string; // hex-encoded ed25519 pubkey added_at: number; // unix timestamp + added_by?: string; // device name that registered this device (optional) } // --- Field history view --- diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 98d72a2..c4d2ba6 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1089,69 +1089,47 @@ textarea { margin-bottom: 12px; } -.device-banner { +.device-row { + padding: 10px 0; + border-bottom: 1px solid var(--border-subtle); +} +.device-row__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; - padding: 10px; - background: #3d1f00; - border: 1px solid #9e6a03; - border-radius: 4px; - margin-bottom: 12px; - font-size: 12px; - color: #f0c674; + margin-bottom: 2px; } - -.device-row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; -} - -.device-row__info { - flex: 1; - min-width: 0; -} - -.device-row__name { - display: block; - font-size: 13px; - color: #c9d1d9; -} - +.device-row__name { color: var(--text); } .device-row__you { font-size: 11px; - color: #58a6ff; + color: var(--text-muted); + margin-left: 8px; } - .device-row__meta { font-size: 11px; - color: #8b949e; + color: var(--text-muted); + margin-top: 2px; } +.device-row__confirm { + margin-top: 8px; + padding: 10px; + border: 1px solid var(--border-subtle); + border-radius: 3px; + background: var(--bg-input); +} +.device-row__confirm-text { margin: 0 0 8px 0; color: var(--text); } +.device-row__confirm-actions { display: flex; gap: 8px; justify-content: flex-end; } -.device-row__revoke { - font-size: 11px; - padding: 4px 8px; - background: #da3633; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.device-row__revoke:hover { - background: #f85149; -} - -.device-row__revoke:disabled { - opacity: 0.5; - cursor: default; +.device-banner { + padding: 12px; + border: 1px solid var(--border-subtle); + border-radius: 3px; + background: var(--bg-pane); + margin-bottom: 16px; } +.device-banner__title { margin-bottom: 4px; } +.device-banner__body { font-size: 12px; margin: 0 0 10px 0; } /* --- Field history view --- */