import { useState, useEffect } from 'react'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Button } from '../ui/button'; import { Separator } from '../ui/separator'; import { toast } from '../ui/sonner'; import { api } from '../../api'; import { formatTime } from '../../utils/messageParser'; import { captureLastViewedConversationFromHash, getReopenLastConversationEnabled, setReopenLastConversationEnabled, } from '../../utils/lastViewedConversation'; import { getLocalLabel, setLocalLabel, type LocalLabel } from '../../utils/localLabel'; import type { AppSettings, AppSettingsUpdate, HealthStatus } from '../../types'; export function SettingsDatabaseSection({ appSettings, health, onSaveAppSettings, onHealthRefresh, onLocalLabelChange, className, }: { appSettings: AppSettings; health: HealthStatus | null; onSaveAppSettings: (update: AppSettingsUpdate) => Promise; onHealthRefresh: () => Promise; onLocalLabelChange?: (label: LocalLabel) => void; className?: string; }) { const [retentionDays, setRetentionDays] = useState('14'); const [cleaning, setCleaning] = useState(false); const [purgingDecryptedRaw, setPurgingDecryptedRaw] = useState(false); const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false); const [reopenLastConversation, setReopenLastConversation] = useState( getReopenLastConversationEnabled ); const [localLabelText, setLocalLabelText] = useState(() => getLocalLabel().text); const [localLabelColor, setLocalLabelColor] = useState(() => getLocalLabel().color); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => { setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert); }, [appSettings]); useEffect(() => { setReopenLastConversation(getReopenLastConversationEnabled()); }, []); const handleCleanup = async () => { const days = parseInt(retentionDays, 10); if (isNaN(days) || days < 1) { toast.error('Invalid retention days', { description: 'Retention days must be at least 1', }); return; } setCleaning(true); try { const result = await api.runMaintenance({ pruneUndecryptedDays: days }); toast.success('Database cleanup complete', { description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`, }); await onHealthRefresh(); } catch (err) { console.error('Failed to run maintenance:', err); toast.error('Database cleanup failed', { description: err instanceof Error ? err.message : 'Unknown error', }); } finally { setCleaning(false); } }; const handlePurgeDecryptedRawPackets = async () => { setPurgingDecryptedRaw(true); try { const result = await api.runMaintenance({ purgeLinkedRawPackets: true }); toast.success('Decrypted raw packets purged', { description: `Deleted ${result.packets_deleted} raw packet${result.packets_deleted === 1 ? '' : 's'}`, }); await onHealthRefresh(); } catch (err) { console.error('Failed to purge decrypted raw packets:', err); toast.error('Failed to purge decrypted raw packets', { description: err instanceof Error ? err.message : 'Unknown error', }); } finally { setPurgingDecryptedRaw(false); } }; const handleSave = async () => { setBusy(true); setError(null); try { await onSaveAppSettings({ auto_decrypt_dm_on_advert: autoDecryptOnAdvert }); toast.success('Database settings saved'); } catch (err) { console.error('Failed to save database settings:', err); setError(err instanceof Error ? err.message : 'Failed to save'); toast.error('Failed to save settings'); } finally { setBusy(false); } }; const handleToggleReopenLastConversation = (enabled: boolean) => { setReopenLastConversation(enabled); setReopenLastConversationEnabled(enabled); if (enabled) { captureLastViewedConversationFromHash(); } }; return (
Database size {health?.database_size_mb ?? '?'} MB
{health?.oldest_undecrypted_timestamp ? (
Oldest undecrypted packet {formatTime(health.oldest_undecrypted_timestamp)} ({Math.floor((Date.now() / 1000 - health.oldest_undecrypted_timestamp) / 86400)}{' '} days old)
) : (
Oldest undecrypted packet None
)}

Permanently deletes stored raw packets containing DMs and channel messages that have not yet been decrypted. These packets are retained in case you later obtain the correct key — once deleted, these messages can never be recovered or decrypted.

setRetentionDays(e.target.value)} className="w-24" />

Deletes archival copies of raw packet bytes for messages that are already decrypted and visible in your chat history.{' '} This will not affect any displayed messages or app functionality. {' '} The raw bytes are only useful for manual packet analysis.

When enabled, the server will automatically try to decrypt stored DM packets when a new contact sends an advertisement. This may cause brief delays on large packet backlogs.

This applies only to this device/browser. It does not sync to server settings.

{ const text = e.target.value; setLocalLabelText(text); setLocalLabel(text, localLabelColor); onLocalLabelChange?.({ text, color: localLabelColor }); }} placeholder="e.g. Home Base, Field Radio 2" className="flex-1" /> { const color = e.target.value; setLocalLabelColor(color); setLocalLabel(localLabelText, color); onLocalLabelChange?.({ text: localLabelText, color }); }} className="w-10 h-9 rounded border border-input cursor-pointer bg-transparent p-0.5" />

Display a colored banner at the top of the page to identify this instance. This applies only to this device/browser.

{error &&
{error}
}
); }