|
|
|
|
@@ -1,18 +1,111 @@
|
|
|
|
|
/// Settings view — capture toggle, prompt style, and blacklist management.
|
|
|
|
|
|
|
|
|
|
import { sendMessage, navigate, escapeHtml } from '../../shared/state';
|
|
|
|
|
import type { DeviceSettings } from '../../shared/types';
|
|
|
|
|
import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SYNC } from '../../shared/glyphs';
|
|
|
|
|
import { sendMessage, escapeHtml, openVaultTab } from '../../shared/state';
|
|
|
|
|
import type { VaultSettings, DeviceSettings, TrashRetention, HistoryRetention } from '../../shared/types';
|
|
|
|
|
import type { ColorScheme } from '../../shared/color-scheme';
|
|
|
|
|
import {
|
|
|
|
|
loadColorScheme, saveColorScheme, resetColorScheme,
|
|
|
|
|
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
|
|
|
|
|
} from '../../shared/color-scheme';
|
|
|
|
|
import { colorizePassword } from '../../shared/password-coloring';
|
|
|
|
|
import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from './generator-panel';
|
|
|
|
|
import { renderSecuritySection, teardownSecuritySection } from './settings-security';
|
|
|
|
|
|
|
|
|
|
export async function renderSettings(app: HTMLElement): Promise<void> {
|
|
|
|
|
app.innerHTML = '<div class="pad" style="text-align:center; padding-top:20px;"><span class="spinner"></span></div>';
|
|
|
|
|
type SettingsSection =
|
|
|
|
|
| 'autofill'
|
|
|
|
|
| 'display'
|
|
|
|
|
| 'security'
|
|
|
|
|
| 'generator'
|
|
|
|
|
| 'retention'
|
|
|
|
|
| 'backup'
|
|
|
|
|
| 'import';
|
|
|
|
|
|
|
|
|
|
// Load settings and blacklist in parallel
|
|
|
|
|
const NAV_ITEMS: Array<{ id: SettingsSection; icon: string; label: string; group: 'device' | 'vault' }> = [
|
|
|
|
|
{ id: 'autofill', icon: '⊙', label: 'Autofill', group: 'device' },
|
|
|
|
|
{ id: 'display', icon: '◈', label: 'Display', group: 'device' },
|
|
|
|
|
{ id: 'security', icon: '◉', label: 'Security', group: 'vault' },
|
|
|
|
|
{ id: 'generator', icon: '↻', label: 'Generator', group: 'vault' },
|
|
|
|
|
{ id: 'retention', icon: '▦', label: 'Retention', group: 'vault' },
|
|
|
|
|
{ id: 'backup', icon: '⤓', label: 'Backup', group: 'vault' },
|
|
|
|
|
{ id: 'import', icon: '≡', label: 'Import', group: 'vault' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let activeSection: SettingsSection = 'autofill';
|
|
|
|
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
|
|
|
let pendingVaultSettings: VaultSettings | null = null;
|
|
|
|
|
let sessionHandle: number | null = null;
|
|
|
|
|
|
|
|
|
|
export async function renderSettings(container: HTMLElement): Promise<void> {
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="settings-layout">
|
|
|
|
|
<nav class="settings-nav" id="settings-nav">
|
|
|
|
|
<div class="settings-nav__group-label">Device</div>
|
|
|
|
|
${NAV_ITEMS.filter(n => n.group === 'device').map(navItemHtml).join('')}
|
|
|
|
|
<div class="settings-nav__group-label">Vault</div>
|
|
|
|
|
${NAV_ITEMS.filter(n => n.group === 'vault').map(navItemHtml).join('')}
|
|
|
|
|
</nav>
|
|
|
|
|
<div class="settings-content" id="settings-content"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const unlockedResp = await sendMessage({ type: 'is_unlocked' });
|
|
|
|
|
sessionHandle = (unlockedResp.ok && unlockedResp.data && (unlockedResp.data as { unlocked: boolean }).unlocked) ? 1 : null;
|
|
|
|
|
|
|
|
|
|
wireNav();
|
|
|
|
|
await renderSection(activeSection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function teardownSettings(): void {
|
|
|
|
|
closeGeneratorPanel();
|
|
|
|
|
teardownSecuritySection();
|
|
|
|
|
if (activeKeyHandler) {
|
|
|
|
|
document.removeEventListener('keydown', activeKeyHandler);
|
|
|
|
|
activeKeyHandler = null;
|
|
|
|
|
}
|
|
|
|
|
pendingVaultSettings = null;
|
|
|
|
|
sessionHandle = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navItemHtml(item: (typeof NAV_ITEMS)[0]): string {
|
|
|
|
|
const active = item.id === activeSection ? ' settings-nav__item--active' : '';
|
|
|
|
|
return `
|
|
|
|
|
<button class="settings-nav__item${active}" data-section="${item.id}">
|
|
|
|
|
<span class="settings-nav__icon" aria-hidden="true">${item.icon}</span>
|
|
|
|
|
<span class="settings-nav__label">${escapeHtml(item.label)}</span>
|
|
|
|
|
</button>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wireNav(): void {
|
|
|
|
|
document.getElementById('settings-nav')?.querySelectorAll<HTMLButtonElement>('[data-section]')
|
|
|
|
|
.forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', async () => {
|
|
|
|
|
teardownSecuritySection();
|
|
|
|
|
closeGeneratorPanel();
|
|
|
|
|
activeSection = btn.dataset.section as SettingsSection;
|
|
|
|
|
document.querySelectorAll('.settings-nav__item').forEach(b => b.classList.remove('settings-nav__item--active'));
|
|
|
|
|
btn.classList.add('settings-nav__item--active');
|
|
|
|
|
await renderSection(activeSection);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renderSection(section: SettingsSection): Promise<void> {
|
|
|
|
|
const content = document.getElementById('settings-content');
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
|
|
|
|
switch (section) {
|
|
|
|
|
case 'autofill': return renderAutofillSection(content);
|
|
|
|
|
case 'display': return renderDisplaySection(content);
|
|
|
|
|
case 'security': return renderSecuritySection(content, sessionHandle);
|
|
|
|
|
case 'generator': return renderGeneratorSection(content);
|
|
|
|
|
case 'retention': return renderRetentionSection(content);
|
|
|
|
|
case 'backup': return renderBackupSection(content);
|
|
|
|
|
case 'import': return renderImportSection(content);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Section stubs (filled in by Tasks 3-9) ---
|
|
|
|
|
|
|
|
|
|
async function renderAutofillSection(content: HTMLElement): Promise<void> {
|
|
|
|
|
const [settingsResp, blacklistResp] = await Promise.all([
|
|
|
|
|
sendMessage({ type: 'get_settings' }),
|
|
|
|
|
sendMessage({ type: 'get_blacklist' }),
|
|
|
|
|
@@ -26,171 +119,314 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
|
|
|
|
? (blacklistResp.data as { blacklist: string[] }).blacklist
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const blacklistHtml = blacklist.length > 0
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Capture</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Auto-detect logins</div>
|
|
|
|
|
<div class="setting-row__desc">Show a prompt when a login form is detected.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<input type="checkbox" id="capture-enabled" ${settings.captureEnabled ? 'checked' : ''}>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Prompt style</div>
|
|
|
|
|
<div class="setting-row__desc">How to prompt when a login is detected.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control" style="display:flex; gap:6px;">
|
|
|
|
|
<button class="btn ${settings.captureStyle === 'bar' ? 'btn-active' : ''}" id="style-bar" style="font-size:11px;">bar</button>
|
|
|
|
|
<button class="btn ${settings.captureStyle === 'toast' ? 'btn-active' : ''}" id="style-toast" style="font-size:11px;">toast</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h3 class="settings-section-title" style="margin-top:20px;">Blocked sites</h3>
|
|
|
|
|
<div id="blacklist-container">
|
|
|
|
|
${blacklist.length > 0
|
|
|
|
|
? blacklist.map((h) => `
|
|
|
|
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:4px 0; border-bottom:1px solid #21262d;">
|
|
|
|
|
<span style="font-size:12px; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(h)}</span>
|
|
|
|
|
<button class="relicario-remove-bl" data-hostname="${escapeHtml(h)}" style="
|
|
|
|
|
background:transparent; color:#ab2b20; border:none; cursor:pointer;
|
|
|
|
|
font-size:11px; padding:2px 6px;
|
|
|
|
|
">remove</button>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">${escapeHtml(h)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn remove-bl" data-hostname="${escapeHtml(h)}" style="font-size:11px;">remove</button>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')
|
|
|
|
|
: '<p class="muted" style="font-size:12px;">no blacklisted sites</p>';
|
|
|
|
|
|
|
|
|
|
app.innerHTML = `
|
|
|
|
|
<div class="pad" style="padding-top:12px;">
|
|
|
|
|
<div style="display:flex; align-items:center; margin-bottom:16px;">
|
|
|
|
|
<button id="settings-back" class="btn" style="font-size:11px; margin-right:8px;">←</button>
|
|
|
|
|
<span style="font-size:14px; font-weight:600;">settings</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:16px;">
|
|
|
|
|
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px;">
|
|
|
|
|
<input type="checkbox" id="capture-enabled" ${settings.captureEnabled ? 'checked' : ''}>
|
|
|
|
|
auto-detect logins
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:16px;">
|
|
|
|
|
<div style="font-size:12px; color:#8b949e; margin-bottom:6px;">prompt style</div>
|
|
|
|
|
<div style="display:flex; gap:8px;">
|
|
|
|
|
<button id="style-bar" class="btn" style="font-size:11px; ${settings.captureStyle === 'bar' ? 'background:#7c5719; color:#fff;' : ''}">bar</button>
|
|
|
|
|
<button id="style-toast" class="btn" style="font-size:11px; ${settings.captureStyle === 'toast' ? 'background:#7c5719; color:#fff;' : ''}">toast</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:16px;">
|
|
|
|
|
<button class="btn" id="trash-btn" style="width:100%;margin-bottom:8px;">${GLYPH_TRASH} trash</button>
|
|
|
|
|
<button class="btn" id="devices-btn" style="width:100%;margin-bottom:8px;">${GLYPH_DEVICES} devices</button>
|
|
|
|
|
<button class="btn" id="sync-now-btn" style="width:100%;margin-bottom:8px;">${GLYPH_SYNC} Sync now</button>
|
|
|
|
|
<div id="sync-status" class="muted" style="font-size:12px;min-height:16px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:16px;" id="display-section-container">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-size:12px; color:#8b949e; margin-bottom:6px;">blacklisted sites</div>
|
|
|
|
|
<div id="blacklist-container">
|
|
|
|
|
${blacklistHtml}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
: '<p class="muted" style="font-size:12px; padding:8px 0;">No blocked sites.</p>'}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Back button
|
|
|
|
|
document.getElementById('settings-back')?.addEventListener('click', () => {
|
|
|
|
|
navigate('locked');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Navigation buttons
|
|
|
|
|
document.getElementById('trash-btn')?.addEventListener('click', () => navigate('trash'));
|
|
|
|
|
document.getElementById('devices-btn')?.addEventListener('click', () => navigate('devices'));
|
|
|
|
|
|
|
|
|
|
// Sync now button
|
|
|
|
|
document.getElementById('sync-now-btn')?.addEventListener('click', async () => {
|
|
|
|
|
const btn = document.getElementById('sync-now-btn') as HTMLButtonElement | null;
|
|
|
|
|
const status = document.getElementById('sync-status');
|
|
|
|
|
if (!btn || !status) return;
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
status.textContent = 'syncing...';
|
|
|
|
|
const result = await sendMessage({ type: 'sync' });
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
status.textContent = result.ok ? 'synced ✓' : `sync failed: ${result.error}`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Capture enabled toggle
|
|
|
|
|
document.getElementById('capture-enabled')?.addEventListener('change', async (e) => {
|
|
|
|
|
const checked = (e.target as HTMLInputElement).checked;
|
|
|
|
|
await sendMessage({ type: 'update_settings', settings: { captureEnabled: checked } });
|
|
|
|
|
const enabled = (e.target as HTMLInputElement).checked;
|
|
|
|
|
await sendMessage({ type: 'update_settings', settings: { captureEnabled: enabled } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Style buttons
|
|
|
|
|
document.getElementById('style-bar')?.addEventListener('click', async () => {
|
|
|
|
|
await sendMessage({ type: 'update_settings', settings: { captureStyle: 'bar' } });
|
|
|
|
|
renderSettings(app);
|
|
|
|
|
renderAutofillSection(content);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('style-toast')?.addEventListener('click', async () => {
|
|
|
|
|
await sendMessage({ type: 'update_settings', settings: { captureStyle: 'toast' } });
|
|
|
|
|
renderSettings(app);
|
|
|
|
|
renderAutofillSection(content);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Blacklist remove buttons
|
|
|
|
|
document.querySelectorAll('.relicario-remove-bl').forEach((btn) => {
|
|
|
|
|
content.querySelectorAll<HTMLButtonElement>('.remove-bl').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', async () => {
|
|
|
|
|
const hostname = (btn as HTMLElement).dataset.hostname;
|
|
|
|
|
if (hostname) {
|
|
|
|
|
await sendMessage({ type: 'remove_blacklist', hostname });
|
|
|
|
|
renderSettings(app);
|
|
|
|
|
}
|
|
|
|
|
const host = btn.dataset.hostname;
|
|
|
|
|
if (!host) return;
|
|
|
|
|
await sendMessage({ type: 'remove_blacklist', hostname: host });
|
|
|
|
|
renderAutofillSection(content);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Render Display section after the rest of the DOM is ready
|
|
|
|
|
await renderDisplaySection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateSwatch(swatch: HTMLElement, digitColor: string, symbolColor: string): void {
|
|
|
|
|
swatch.style.setProperty('--relicario-pwd-digit-color', digitColor);
|
|
|
|
|
swatch.style.setProperty('--relicario-pwd-symbol-color', symbolColor);
|
|
|
|
|
swatch.innerHTML = '';
|
|
|
|
|
swatch.appendChild(colorizePassword('Abc123!@#xyz'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renderDisplaySection(): Promise<void> {
|
|
|
|
|
// The Display section container must be present in the DOM before we call this
|
|
|
|
|
const container = document.getElementById('display-section-container');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
async function renderDisplaySection(content: HTMLElement): Promise<void> {
|
|
|
|
|
const scheme = await loadColorScheme();
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div style="font-size:12px; color:#8b949e; margin-bottom:6px;">display</div>
|
|
|
|
|
<div style="margin-bottom:8px;">
|
|
|
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:13px;">
|
|
|
|
|
<input type="color" id="display-digit-color" value="${escapeHtml(scheme.digit_color)}">
|
|
|
|
|
digit color
|
|
|
|
|
</label>
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Password coloring</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Digit color</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-bottom:8px;">
|
|
|
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:13px;">
|
|
|
|
|
<input type="color" id="display-symbol-color" value="${escapeHtml(scheme.symbol_color)}">
|
|
|
|
|
symbol color
|
|
|
|
|
</label>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<input type="color" id="digit-color" value="${escapeHtml(scheme.digit_color)}">
|
|
|
|
|
</div>
|
|
|
|
|
<div id="display-swatch" class="color-preview-swatch"></div>
|
|
|
|
|
<div style="margin-top:8px;">
|
|
|
|
|
<button id="display-reset" class="btn" style="font-size:11px;">reset to defaults</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Symbol color</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<input type="color" id="symbol-color" value="${escapeHtml(scheme.symbol_color)}">
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Preview</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<span id="color-preview" style="font-family:monospace; font-size:13px;"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top:12px;">
|
|
|
|
|
<button class="btn" id="reset-colors" style="font-size:11px;">Reset defaults</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const digitInput = document.getElementById('display-digit-color') as HTMLInputElement;
|
|
|
|
|
const symbolInput = document.getElementById('display-symbol-color') as HTMLInputElement;
|
|
|
|
|
const swatch = document.getElementById('display-swatch') as HTMLElement;
|
|
|
|
|
|
|
|
|
|
// Render initial swatch
|
|
|
|
|
updateSwatch(swatch, scheme.digit_color, scheme.symbol_color);
|
|
|
|
|
|
|
|
|
|
async function onColorChange(): Promise<void> {
|
|
|
|
|
const newScheme = { digit_color: digitInput.value, symbol_color: symbolInput.value };
|
|
|
|
|
await saveColorScheme(newScheme);
|
|
|
|
|
updateSwatch(swatch, newScheme.digit_color, newScheme.symbol_color);
|
|
|
|
|
function refreshPreview(s: ColorScheme): void {
|
|
|
|
|
const preview = document.getElementById('color-preview');
|
|
|
|
|
if (!preview) return;
|
|
|
|
|
preview.style.setProperty('--relicario-pwd-digit-color', s.digit_color);
|
|
|
|
|
preview.style.setProperty('--relicario-pwd-symbol-color', s.symbol_color);
|
|
|
|
|
preview.innerHTML = '';
|
|
|
|
|
preview.appendChild(colorizePassword('Abc123!@#'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
digitInput.addEventListener('change', () => void onColorChange());
|
|
|
|
|
symbolInput.addEventListener('change', () => void onColorChange());
|
|
|
|
|
refreshPreview(scheme);
|
|
|
|
|
|
|
|
|
|
document.getElementById('display-reset')?.addEventListener('click', async () => {
|
|
|
|
|
document.getElementById('digit-color')?.addEventListener('change', async (e) => {
|
|
|
|
|
const color = (e.target as HTMLInputElement).value;
|
|
|
|
|
const current = await loadColorScheme();
|
|
|
|
|
await saveColorScheme({ ...current, digit_color: color });
|
|
|
|
|
refreshPreview({ ...current, digit_color: color });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('symbol-color')?.addEventListener('change', async (e) => {
|
|
|
|
|
const color = (e.target as HTMLInputElement).value;
|
|
|
|
|
const current = await loadColorScheme();
|
|
|
|
|
await saveColorScheme({ ...current, symbol_color: color });
|
|
|
|
|
refreshPreview({ ...current, symbol_color: color });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('reset-colors')?.addEventListener('click', async () => {
|
|
|
|
|
await resetColorScheme();
|
|
|
|
|
digitInput.value = DEFAULT_DIGIT_COLOR;
|
|
|
|
|
symbolInput.value = DEFAULT_SYMBOL_COLOR;
|
|
|
|
|
updateSwatch(swatch, DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR);
|
|
|
|
|
renderDisplaySection(content);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DEV-B interface contract stub — will be replaced with real teardown logic at merge time
|
|
|
|
|
export function teardownSettings(): void {
|
|
|
|
|
// no-op stub
|
|
|
|
|
async function renderGeneratorSection(content: HTMLElement): Promise<void> {
|
|
|
|
|
content.innerHTML = '<p class="muted" style="padding:20px;font-size:12px;">Loading…</p>';
|
|
|
|
|
const resp = await sendMessage({ type: 'get_vault_settings' });
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
const errorMsg = (resp as { ok: false; error: string }).error;
|
|
|
|
|
content.innerHTML = `<p class="muted" style="padding:20px;">Failed to load: ${escapeHtml(errorMsg)}</p>`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const settings = (resp.data as { settings: VaultSettings }).settings;
|
|
|
|
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Generator defaults</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Configure generator</div>
|
|
|
|
|
<div class="setting-row__desc">Password length, character classes, passphrase word count.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<button class="btn" id="open-generator-panel" style="font-size:11px;">Configure ▸</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.getElementById('open-generator-panel')?.addEventListener('click', (e) => {
|
|
|
|
|
const trigger = e.currentTarget as HTMLElement;
|
|
|
|
|
if (isGeneratorPanelOpen()) {
|
|
|
|
|
closeGeneratorPanel();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
openGeneratorPanel({
|
|
|
|
|
parent: content,
|
|
|
|
|
trigger,
|
|
|
|
|
initial: settings.generator_defaults,
|
|
|
|
|
context: 'configure-defaults',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function renderRetentionSection(content: HTMLElement): Promise<void> {
|
|
|
|
|
content.innerHTML = '<p class="muted" style="padding:20px;font-size:12px;">Loading…</p>';
|
|
|
|
|
const resp = await sendMessage({ type: 'get_vault_settings' });
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
content.innerHTML = `<p class="muted" style="padding:20px;">Failed to load: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const settings = (resp.data as { settings: VaultSettings }).settings;
|
|
|
|
|
pendingVaultSettings = { ...settings };
|
|
|
|
|
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Trash retention</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Keep deleted items for</div>
|
|
|
|
|
<div class="setting-row__desc">Items in trash older than this are permanently deleted on the next sync.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<select id="trash-retention" style="font-size:12px;">
|
|
|
|
|
<option value="days:7">7 days</option>
|
|
|
|
|
<option value="days:30">30 days</option>
|
|
|
|
|
<option value="days:90">90 days</option>
|
|
|
|
|
<option value="forever">Forever</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h3 class="settings-section-title" style="margin-top:20px;">Field history retention</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Keep password history for</div>
|
|
|
|
|
<div class="setting-row__desc">History entries older than this are pruned on save.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<select id="history-retention" style="font-size:12px;">
|
|
|
|
|
<option value="last_n:5">Last 5</option>
|
|
|
|
|
<option value="last_n:10">Last 10</option>
|
|
|
|
|
<option value="days:90">90 days</option>
|
|
|
|
|
<option value="days:365">1 year</option>
|
|
|
|
|
<option value="forever">Forever</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top:12px;">
|
|
|
|
|
<button class="btn btn-primary" id="save-retention" style="font-size:11px;">Save retention settings</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Set current select values
|
|
|
|
|
(document.getElementById('trash-retention') as HTMLSelectElement).value =
|
|
|
|
|
trashRetentionToValue(settings.trash_retention);
|
|
|
|
|
(document.getElementById('history-retention') as HTMLSelectElement).value =
|
|
|
|
|
historyRetentionToValue(settings.field_history_retention);
|
|
|
|
|
|
|
|
|
|
document.getElementById('trash-retention')?.addEventListener('change', (e) => {
|
|
|
|
|
if (pendingVaultSettings) {
|
|
|
|
|
pendingVaultSettings.trash_retention = valueToTrashRetention((e.target as HTMLSelectElement).value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('history-retention')?.addEventListener('change', (e) => {
|
|
|
|
|
if (pendingVaultSettings) {
|
|
|
|
|
pendingVaultSettings.field_history_retention = valueToHistoryRetention((e.target as HTMLSelectElement).value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('save-retention')?.addEventListener('click', async () => {
|
|
|
|
|
if (!pendingVaultSettings) return;
|
|
|
|
|
const r = await sendMessage({ type: 'update_vault_settings', settings: pendingVaultSettings });
|
|
|
|
|
if (!r.ok) alert(`Save failed: ${r.error}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderBackupSection(content: HTMLElement): void {
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Backup & restore</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Export & restore backup</div>
|
|
|
|
|
<div class="setting-row__desc">Download an encrypted backup or restore from a file. Opens in the vault tab.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<button class="btn" id="open-backup-tab" style="font-size:11px;">Open backup ▸</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
document.getElementById('open-backup-tab')?.addEventListener('click', () => openVaultTab('backup'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderImportSection(content: HTMLElement): void {
|
|
|
|
|
content.innerHTML = `
|
|
|
|
|
<h3 class="settings-section-title">Import</h3>
|
|
|
|
|
<div class="setting-row">
|
|
|
|
|
<div class="setting-row__info">
|
|
|
|
|
<div class="setting-row__title">Import from LastPass</div>
|
|
|
|
|
<div class="setting-row__desc">Import a LastPass CSV export. Opens in the vault tab for review before committing.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="setting-row__control">
|
|
|
|
|
<button class="btn" id="open-import-tab" style="font-size:11px;">Open import ▸</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
document.getElementById('open-import-tab')?.addEventListener('click', () => openVaultTab('import'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { renderAutofillSection, renderDisplaySection, renderGeneratorSection,
|
|
|
|
|
renderRetentionSection, renderBackupSection, renderImportSection };
|
|
|
|
|
|
|
|
|
|
// Suppress unused-import warnings — these are used by Tasks 3-9
|
|
|
|
|
void sendMessage;
|
|
|
|
|
void loadColorScheme;
|
|
|
|
|
void saveColorScheme;
|
|
|
|
|
void resetColorScheme;
|
|
|
|
|
void DEFAULT_DIGIT_COLOR;
|
|
|
|
|
void DEFAULT_SYMBOL_COLOR;
|
|
|
|
|
void colorizePassword;
|
|
|
|
|
void openGeneratorPanel;
|
|
|
|
|
void pendingVaultSettings;
|
|
|
|
|
void activeKeyHandler;
|
|
|
|
|
|