/// Vault-level settings screen. Covers retention (trash + field history), /// generator defaults (preview + "configure" → opens popover), and /// autofill origin-ack revocation. import { getState, setState, sendMessage, navigate, escapeHtml } from '../popup'; import type { VaultSettings, TrashRetention, HistoryRetention, GeneratorRequest, } from '../../shared/types'; import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from './generator-panel'; let pendingSettings: VaultSettings | null = null; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; export function teardown(): void { closeGeneratorPanel(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; } pendingSettings = null; } // --- Retention helpers --- function trashRetentionToValue(r: TrashRetention): string { if (r.kind === 'forever') return 'forever'; return `days:${r.value}`; } function valueToTrashRetention(v: string): TrashRetention { if (v === 'forever') return { kind: 'forever' }; const m = /^days:(\d+)$/.exec(v); if (m) return { kind: 'days', value: Number(m[1]) }; return { kind: 'forever' }; } function historyRetentionToValue(r: HistoryRetention): string { if (r.kind === 'forever') return 'forever'; if (r.kind === 'last_n') return `last_n:${r.value}`; return `days:${r.value}`; } function valueToHistoryRetention(v: string): HistoryRetention { if (v === 'forever') return { kind: 'forever' }; const mLast = /^last_n:(\d+)$/.exec(v); if (mLast) return { kind: 'last_n', value: Number(mLast[1]) }; const mDays = /^days:(\d+)$/.exec(v); if (mDays) return { kind: 'days', value: Number(mDays[1]) }; return { kind: 'forever' }; } // --- Generator summary --- function generatorSummary(req: GeneratorRequest): string { if (req.kind === 'random') { const classes: string[] = []; if (req.classes.lower) classes.push('lower'); if (req.classes.upper) classes.push('upper'); if (req.classes.digits) classes.push('digits'); if (req.classes.symbols) classes.push('symbols'); const sc = req.symbol_charset.kind; return `Random, ${req.length} chars, ${classes.join('+') || 'no classes'}, ${sc} symbols`; } return `BIP39, ${req.word_count} words, "${req.separator}" separator, ${req.capitalization}`; } // --- Time formatting --- 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`; return `${Math.floor(diff / 86400)}d ago`; } // --- Render --- export function renderVaultSettings(app: HTMLElement): void { const state = getState(); const base = state.vaultSettings; if (!base) { app.innerHTML = `

Vault settings not loaded yet.

`; return; } pendingSettings = JSON.parse(JSON.stringify(base)) as VaultSettings; function rerender(): void { if (!pendingSettings) return; const acksEntries = Object.entries(pendingSettings.autofill_origin_acks) .sort(([, a], [, b]) => b - a); app.innerHTML = `

vault settings

retention
trash
field history
generator

${escapeHtml(generatorSummary(pendingSettings.generator_defaults))}

autofill origins
${acksEntries.length === 0 ? `

No origins acknowledged yet.

` : acksEntries.map(([host, ts]) => `
${escapeHtml(host)} ${escapeHtml(relativeTime(ts))}
`).join('')}
`; // Set current select values (document.getElementById('trash-retention') as HTMLSelectElement).value = trashRetentionToValue(pendingSettings.trash_retention); (document.getElementById('history-retention') as HTMLSelectElement).value = historyRetentionToValue(pendingSettings.field_history_retention); wireHandlers(); updateSaveEnabled(); } function updateSaveEnabled(): void { const saveBtn = document.getElementById('save-btn') as HTMLButtonElement | null; if (!saveBtn || !pendingSettings || !base) return; const changed = JSON.stringify(pendingSettings) !== JSON.stringify(base); saveBtn.disabled = !changed; } function wireHandlers(): void { document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); document.getElementById('discard-btn')?.addEventListener('click', () => navigate('list')); document.getElementById('trash-retention')?.addEventListener('change', (e) => { if (!pendingSettings) return; pendingSettings.trash_retention = valueToTrashRetention((e.target as HTMLSelectElement).value); updateSaveEnabled(); }); document.getElementById('history-retention')?.addEventListener('change', (e) => { if (!pendingSettings) return; pendingSettings.field_history_retention = valueToHistoryRetention((e.target as HTMLSelectElement).value); updateSaveEnabled(); }); document.querySelectorAll('[data-revoke]').forEach((btn) => { btn.addEventListener('click', () => { if (!pendingSettings) return; const host = btn.dataset.revoke ?? ''; delete pendingSettings.autofill_origin_acks[host]; rerender(); }); }); document.getElementById('configure-gen')?.addEventListener('click', (e) => { const trigger = e.currentTarget as HTMLElement; if (isGeneratorPanelOpen()) { closeGeneratorPanel(); return; } const generatorSection = trigger.closest('.settings-section') as HTMLElement | null; if (!generatorSection || pendingSettings === null) return; openGeneratorPanel({ parent: generatorSection, trigger, initial: pendingSettings.generator_defaults, context: 'configure-defaults', }); }); document.getElementById('save-btn')?.addEventListener('click', async () => { if (!pendingSettings) return; const resp = await sendMessage({ type: 'update_vault_settings', settings: pendingSettings }); if (resp.ok) { // Refresh cached state and navigate back. const refreshed = await sendMessage({ type: 'get_vault_settings' }); if (refreshed.ok && refreshed.data) { const vs = (refreshed.data as { settings: VaultSettings }).settings; if (vs) { setState({ vaultSettings: vs, generatorDefaults: vs.generator_defaults }); } } navigate('list'); } else { setState({ error: resp.error }); } }); } rerender(); const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (activeKeyHandler) document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; navigate('list'); } }; activeKeyHandler = handler; document.addEventListener('keydown', handler); }