/// Device management view — list devices with revoke actions. import { setState, sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { Device } from '../../shared/types'; interface RevokedEntry { name: string; public_key: string; revoked_at: number; revoked_by: string; } function relativeTime(unixSec: number): string { const now = Math.floor(Date.now() / 1000); const diff = now - unixSec; if (diff < 60) return 'just now'; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`; return `${Math.floor(diff / 2592000)}mo ago`; } function detectDefaultDeviceName(): string { const ua = navigator.userAgent ?? ''; const platform = (navigator.platform ?? '').toLowerCase(); const isFirefox = /firefox/i.test(ua); const isEdge = /edg/i.test(ua); const isChrome = /chrome/i.test(ua) && !isEdge; const browser = isFirefox ? 'Firefox' : isEdge ? 'Edge' : isChrome ? 'Chrome' : 'Browser'; const os = platform.includes('mac') ? 'macOS' : platform.includes('win') ? 'Windows' : platform.includes('linux') ? 'Linux' : 'Unknown'; return `${browser} on ${os}`; } export function teardown(): void { // No cleanup needed } export async function renderDevices(app: HTMLElement): Promise { // Get current device name from local storage const stored = await chrome.storage.local.get(['device_name']); const currentDeviceName: string | undefined = stored.device_name as string | undefined; // Fetch active device list and revoked list in parallel const [devicesResp, revokedResp] = await Promise.all([ sendMessage({ type: 'list_devices' }), sendMessage({ type: 'list_revoked' }), ]); if (!devicesResp.ok) { app.innerHTML = `

Failed to load devices

`; return; } const devices = (devicesResp.data as { devices: Device[] }).devices; const revokedDevices: RevokedEntry[] = revokedResp.ok ? (revokedResp.data as { revoked: RevokedEntry[] }).revoked : []; const isRegistered = currentDeviceName && devices.some((d) => d.name === currentDeviceName); const activeDevicesHtml = devices.length === 0 ? `

No devices registered

` : devices.map((d) => { const isCurrentDevice = d.name === currentDeviceName; return `
${escapeHtml(d.name)}${isCurrentDevice ? ' ← you' : ''} added ${relativeTime(d.added_at)}
${isCurrentDevice ? '' : ``}
`; }).join(''); const revokedSectionHtml = revokedDevices.length === 0 ? '' : `
${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)}` : ''}
`).join('')}
`; app.innerHTML = `

devices

${!isRegistered ? `
⚠ This device is not registered
` : ''} ${activeDevicesHtml} ${revokedSectionHtml}
`; // Wire handlers document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); document.getElementById('register-btn')?.addEventListener('click', () => { const banner = document.querySelector('.device-banner'); if (!banner) return; const defaultName = detectDefaultDeviceName(); banner.innerHTML = `
`; document.getElementById('register-cancel-btn')?.addEventListener('click', () => { renderDevices(app); }); document.getElementById('register-confirm-btn')?.addEventListener('click', async () => { const input = document.getElementById('register-name-input') as HTMLInputElement | null; const name = input?.value.trim(); if (!name) { setState({ error: 'Device name is required' }); return; } const result = await sendMessage({ type: 'register_this_device', name }); if (result.ok) { renderDevices(app); } else { setState({ error: result.error }); } }); }); document.querySelectorAll('[data-revoke]').forEach((btn) => { btn.addEventListener('click', async () => { const name = btn.dataset.revoke; if (!name) return; if (!confirm(`Revoke ${name}? This device will no longer be authorized.`)) return; 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 }); } }); }); }