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 { BulkDeleteContactsModal } from './BulkDeleteContactsModal'; import type { AppSettings, AppSettingsUpdate, Contact, HealthStatus } from '../../types'; export function SettingsDatabaseSection({ appSettings, health, onSaveAppSettings, onHealthRefresh, blockedKeys = [], blockedNames = [], onToggleBlockedKey, onToggleBlockedName, contacts = [], onBulkDeleteContacts, trackedTelemetryRepeaters = [], onToggleTrackedTelemetry, className, }: { appSettings: AppSettings; health: HealthStatus | null; onSaveAppSettings: (update: AppSettingsUpdate) => Promise; onHealthRefresh: () => Promise; blockedKeys?: string[]; blockedNames?: string[]; onToggleBlockedKey?: (key: string) => void; onToggleBlockedName?: (name: string) => void; contacts?: Contact[]; onBulkDeleteContacts?: (deletedKeys: string[]) => void; trackedTelemetryRepeaters?: string[]; onToggleTrackedTelemetry?: (publicKey: string) => Promise; className?: string; }) { const [retentionDays, setRetentionDays] = useState('14'); const [cleaning, setCleaning] = useState(false); const [purgingDecryptedRaw, setPurgingDecryptedRaw] = useState(false); const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false); const [discoveryBlockedTypes, setDiscoveryBlockedTypes] = useState([]); const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => { setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert); setDiscoveryBlockedTypes(appSettings.discovery_blocked_types ?? []); }, [appSettings]); 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 { const update: AppSettingsUpdate = { auto_decrypt_dm_on_advert: autoDecryptOnAdvert }; const currentBlocked = appSettings.discovery_blocked_types ?? []; if ( discoveryBlockedTypes.length !== currentBlocked.length || discoveryBlockedTypes.some((t) => !currentBlocked.includes(t)) ) { update.discovery_blocked_types = discoveryBlockedTypes; } await onSaveAppSettings(update); 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); } }; return (
{/* ── Database Overview ── */}
Database size {health?.database_size_mb ?? '?'} MB
Oldest undecrypted packet {health?.oldest_undecrypted_timestamp ? ( {formatTime(health.oldest_undecrypted_timestamp)} ({Math.floor((Date.now() / 1000 - health.oldest_undecrypted_timestamp) / 86400)}{' '} days) ) : ( None )}
{/* ── Storage Cleanup ── */}

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

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

Deletes the raw packet bytes behind messages that are already decrypted and visible in chat. This frees space but removes packet-analysis availability for those messages. It does not affect displayed messages or future decryption.

{/* ── DM Decryption ── */}

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.

{/* ── Tracked Repeater Telemetry ── */}

Repeaters opted into automatic telemetry collection are polled every 8 hours. Up to 8 repeaters may be tracked at a time ({trackedTelemetryRepeaters.length} / 8 slots used).

{trackedTelemetryRepeaters.length === 0 ? (

No repeaters are being tracked. Enable tracking from a repeater's dashboard.

) : (
{trackedTelemetryRepeaters.map((key) => { const contact = contacts.find((c) => c.public_key === key); const displayName = contact?.name ?? key.slice(0, 12); return (
{displayName} {key.slice(0, 12)}
{onToggleTrackedTelemetry && ( )}
); })}
)}
{error && (
{error}
)} {/* ── Contact Management ── */}
{/* Block discovery of new node types */}

Checked types will be ignored when heard via advertisement. Existing contacts of these types are still updated. This does not affect contacts added manually or via DM.

{( [ [1, 'Block clients'], [2, 'Block repeaters'], [3, 'Block room servers'], [4, 'Block sensors'], ] as const ).map(([typeCode, label]) => { const checked = discoveryBlockedTypes.includes(typeCode); return ( ); })}
{discoveryBlockedTypes.length > 0 && (

New{' '} {discoveryBlockedTypes .map((t) => t === 1 ? 'clients' : t === 2 ? 'repeaters' : t === 3 ? 'room servers' : 'sensors' ) .join(', ')}{' '} heard via advertisement will not be added to your contact list.

)}
{/* Blocked contacts list */}

Blocked contacts are hidden from the sidebar. Blocking only hides messages from the UI — MQTT forwarding and bot responses are not affected. Messages are still stored and will reappear if unblocked.

{blockedKeys.length === 0 && blockedNames.length === 0 ? (

No blocked contacts. Block contacts from their info pane, viewed by clicking their avatar in any channel, or their name within the top status bar with the conversation open.

) : (
{blockedKeys.length > 0 && (
Blocked Keys
{blockedKeys.map((key) => (
{key} {onToggleBlockedKey && ( )}
))}
)} {blockedNames.length > 0 && (
Blocked Names
{blockedNames.map((name) => (
{name} {onToggleBlockedName && ( )}
))}
)}
)}
{/* Bulk delete */}

Remove multiple contacts or repeaters at once. Useful for cleaning up spam or unwanted nodes. Message history will be preserved.

setBulkDeleteOpen(false)} contacts={contacts} onDeleted={(keys) => onBulkDeleteContacts?.(keys)} />
); }