/// Trash view — lists soft-deleted items with restore/purge actions. import { getState, setState, sendMessage, navigate, escapeHtml } from '../../shared/state'; import type { ItemId, ManifestEntry, VaultSettings } from '../../shared/types'; import { GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_CARD, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, GLYPH_TYPE_TOTP, } from '../../shared/glyphs'; const TYPE_ICONS: Record = { login: GLYPH_TYPE_LOGIN, secure_note: GLYPH_TYPE_SECURE_NOTE, identity: GLYPH_TYPE_IDENTITY, card: GLYPH_TYPE_CARD, key: GLYPH_TYPE_KEY, document: GLYPH_TYPE_DOCUMENT, totp: GLYPH_TYPE_TOTP, }; 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`; } function daysUntilPurge(trashedAt: number, retention: VaultSettings['trash_retention']): number | null { if (retention.kind === 'forever') return null; const trashedDaysAgo = Math.floor((Date.now() / 1000 - trashedAt) / 86400); return Math.max(0, retention.value - trashedDaysAgo); } export function teardown(): void { // No cleanup needed } export async function renderTrash(app: HTMLElement): Promise { const state = getState(); // Fetch trashed items const resp = await sendMessage({ type: 'list_trashed' }); if (!resp.ok) { app.innerHTML = `

Failed to load trash

`; return; } const items = (resp.data as { items: Array<[ItemId, ManifestEntry]> }).items; const retention = state.vaultSettings?.trash_retention ?? { kind: 'days', value: 30 }; // Calculate days until oldest auto-purges let oldestPurgeDays: number | null = null; if (items.length > 0 && retention.kind === 'days') { const oldest = items[items.length - 1][1]; oldestPurgeDays = daysUntilPurge(oldest.trashed_at ?? 0, retention); } const headerInfo = items.length === 0 ? '' : oldestPurgeDays !== null ? `${items.length} item${items.length === 1 ? '' : 's'} · oldest auto-purges in ${oldestPurgeDays}d` : `${items.length} item${items.length === 1 ? '' : 's'}`; app.innerHTML = `

trash

${headerInfo ? `

${escapeHtml(headerInfo)}

` : ''} ${items.length === 0 ? `

Trash is empty

` : items.map(([id, entry]) => `
${TYPE_ICONS[entry.type] ?? '◻'}
${escapeHtml(entry.title)} trashed ${relativeTime(entry.trashed_at ?? 0)}
`).join('')} ${items.length > 0 ? `
` : ''}
`; // Wire handlers document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); document.querySelectorAll('[data-restore]').forEach((btn) => { btn.addEventListener('click', async () => { const id = btn.dataset.restore; if (!id) return; btn.disabled = true; btn.textContent = '...'; const result = await sendMessage({ type: 'restore_item', id }); if (result.ok) { await sendMessage({ type: 'sync' }); renderTrash(app); } else { setState({ error: result.error }); } }); }); document.getElementById('empty-trash-btn')?.addEventListener('click', async () => { if (!confirm(`Permanently delete ${items.length} item${items.length === 1 ? '' : 's'}? This cannot be undone.`)) { return; } const btn = document.getElementById('empty-trash-btn') as HTMLButtonElement; btn.disabled = true; btn.textContent = 'deleting...'; const result = await sendMessage({ type: 'purge_all_trash' }); if (result.ok) { await sendMessage({ type: 'sync' }); renderTrash(app); } else { setState({ error: result.error }); } }); }