"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, Badge } from "@/shared/components"; import { useLocale, useTranslations } from "next-intl"; import DatabaseBackupRetentionCard from "./DatabaseBackupRetentionCard"; export default function SystemStorageTab() { const [backups, setBackups] = useState([]); const [backupsLoading, setBackupsLoading] = useState(false); const [backupsExpanded, setBackupsExpanded] = useState(false); const [restoreStatus, setRestoreStatus] = useState({ type: "", message: "" }); const [restoringId, setRestoringId] = useState(null); const [confirmRestoreId, setConfirmRestoreId] = useState(null); const [manualBackupLoading, setManualBackupLoading] = useState(false); const [manualBackupStatus, setManualBackupStatus] = useState({ type: "", message: "" }); const [exportLoading, setExportLoading] = useState(false); const [importLoading, setImportLoading] = useState(false); const [importStatus, setImportStatus] = useState({ type: "", message: "" }); const [confirmImport, setConfirmImport] = useState(false); const [pendingImportFile, setPendingImportFile] = useState(null); const [clearCacheLoading, setClearCacheLoading] = useState(false); const [clearCacheStatus, setClearCacheStatus] = useState({ type: "", message: "" }); const [purgeLogsLoading, setPurgeLogsLoading] = useState(false); const [purgeLogsStatus, setPurgeLogsStatus] = useState({ type: "", message: "" }); const [manualVacuumLoading, setManualVacuumLoading] = useState(false); const [manualVacuumStatus, setManualVacuumStatus] = useState({ type: "", message: "" }); const [cleanupBackupsLoading, setCleanupBackupsLoading] = useState(false); const [cleanupBackupsStatus, setCleanupBackupsStatus] = useState({ type: "", message: "" }); const [saveBackupRetentionLoading, setSaveBackupRetentionLoading] = useState(false); const [backupRetentionStatus, setBackupRetentionStatus] = useState({ type: "", message: "" }); const [purgeQuotaSnapshotsLoading, setPurgeQuotaSnapshotsLoading] = useState(false); const [purgeQuotaSnapshotsStatus, setPurgeQuotaSnapshotsStatus] = useState({ type: "", message: "", }); const [purgeCallLogsLoading, setPurgeCallLogsLoading] = useState(false); const [purgeCallLogsStatus, setPurgeCallLogsStatus] = useState({ type: "", message: "" }); const [purgeDetailedLogsLoading, setPurgeDetailedLogsLoading] = useState(false); const [purgeDetailedLogsStatus, setPurgeDetailedLogsStatus] = useState({ type: "", message: "" }); const fileInputRef = useRef(null); const jsonInputRef = useRef(null); const locale = useLocale(); const t = useTranslations("settings"); const tc = useTranslations("common"); const [storageHealth, setStorageHealth] = useState({ driver: "sqlite", dbPath: "~/.omniroute/storage.sqlite", sizeBytes: 0, retentionDays: { app: 7, call: 7, }, tableMaxRows: { callLogs: 100000, proxyLogs: 100000, }, backupCount: 0, backupRetention: { maxFiles: 20, days: 0, }, lastBackupAt: null, }); const [backupCleanupOptions, setBackupCleanupOptions] = useState({ keepLatest: 20, retentionDays: 0, }); // Database settings state (tasks 23-26) const [dbSettings, setDbSettings] = useState(null); const [dbSettingsLoading, setDbSettingsLoading] = useState(true); const [dbSettingsSaving, setDbSettingsSaving] = useState(false); const [dbStatsRefreshing, setDbStatsRefreshing] = useState(false); const loadBackups = async () => { setBackupsLoading(true); try { const res = await fetch("/api/db-backups"); const data = await res.json(); setBackups(data.backups || []); } catch (err) { console.error("Failed to fetch backups:", err); } finally { setBackupsLoading(false); } }; const loadStorageHealth = async () => { try { const res = await fetch("/api/storage/health"); if (!res.ok) return; const data = await res.json(); setStorageHealth((prev) => ({ ...prev, ...data })); setBackupCleanupOptions({ keepLatest: data.backupRetention?.maxFiles || 20, retentionDays: data.backupRetention?.days || 0, }); } catch (err) { console.error("Failed to fetch storage health:", err); } }; const loadDatabaseSettings = async () => { setDbSettingsLoading(true); try { const res = await fetch("/api/settings/database"); if (res.ok) { const data = await res.json(); setDbSettings(data); } } catch (err) { console.error("Failed to load database settings:", err); } finally { setDbSettingsLoading(false); } }; const saveDatabaseSettings = async () => { if (!dbSettings) return; setDbSettingsSaving(true); try { const { logs, backup, cache, retention, aggregation, optimization } = dbSettings; const res = await fetch("/api/settings/database", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ logs, backup, cache, retention, aggregation, optimization }), }); if (res.ok) { await loadDatabaseSettings(); } } catch (err) { console.error("Failed to save database settings:", err); } finally { setDbSettingsSaving(false); } }; const refreshDatabaseStats = async () => { setDbStatsRefreshing(true); try { await fetch("/api/settings/database/refresh-stats", { method: "POST" }); await loadDatabaseSettings(); } catch (err) { console.error("Failed to refresh database stats:", err); } finally { setDbStatsRefreshing(false); } }; const handleSaveBackupRetention = async () => { setSaveBackupRetentionLoading(true); setBackupRetentionStatus({ type: "", message: "" }); try { const res = await fetch("/api/db-backups", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(backupCleanupOptions), }); const data = await res.json(); if (res.ok) { setBackupRetentionStatus({ type: "success", message: "Backup retention saved.", }); await loadStorageHealth(); } else { setBackupRetentionStatus({ type: "error", message: data.error || "Failed to save backup retention", }); } } catch { setBackupRetentionStatus({ type: "error", message: t("errorOccurred") }); } finally { setSaveBackupRetentionLoading(false); } }; const handleCleanupBackups = async () => { setCleanupBackupsLoading(true); setCleanupBackupsStatus({ type: "", message: "" }); try { const res = await fetch("/api/db-backups", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify(backupCleanupOptions), }); const data = await res.json(); if (res.ok) { setCleanupBackupsStatus({ type: "success", message: `Deleted ${data.deletedBackupFamilies} backup set(s) and ${data.deletedFiles} file(s).`, }); await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { setCleanupBackupsStatus({ type: "error", message: data.error || "Failed to clean database backups", }); } } catch { setCleanupBackupsStatus({ type: "error", message: t("errorOccurred") }); } finally { setCleanupBackupsLoading(false); } }; const handleClearCache = async () => { setClearCacheLoading(true); setClearCacheStatus({ type: "", message: "" }); try { const res = await fetch("/api/cache", { method: "DELETE" }); const data = await res.json().catch(() => null); if (res.ok) { setClearCacheStatus({ type: "success", message: t("cacheCleared") || "Cache cleared successfully", }); } else { setClearCacheStatus({ type: "error", message: data?.error || t("clearCacheFailed") || "Failed to clear cache", }); } } catch { setClearCacheStatus({ type: "error", message: t("errorOccurred") }); } finally { setClearCacheLoading(false); } }; const handlePurgeExpiredLogs = async () => { setPurgeLogsLoading(true); setPurgeLogsStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/purge-logs", { method: "POST" }); const data = await res.json().catch(() => null); if (res.ok) { const deleted = data?.deleted ?? 0; setPurgeLogsStatus({ type: "success", message: t("logsDeleted", { count: deleted }) || `Purged ${deleted} expired log(s)`, }); } else { setPurgeLogsStatus({ type: "error", message: data?.error || t("purgeLogsFailed") || "Failed to purge logs", }); } } catch { setPurgeLogsStatus({ type: "error", message: t("errorOccurred") }); } finally { setPurgeLogsLoading(false); } }; const handlePurgeQuotaSnapshots = async () => { setPurgeQuotaSnapshotsLoading(true); setPurgeQuotaSnapshotsStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/purge-quota-snapshots", { method: "POST" }); const data = await res.json(); if (res.ok) { setPurgeQuotaSnapshotsStatus({ type: "success", message: `Purged ${data.deleted} quota snapshots`, }); } else { setPurgeQuotaSnapshotsStatus({ type: "error", message: data.error || "Failed to purge quota snapshots", }); } } catch { setPurgeQuotaSnapshotsStatus({ type: "error", message: t("errorOccurred") }); } finally { setPurgeQuotaSnapshotsLoading(false); } }; const handlePurgeCallLogs = async () => { setPurgeCallLogsLoading(true); setPurgeCallLogsStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/purge-call-logs", { method: "POST" }); const data = await res.json(); if (res.ok) { setPurgeCallLogsStatus({ type: "success", message: `Purged ${data.deleted} call logs`, }); } else { setPurgeCallLogsStatus({ type: "error", message: data.error || "Failed to purge call logs", }); } } catch { setPurgeCallLogsStatus({ type: "error", message: t("errorOccurred") }); } finally { setPurgeCallLogsLoading(false); } }; const handlePurgeDetailedLogs = async () => { setPurgeDetailedLogsLoading(true); setPurgeDetailedLogsStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/purge-detailed-logs", { method: "POST" }); const data = await res.json(); if (res.ok) { setPurgeDetailedLogsStatus({ type: "success", message: `Purged ${data.deleted} detailed logs`, }); } else { setPurgeDetailedLogsStatus({ type: "error", message: data.error || "Failed to purge detailed logs", }); } } catch { setPurgeDetailedLogsStatus({ type: "error", message: t("errorOccurred") }); } finally { setPurgeDetailedLogsLoading(false); } }; const handleManualVacuum = async () => { setManualVacuumLoading(true); setManualVacuumStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/database/vacuum", { method: "POST" }); const data = await res.json().catch(() => null); if (res.ok && data?.success !== false) { setManualVacuumStatus({ type: "success", message: data?.message || "VACUUM completed", }); await loadDatabaseSettings(); await loadStorageHealth(); } else { setManualVacuumStatus({ type: "error", message: data?.error || "VACUUM failed", }); } } catch { setManualVacuumStatus({ type: "error", message: t("errorOccurred") }); } finally { setManualVacuumLoading(false); } }; const handleManualBackup = async () => { setManualBackupLoading(true); setManualBackupStatus({ type: "", message: "" }); try { const res = await fetch("/api/db-backups", { method: "PUT" }); const data = await res.json(); if (res.ok) { if (data.filename) { setManualBackupStatus({ type: "success", message: t("backupCreated", { file: data.filename }), }); } else { setManualBackupStatus({ type: "info", message: data.message || t("noChangesSinceBackup"), }); } await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { setManualBackupStatus({ type: "error", message: data.error || t("backupFailed") }); } } catch { setManualBackupStatus({ type: "error", message: t("errorOccurred") }); } finally { setManualBackupLoading(false); } }; const handleRestore = async (backupId) => { setRestoringId(backupId); setRestoreStatus({ type: "", message: "" }); try { const res = await fetch("/api/db-backups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ backupId }), }); const data = await res.json(); if (res.ok) { setRestoreStatus({ type: "success", message: t("restoreSuccess", { connections: data.connectionCount, nodes: data.nodeCount, combos: data.comboCount, apiKeys: data.apiKeyCount, }), }); await loadBackups(); await loadStorageHealth(); } else { setRestoreStatus({ type: "error", message: data.error || t("restoreFailed") }); } } catch { setRestoreStatus({ type: "error", message: t("errorDuringRestore") }); } finally { setRestoringId(null); setConfirmRestoreId(null); } }; useEffect(() => { loadStorageHealth(); loadDatabaseSettings(); }, []); /** Triggers a browser file download from an existing Blob. */ const triggerDownload = (blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; /** Fetches a URL, reads the response as a Blob and triggers a download. */ const fetchAndDownload = async ( apiUrl: string, fallbackFilename: string, errorMessage: string ) => { const res = await fetch(apiUrl); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error((data as { error?: string }).error || errorMessage); } const blob = await res.blob(); const disposition = res.headers.get("Content-Disposition") || ""; const filenameMatch = disposition.match(/filename="?([^"]+)"?/); triggerDownload(blob, filenameMatch?.[1] || fallbackFilename); }; const handleExportJson = async () => { setExportLoading(true); try { await fetchAndDownload( "/api/settings/export-json", `omniroute-legacy-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.json`, "JSON Export failed" ); } catch (err) { console.error("Export JSON failed:", err); setImportStatus({ type: "error", message: t("exportFailedWithError", { error: (err as Error).message }), }); } finally { setExportLoading(false); } }; const handleImportJsonClick = () => { jsonInputRef.current?.click(); }; const handleJsonSelected = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.name.endsWith(".json")) { setImportStatus({ type: "error", message: "Invalid file type. Only .json allowed.", }); return; } // Auto import JSON const reader = new FileReader(); reader.onload = async (e) => { try { setImportLoading(true); const res = await fetch("/api/settings/import-json", { method: "POST", headers: { "Content-Type": "application/json" }, body: e.target?.result as string, }); const data = await res.json(); if (res.ok) { setImportStatus({ type: "success", message: data.message || "Legacy JSON imported successfully!", }); await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { setImportStatus({ type: "error", message: data.error || "Failed to import JSON" }); } } catch (err) { setImportStatus({ type: "error", message: "Error during JSON import" }); } finally { setImportLoading(false); if (jsonInputRef.current) jsonInputRef.current.value = ""; } }; reader.readAsText(file); }; const handleExport = async () => { setExportLoading(true); try { await fetchAndDownload( "/api/db-backups/export", `omniroute-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.sqlite`, t("exportFailed") ); } catch (err) { console.error("Export failed:", err); setImportStatus({ type: "error", message: t("exportFailedWithError", { error: (err as Error).message }), }); } finally { setExportLoading(false); } }; const handleImportClick = () => { fileInputRef.current?.click(); }; const handleFileSelected = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.name.endsWith(".sqlite")) { setImportStatus({ type: "error", message: t("invalidFileType"), }); return; } setPendingImportFile(file); setConfirmImport(true); if (fileInputRef.current) fileInputRef.current.value = ""; }; const handleImportConfirm = async () => { if (!pendingImportFile) return; setImportLoading(true); setImportStatus({ type: "", message: "" }); setConfirmImport(false); try { const arrayBuffer = await pendingImportFile.arrayBuffer(); const res = await fetch( `/api/db-backups/import?filename=${encodeURIComponent(pendingImportFile.name)}`, { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: arrayBuffer, } ); const data = await res.json(); if (res.ok) { setImportStatus({ type: "success", message: t("importSuccess", { connections: data.connectionCount, nodes: data.nodeCount, combos: data.comboCount, apiKeys: data.apiKeyCount, }), }); await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { setImportStatus({ type: "error", message: data.error || t("importFailed") }); } } catch { setImportStatus({ type: "error", message: t("errorDuringImport") }); } finally { setImportLoading(false); setPendingImportFile(null); } }; const handleImportCancel = () => { setConfirmImport(false); setPendingImportFile(null); }; const formatBytes = (bytes) => { if (!bytes || bytes === 0) return "0 B"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const formatRelativeTime = (isoString) => { if (!isoString) return null; const now = new Date(); const then = new Date(isoString); const diffMs = (now as any) - (then as any); const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return t("justNow"); if (diffMin < 60) return t("minutesAgo", { count: diffMin }); const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return t("hoursAgo", { count: diffHr }); const diffDays = Math.floor(diffHr / 24); return t("daysAgo", { count: diffDays }); }; const formatBackupReason = (reason) => { if (reason === "manual") return t("backupReasonManual"); if (reason === "pre-restore") return t("backupReasonPreRestore"); return reason; }; const renderStatusAlert = (status, index) => { if (!status.message) return null; const isInfo = status.type === "info"; const isSuccess = status.type === "success"; const className = "p-3 rounded-lg text-sm " + (isSuccess ? "bg-green-500/10 text-green-500 border border-green-500/20" : isInfo ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-red-500/10 text-red-500 border border-red-500/20"); return (
{status.message}
); }; const renderDatabaseStatistics = () => { if (dbSettingsLoading || !dbSettings?.stats) return null; return (

Database Statistics

{t("storageDatabaseSize")}

{formatBytes(dbSettings.stats.databaseSizeBytes)}

{t("storagePageCount")}

{dbSettings.stats.pageCount.toLocaleString()}

{t("storageFreelistCount")}

{dbSettings.stats.freelistCount.toLocaleString()}

{t("storageLastVacuum")}

{dbSettings.stats.lastVacuumAt ? new Date(dbSettings.stats.lastVacuumAt).toLocaleString(locale) : "Never"}

{t("storageLastOptimization")}

{dbSettings.stats.lastOptimizationAt ? new Date(dbSettings.stats.lastOptimizationAt).toLocaleString(locale) : "Never"}

{t("storageIntegrityCheck")}

{dbSettings.stats.integrityCheck === "ok" ? ( {t("storageIntegrityOk")} ) : dbSettings.stats.integrityCheck === "error" ? ( {t("storageIntegrityError")} ) : ( "Not checked" )}

); }; const renderBackupList = () => { if (!backupsExpanded) return null; return (
{backupsLoading ? (
{t("loadingBackups")}
) : backups.length === 0 ? (
{t("noBackupsYet")}
) : ( <>
{t("backupsAvailable", { count: backups.length })}
{backups.map((backup) => (
{new Date(backup.createdAt).toLocaleString(locale)} {formatBackupReason(backup.reason)}
{t("connectionsCount", { count: backup.connectionCount })} {formatBytes(backup.size)}
{confirmRestoreId === backup.id ? ( <> {t("confirm")} ) : ( )}
))} )}
); }; const renderRetentionSettings = () => { if (dbSettingsLoading || !dbSettings) return null; const retentionFields = [ ["quotaSnapshots", t("retentionQuotaSnapshots"), 7], ["compressionAnalytics", t("retentionCompressionAnalytics"), 30], ["mcpAudit", t("retentionMcpAudit"), 30], ["a2aEvents", t("retentionA2aEvents"), 30], ["callLogs", t("retentionCallLogs"), 30], ["usageHistory", t("retentionUsageHistory"), 30], ["memoryEntries", t("retentionMemoryEntries"), 30], ]; return (

{t("storageRetentionCleanup")}

{t("storageRetentionCleanupDesc")}

{t("retentionCallDays", { count: storageHealth.retentionDays.call })} {t("retentionAppDays", { count: storageHealth.retentionDays.app })} {t("retentionRows", { count: (storageHealth.tableMaxRows?.callLogs ?? 100000).toLocaleString(), })}
{retentionFields.map(([key, label, fallback]) => (
setDbSettings({ ...dbSettings, retention: { ...dbSettings.retention, [key]: parseInt(e.target.value) || fallback, }, }) } className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-bg focus:outline-none focus:ring-2 focus:ring-primary" />
))}
); }; const renderOptimizationSettings = () => { if (dbSettingsLoading || !dbSettings) return null; return (

Optimization Settings

setDbSettings({ ...dbSettings, optimization: { ...dbSettings.optimization, vacuumHour: parseInt(e.target.value) || 2, }, }) } className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-bg focus:outline-none focus:ring-2 focus:ring-primary" />
setDbSettings({ ...dbSettings, optimization: { ...dbSettings.optimization, pageSize: parseInt(e.target.value) || 4096, }, }) } className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-bg focus:outline-none focus:ring-2 focus:ring-primary" />
setDbSettings({ ...dbSettings, optimization: { ...dbSettings.optimization, cacheSize: parseInt(e.target.value) || 16384, }, }) } className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-bg focus:outline-none focus:ring-2 focus:ring-primary" />
setDbSettings({ ...dbSettings, optimization: { ...dbSettings.optimization, optimizeOnStartup: e.target.checked, }, }) } className="w-4 h-4 rounded border-border text-primary focus:ring-2 focus:ring-primary" />
); }; const renderCompressionAggregationSettings = () => { if (dbSettingsLoading || !dbSettings) return null; return (

Compression & Aggregation Settings

setDbSettings({ ...dbSettings, aggregation: { ...dbSettings.aggregation, enabled: e.target.checked }, }) } className="w-4 h-4 rounded border-border text-primary focus:ring-2 focus:ring-primary" />
setDbSettings({ ...dbSettings, aggregation: { ...dbSettings.aggregation, rawDataRetentionDays: parseInt(e.target.value) || 30, }, }) } className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-bg focus:outline-none focus:ring-2 focus:ring-primary" />
); }; return (

{t("systemStorage")}

{t("allDataLocal")}

{storageHealth.driver || "json"}

{t("databasePath")}

{storageHealth.dbPath || "~/.omniroute/storage.sqlite"}

{renderDatabaseStatistics()}

{t("export")}

{confirmImport && pendingImportFile && (

{t("confirmDbImport")}

{t("confirmDbImportDesc", { file: pendingImportFile.name })}

)} {importStatus.message &&
{renderStatusAlert(importStatus, 0)}
}

{t("maintenance") || "Maintenance"}

{[ clearCacheStatus, purgeLogsStatus, manualVacuumStatus, purgeQuotaSnapshotsStatus, purgeCallLogsStatus, purgeDetailedLogsStatus, ].map(renderStatusAlert)}

{t("lastBackup")}

{storageHealth.lastBackupAt ? new Date(storageHealth.lastBackupAt).toLocaleString(locale) + " (" + formatRelativeTime(storageHealth.lastBackupAt) + ")" : t("noBackupYet")}

{manualBackupStatus.message && (
{renderStatusAlert(manualBackupStatus, 0)}
)} {restoreStatus.message &&
{renderStatusAlert(restoreStatus, 1)}
} {renderBackupList()} {renderRetentionSettings()} {renderOptimizationSettings()} {renderCompressionAggregationSettings()}
); }