Files
relicario/extension/src/popup/components/devices.ts
adlee-was-taken 7fe54472b3 feat(ext/popup): devices view — list devices with revoke actions
Shows registered devices with "← you" indicator on current device.
Revoke button on other devices. Unregistered banner if current
device not in list.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-27 00:19:59 -04:00

92 lines
3.5 KiB
TypeScript

/// Device management view — list devices with revoke actions.
import { setState, sendMessage, navigate, escapeHtml } from '../popup';
import type { Device } from '../../shared/types';
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`;
}
export function teardown(): void {
// No cleanup needed
}
export async function renderDevices(app: HTMLElement): Promise<void> {
// 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 device list
const resp = await sendMessage({ type: 'list_devices' });
if (!resp.ok) {
app.innerHTML = `<div class="pad"><p class="error">Failed to load devices</p></div>`;
return;
}
const devices = (resp.data as { devices: Device[] }).devices;
const isRegistered = currentDeviceName && devices.some((d) => d.name === currentDeviceName);
app.innerHTML = `
<div class="pad">
<div class="devices-header">
<button class="btn" id="back-btn">← back</button>
<h3 style="margin:0;">devices</h3>
</div>
${!isRegistered ? `
<div class="device-banner">
<span>⚠ This device is not registered</span>
<button class="btn btn-primary" id="register-btn">Register this device</button>
</div>
` : ''}
${devices.length === 0
? `<p class="muted" style="text-align:center;margin-top:32px;">No devices registered</p>`
: devices.map((d) => {
const isCurrentDevice = d.name === currentDeviceName;
return `
<div class="device-row">
<div class="device-row__info">
<span class="device-row__name">${escapeHtml(d.name)}${isCurrentDevice ? ' <span class="device-row__you">← you</span>' : ''}</span>
<span class="device-row__meta">added ${relativeTime(d.added_at)}</span>
</div>
${isCurrentDevice ? '' : `<button class="device-row__revoke" data-revoke="${escapeHtml(d.name)}">revoke</button>`}
</div>
`;
}).join('')}
</div>
`;
// Wire handlers
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
document.getElementById('register-btn')?.addEventListener('click', async () => {
// Generate keypair and register
// This would need WASM access - for now, redirect to a registration flow
// The full implementation happens in Task 12 (setup wizard integration)
setState({ error: 'Device registration from here is not yet implemented. Use setup wizard.' });
});
document.querySelectorAll<HTMLButtonElement>('[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 });
}
});
});
}