// Options page: transport & pairing, the daemon's capture policy (mirrored, editable // only over the privileged uds transport), min size / exclusions / bypass modifier // (same privileged path), and the one setting that is genuinely the extension's own — // the default category for extension-initiated downloads (prefs.ts). docs/05 §3. import { PanelClient } from '../shared/panel-client.js'; import type { PanelStatus } from '../background/bridge.js'; import type { Category, CaptureRules } from '../shared/protocol/index.js'; import { getDefaultCategoryId, setDefaultCategoryId } from './prefs.js'; import { captureRulesEditable, pairingAvailable, statusLine, unpairAvailable } from './view.js'; const client = new PanelClient(); const $transportSelect = document.querySelector('#transport-override')!; const $statusText = document.querySelector('#transport-status')!; const $pairCode = document.querySelector('#pair-code')!; const $pairButton = document.querySelector('#pair-button')!; const $unpairButton = document.querySelector('#unpair-button')!; const $pairMessage = document.querySelector('#pair-message')!; const $rulesSummary = document.querySelector('#rules-summary')!; const $rulesForm = document.querySelector('#rules-form')!; const $rulesLocked = document.querySelector('#rules-locked')!; const $minSize = document.querySelector('#min-size')!; const $excludedHosts = document.querySelector('#excluded-hosts')!; const $bypassModifier = document.querySelector('#bypass-modifier')!; const $defaultCategory = document.querySelector('#default-category')!; let latestRules: CaptureRules | null = null; function renderStatus(status: PanelStatus): void { $transportSelect.value = status.kind ?? 'auto'; $statusText.textContent = statusLine(status); $pairButton.disabled = !pairingAvailable(status); $pairCode.disabled = !pairingAvailable(status); $unpairButton.disabled = !unpairAvailable(status); const canEdit = captureRulesEditable(status); $rulesForm.hidden = !canEdit; $rulesLocked.hidden = canEdit; } function renderRulesSummary(rules: CaptureRules): void { latestRules = rules; $rulesSummary.textContent = rules.enabled ? `Capturing ${rules.monitoredExtensions.length} extension(s) and ${rules.monitoredMimeTypes.length} MIME type(s), min size ${rules.minSizeBytes} bytes.` : 'Capture is disabled on the daemon.'; $minSize.value = String(rules.minSizeBytes); $excludedHosts.value = rules.excludedHosts.join('\n'); $bypassModifier.value = rules.bypassModifier ?? 'alt'; } async function refreshRules(): Promise { try { const rules = await client.call('capture.getRules', {}); renderRulesSummary(rules); } catch { $rulesSummary.textContent = 'Could not read the daemon’s capture policy.'; } } function makeOption(value: string, label: string): HTMLOptionElement { const opt = document.createElement('option'); opt.value = value; opt.textContent = label; return opt; } async function refreshCategories(): Promise { try { const { items } = await client.call('category.list', {}); const current = await getDefaultCategoryId(); $defaultCategory.replaceChildren( makeOption('', '(none — daemon default)'), ...items.map((c: Category) => makeOption(c.categoryId, c.name)), ); $defaultCategory.value = current ?? ''; } catch { $defaultCategory.replaceChildren(makeOption('', '(unavailable — not connected)')); } } $transportSelect.addEventListener('change', () => { client.setOverride($transportSelect.value as 'auto' | 'ws' | 'uds'); }); $pairButton.addEventListener('click', () => { const code = $pairCode.value.trim(); if (!code) return; $pairMessage.textContent = 'Pairing…'; client.pair(code); }); $unpairButton.addEventListener('click', () => { client.unpair(); $pairMessage.textContent = 'Unpaired.'; }); $defaultCategory.addEventListener('change', () => { void setDefaultCategoryId($defaultCategory.value || null); }); $rulesForm.addEventListener('submit', (e) => { e.preventDefault(); if (!latestRules) return; const values = { 'capture.minSizeBytes': Number($minSize.value) || 0, 'capture.excludedHosts': $excludedHosts.value .split('\n') .map((s) => s.trim()) .filter(Boolean), 'capture.bypassModifier': $bypassModifier.value as CaptureRules['bypassModifier'], }; client .call('settings.set', { values }) .then(() => refreshRules()) .catch(() => { $rulesSummary.textContent = 'Failed to save — is Velox still connected via native messaging?'; }); }); async function start(): Promise { client.onStatus(renderStatus); client.onPairError((err) => { $pairMessage.textContent = `Pairing failed: ${err.message}`; }); client.on('event.settings.changed', (payload) => { if (payload.keys.some((k) => k.startsWith('capture.'))) void refreshRules(); }); await Promise.all([refreshRules(), refreshCategories()]); } void start();