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>
92 lines
3.5 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|
|
}
|