/// 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 = `
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);
}