1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { d$ as Archive, b0 as FileText, cM as RefreshCw, ci as Database, fI as Cloud, b9 as Settings, dO as Calendar, cd as Save, a_ as CircleCheck, ea as CircleX, b5 as Trash2, c4 as RotateCcw, cB as ChevronUp, cC as ChevronDown, bf as Search } from "./vendor-D0P4XgNj.js"; 3import "./vendor-i18n-BC7ZMXWQ.js"; 4const BackupManager = () => { 5 var _a; 6 const [backups, setBackups] = reactExports.useState([]); 7 const [selectedBackup, setSelectedBackup] = reactExports.useState(null); 8 const [files, setFiles] = reactExports.useState([]); 9 const [selectedFile, setSelectedFile] = reactExports.useState(null); 10 const [fileContent, setFileContent] = reactExports.useState(""); 11 const [loading, setLoading] = reactExports.useState(false); 12 const [creating, setCreating] = reactExports.useState(false); 13 const [backupProgress, setBackupProgress] = reactExports.useState(null); 14 const [savingSchedule, setSavingSchedule] = reactExports.useState(false); 15 const [scheduleExpanded, setScheduleExpanded] = reactExports.useState(false); 16 const [dbStats, setDbStats] = reactExports.useState(null); 17 const [statsTitle, setStatsTitle] = reactExports.useState("Live Database Statistics"); 18 const [showStatsModal, setShowStatsModal] = reactExports.useState(false); 19 const [sortConfig, setSortConfig] = reactExports.useState(null); 20 const [activeTab, setActiveTab] = reactExports.useState("database"); 21 const [compareModalOpen, setCompareModalOpen] = reactExports.useState(false); 22 const [diffData, setDiffData] = reactExports.useState(null); 23 const [settingsRestoreModalOpen, setSettingsRestoreModalOpen] = reactExports.useState(false); 24 const [settingsRestoreData, setSettingsRestoreData] = reactExports.useState(null); 25 const [selectedSettingsSections, setSelectedSettingsSections] = reactExports.useState([]); 26 const [viewingTable, setViewingTable] = reactExports.useState(null); 27 const [tableData, setTableData] = reactExports.useState(null); 28 const [isTableLoading, setIsTableLoading] = reactExports.useState(false); 29 const [tableSearchQuery, setTableSearchQuery] = reactExports.useState(""); 30 const [tableSearchField, setTableSearchField] = reactExports.useState("ALL"); 31 const [tableCurrentPage, setTableCurrentPage] = reactExports.useState(1); 32 const rowsPerPage = 50; 33 const [schedule, setSchedule] = reactExports.useState({ 34 enabled: true, 35 cronExpression: "0 2 * * *", 36 keepDaily: 7, 37 keepWeekly: 4, 38 keepMonthly: 12 39 }); 40 const [settingsSchedule, setSettingsSchedule] = reactExports.useState({ 41 enabled: true, 42 cronExpression: "0 2 * * *", 43 keepDaily: 7, 44 keepWeekly: 4, 45 keepMonthly: 12 46 }); 47 const [cfTokens, setCfTokens] = reactExports.useState({ dnsToken: "", tunnelToken: "" }); 48 const [savingCfTokens, setSavingCfTokens] = reactExports.useState(false); 49 const [cfConfigExpanded, setCfConfigExpanded] = reactExports.useState(true); 50 reactExports.useEffect(() => { 51 loadBackups(); 52 loadSchedule(); 53 loadSettingsSchedule(); 54 loadCfTokens(); 55 }, []); 56 const loadCfTokens = async () => { 57 try { 58 const res = await fetch("/api/admin/backups/cloudflare/config"); 59 if (res.ok) { 60 const data = await res.json(); 61 setCfTokens({ dnsToken: data.dnsToken || "", tunnelToken: data.tunnelToken || "" }); 62 } 63 } catch (e) { 64 console.error("Failed to restore Cloudflare config:", e); 65 alert(`Failed: ${e.message}`); 66 } 67 }; 68 const handleBrowseTable = async (tableName) => { 69 if (!selectedBackup || !selectedBackup.id) return; 70 setViewingTable(tableName); 71 setIsTableLoading(true); 72 setTableData(null); 73 setTableSearchQuery(""); 74 setTableSearchField("ALL"); 75 setTableCurrentPage(1); 76 try { 77 const res = await fetch(`/api/admin/backups/${selectedBackup.id}/read-table?table=${tableName}`); 78 if (!res.ok) throw new Error(await res.text()); 79 const data = await res.json(); 80 if (data.success) { 81 setTableData({ columns: data.columns, rows: data.rows, limitReached: data.limitReached }); 82 } else { 83 throw new Error(data.error); 84 } 85 } catch (e) { 86 console.error("Failed to read table:", e); 87 alert(`Failed to extract table data: ${e.message || e}`);
88 setViewingTable(null); 89 } finally { 90 setIsTableLoading(false); 91 } 92 }; 93 const handleBrowseSettingsTable = (tableName, records) => { 94 setViewingTable(tableName); 95 let columns = []; 96 if (records && records.length > 0) { 97 columns = Object.keys(records[0]); 98 } 99 setTableData({ 100 columns, 101 rows: records, 102 limitReached: false 103 }); 104 setTableSearchQuery(""); 105 setTableSearchField("ALL"); 106 setTableCurrentPage(1); 107 }; 108 const saveCfTokens = async () => { 109 setSavingCfTokens(true); 110 try { 111 const res = await fetch("/api/admin/backups/cloudflare/config", { 112 method: "POST", 113 headers: { "Content-Type": "application/json" }, 114 body: JSON.stringify(cfTokens) 115 }); 116 const data = await res.json(); 117 if (data.success) { 118 alert("Cloudflare API tokens saved successfully."); 119 } else { 120 alert("Failed to save tokens: " + (data.error || "Unknown error")); 121 } 122 } catch (e) { 123 alert("Failed to save tokens: " + e.message); 124 } finally { 125 setSavingCfTokens(false); 126 } 127 }; 128 reactExports.useEffect(() => { 129 if (selectedBackup && selectedBackup.type === "snapshot") { 130 loadFiles(selectedBackup.id); 131 } else { 132 setFiles([]); 133 setSelectedFile(null); 134 } 135 }, [selectedBackup]); 136 reactExports.useEffect(() => { 137 if (selectedFile && selectedBackup) { 138 loadFileContent(selectedBackup.id, selectedFile.backupFilename); 139 } else { 140 setFileContent(""); 141 } 142 }, [selectedFile, selectedBackup]); 143 const loadSchedule = async () => { 144 try { 145 const res = await fetch("/api/admin/backups/schedule"); 146 if (res.ok) { 147 const data = await res.json(); 148 setSchedule(data); 149 } 150 } catch (e) { 151 console.error("Failed to load backup schedule:", e); 152 } 153 }; 154 const saveSchedule = async () => { 155 setSavingSchedule(true); 156 try { 157 const res = await fetch("/api/admin/backups/schedule", { 158 method: "POST", 159 headers: { "Content-Type": "application/json" }, 160 body: JSON.stringify(schedule) 161 }); 162 const data = await res.json(); 163 if (data.success) { 164 alert("Backup schedule & retention rules saved successfully."); 165 setSchedule(data.schedule); 166 } else { 167 alert("Failed to save schedule: " + (data.error || "Unknown error")); 168 } 169 } catch (e) { 170 alert("Failed to save schedule: " + e.message); 171 } finally { 172 setSavingSchedule(false); 173 } 174 }; 175 const loadSettingsSchedule = async () => { 176 try { 177 const res = await fetch("/api/admin/backups/settings/schedule"); 178 if (res.ok) { 179 const data = await res.json(); 180 setSettingsSchedule(data); 181 } 182 } catch (e) { 183 console.error("Failed to load settings backup schedule:", e); 184 } 185 }; 186 const saveSettingsSchedule = async () => { 187 setSavingSchedule(true); 188 try { 189 const res = await fetch("/api/admin/backups/settings/schedule", { 190 method: "POST", 191 headers: { "Content-Type": "application/json" }, 192 body: JSON.stringify(settingsSchedule) 193 }); 194 const data = await res.json(); 195 if (data.success) { 196 alert("Settings backup schedule & retention rules saved successfully."); 197 setSettingsSchedule(data.schedule); 198 } else { 199 alert("Failed to save settings schedule: " + (data.error || "Unknown error")); 200 } 201 } catch (e) { 202 alert("Failed to save settings schedule: " + e.message); 203 } finally { 204 setSavingSchedule(false); 205 } 206 }; 207 const loadBackups = async () => { 208 setLoading(true); 209 try { 210 const res = await fetch("/api/admin/backups"); 211 if (res.ok) { 212 const data = await res.json(); 213 setBackups(data); 214 } 215 } catch (e) { 216 console.error(e); 217 } finally { 218 setLoading(false); 219 } 220 }; 221 const loadFiles = async (id) => { 222 try { 223 const res = await fetch(`/api/admin/backups/${id}/files`); 224 if (res.ok) setFiles(await res.json()); 225 } catch (e) { 226 console.error(e); 227 } 228 }; 229 const loadFileContent = async (backupId, filename) => { 230 try { 231 const res = await fetch(`/api/admin/backups/${backupId}/read?file=${filename}`); 232 if (res.ok) { 233 const data = await res.json(); 234 setFileContent(data.content); 235 } 236 } catch (e) { 237 console.error(e); 238 } 239 }; 240 const loadDbStats = async () => { 241 try { 242 const res = await fetch("/api/admin/backups/stats"); 243 const data = await res.json(); 244 if (data.success) { 245 setDbStats(data.stats); 246 setStatsTitle("Live Database Statistics"); 247 setShowStatsModal(true); 248 } else { 249 alert("Failed to load stats: " + data.error); 250 } 251 } catch (e) { 252 console.error(e); 253 } 254 }; 255 const checkBackupStatus = async (filename) => { 256 try { 257 const res = await fetch(`/api/admin/backups/status/${filename}`); 258 const data = await res.json(); 259 if (data.status === "running") { 260 setBackupProgress({ filename, size: data.size }); 261 setTimeout(() => checkBackupStatus(filename), 1500); 262 } else if (data.status === "success") { 263 alert(`Database backup ${filename} created successfully.`); 264 setCreating(false); 265 setBackupProgress(null); 266 loadBackups(); 267 loadSchedule(); 268 } else if (data.status === "error") { 269 alert("Backup failed: " + data.error); 270 setCreating(false); 271 setBackupProgress(null); 272 } else { 273 setCreating(false); 274 setBackupProgress(null); 275 } 276 } catch (e) { 277 console.error("Failed to poll status:", e); 278 setTimeout(() => checkBackupStatus(filename), 2e3); 279 } 280 }; 281 const createDbBackup = async () => { 282 if (!confirm("Trigger on-demand PostgreSQL database backup? This will execute pg_dump and save to backups storage.")) return; 283 setCreating(true); 284 setBackupProgress(null); 285 try { 286 const res = await fetch("/api/admin/backups/create", { method: "POST" }); 287 const data = await res.json(); 288 if (data.success) { 289 setBackupProgress({ filename: data.filename, size: 0 }); 290 checkBackupStatus(data.filename); 291 } else { 292 alert("Backup failed: " + data.error); 293 setCreating(false); 294 } 295 } catch (e) { 296 alert("Backup failed: " + e.message); 297 setCreating(false); 298 } 299 }; 300 const createSettingsBackup = async () => { 301 if (!confirm("Create a new manual settings backup?")) return; 302 setCreating(true); 303 try { 304 const res = await fetch("/api/admin/backups/settings/create", { method: "POST" }); 305 const data = await res.json(); 306 if (data.success) { 307 alert(`Settings backup ${data.filename} created successfully.`); 308 loadBackups(); 309 loadSettingsSchedule(); 310 } else { 311 alert("Backup failed: " + data.error); 312 } 313 } catch (e) { 314 alert("Backup failed: " + e.message); 315 } finally { 316 setCreating(false); 317 } 318 }; 319 const createCfBackup = async () => { 320 if (!confirm("Trigger Cloudflare backup? This will fetch all DNS records and Tunnel configurations.")) return; 321 setCreating(true); 322 setBackupProgress(null); 323 try { 324 const res = await fetch("/api/admin/backups/cloudflare/create", { method: "POST" }); 325 const data = await res.json(); 326 if (data.success) { 327 setBackupProgress({ filename: data.filename, size: 0 }); 328 checkBackupStatus(data.filename); 329 } else { 330 alert("Backup failed: " + data.error); 331 setCreating(false); 332 } 333 } catch (e) { 334 alert("Backup failed: " + e.message); 335 setCreating(false); 336 } 337 }; 338 const deleteBackup = async (id, e) => { 339 e.stopPropagation(); 340 if (!confirm(`Are you sure you want to delete ${id}? This action cannot be undone.`)) return; 341 try { 342 const res = await fetch(`/api/admin/backups/${id}`, { method: "DELETE" }); 343 const data = await res.json(); 344 if (data.success) { 345 if ((selectedBackup == null ? void 0 : selectedBackup.id) === id) setSelectedBackup(null); 346 loadBackups(); 347 } else { 348 alert("Delete failed: " + data.error); 349 } 350 } catch (err) { 351 alert("Delete failed: " + err.message); 352 } 353 }; 354 const openCompareModal = async () => { 355 if (!selectedFile || !selectedBackup) return;
356 setLoading(true); 357 try { 358 const res = await fetch(`/api/admin/backups/cloudflare/diff?backupId=${selectedBackup.id}&file=${selectedFile.backupFilename}`); 359 const data = await res.json(); 360 if (data.success) { 361 setDiffData({ backup: data.backup, live: data.live }); 362 setCompareModalOpen(true); 363 } else { 364 alert("Diff failed: " + data.error); 365 } 366 } catch (e) { 367 alert("Diff failed: " + e.message); 368 } finally { 369 setLoading(false); 370 } 371 }; 372 const handleRestoreCloudflare = async () => { 373 if (!selectedFile || !selectedBackup || !diffData) return; 374 let payload = {}; 375 if (selectedFile.backupFilename === "dns_records.json") { 376 const recordsToRestore = []; 377 const backupRecs = diffData.backup.result || []; 378 const liveRecs = diffData.live.result || []; 379 for (const b of backupRecs) { 380 const live = liveRecs.find((l) => l.name === b.name && l.type === b.type); 381 if (!live) { 382 recordsToRestore.push({ action: "create", data: b }); 383 } else if (live.content !== b.content || live.proxied !== b.proxied) { 384 recordsToRestore.push({ action: "update", data: { ...b, id: live.id } }); 385 } 386 } 387 if (recordsToRestore.length === 0) { 388 alert("No changes to restore for DNS."); 389 return; 390 } 391 if (!confirm(`Restore ${recordsToRestore.length} DNS record changes?`)) return; 392 payload = { type: "dns", items: recordsToRestore }; 393 } else if (selectedFile.backupFilename.startsWith("tunnel_config_")) { 394 if (!confirm(`Restore Tunnel Configuration from backup? This will overwrite the live configuration for this tunnel.`)) return; 395 payload = { 396 type: "tunnel_config", 397 items: diffData.backup.result, 398 tunnelName: selectedFile.backupFilename.replace("tunnel_config_", "").replace(".json", "") 399 }; 400 } 401 try { 402 const res = await fetch("/api/admin/backups/cloudflare/restore", { 403 method: "POST", 404 headers: { "Content-Type": "application/json" }, 405 body: JSON.stringify(payload) 406 }); 407 const data = await res.json(); 408 if (data.success) { 409 alert(`Successfully restored configuration.`); 410 setCompareModalOpen(false); 411 } else { 412 alert("Restore failed: " + data.error); 413 } 414 } catch (e) { 415 alert("Restore failed: " + e.message); 416 } 417 }; 418 const handleBrowseSettings = async () => { 419 if (!selectedBackup || selectedBackup.type !== "settings") return; 420 setLoading(true); 421 try { 422 const res = await fetch(`/api/admin/backups/settings/${selectedBackup.id}/browse`); 423 const data = await res.json(); 424 if (data.success) { 425 setSettingsRestoreData(data.data); 426 setSelectedSettingsSections([]); 427 setSettingsRestoreModalOpen(true); 428 } else { 429 alert("Browse failed: " + data.error); 430 } 431 } catch (e) { 432 alert("Browse failed: " + e.message); 433 } finally { 434 setLoading(false); 435 } 436 }; 437 const handleRestoreSettings = async () => { 438 if (!selectedBackup || selectedBackup.type !== "settings") return; 439 if (selectedSettingsSections.length === 0) { 440 alert("Please select at least one section to restore."); 441 return; 442 } 443 if (!confirm(`Are you sure you want to restore ${selectedSettingsSections.length} sections? This will overwrite the live settings in the database for these sections.`)) return; 444 setLoading(true); 445 try { 446 const res = await fetch(`/api/admin/backups/settings/${selectedBackup.id}/restore`, { 447 method: "POST", 448 headers: { "Content-Type": "application/json" }, 449 body: JSON.stringify({ sections: selectedSettingsSections }) 450 }); 451 const data = await res.json(); 452 if (data.success) { 453 alert(`Successfully restored settings: ${data.message}`); 454 setSettingsRestoreModalOpen(false); 455 } else { 456 alert("Restore failed: " + data.error); 457 } 458 } catch (e) { 459 alert("Restore failed: " + e.message); 460 } finally { 461 setLoading(false); 462 } 463 }; 464 const toggleSettingsSection = (sectionName) => { 465 if (selectedSettingsSections.includes(sectionName)) { 466 setSelectedSettingsSections(selectedSettingsSections.filter((s) => s !== sectionName)); 467 } else { 468 setSelectedSettingsSections([...selectedSettingsSections, sectionName]); 469 } 470 }; 471 const handleRestore = async () => { 472 if (!selectedFile || !selectedBackup) return; 473 if (!confirm(`Restore ${selectedFile.originalPath} from backup? This overwrites the current live file.`)) return; 474 try { 475 const res = await fetch("/api/dev-agent/restore", { 476 method: "POST", 477 headers: { "Content-Type": "application/json" }, 478 body: JSON.stringify({ 479 file: { 480 backupPath: `backup/${selectedBackup.id}/${selectedFile.backupFilename}`, 481 originalPath: selectedFile.originalPath 482 } 483 }) 484 }); 485 const data = await res.json(); 486 if (data.success) { 487 alert("File restored successfully."); 488 } else { 489 alert("Restore failed: " + data.error); 490 } 491 } catch (e) { 492 alert("Restore error: " + e.message); 493 } 494 }; 495 const formatBytes = (bytes) => { 496 if (!bytes) return "0 B";
497 const k = 1024; 498 const sizes = ["B", "KB", "MB", "GB"]; 499 const i = Math.floor(Math.log(bytes) / Math.log(k)); 500 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; 501 }; 502 const filteredTableRows = reactExports.useMemo(() => { 503 if (!tableData) return []; 504 if (!tableSearchQuery.trim()) return tableData.rows; 505 const query = tableSearchQuery.toLowerCase(); 506 return tableData.rows.filter((row) => { 507 if (tableSearchField === "ALL") { 508 return Object.values(row).some((val) => String(val).toLowerCase().includes(query)); 509 } 510 return String(row[tableSearchField] || "").toLowerCase().includes(query); 511 }); 512 }, [tableData, tableSearchQuery, tableSearchField]); 513 const filteredBackups = reactExports.useMemo(() => { 514 return backups.filter((bk) => { 515 if (activeTab === "cloudflare") return bk.type === "cloudflare"; 516 if (activeTab === "settings") return bk.type === "settings"; 517 return bk.type === "database" || bk.type === "snapshot"; 518 }); 519 }, [backups, activeTab]); 520 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full space-y-4 animate-in fade-in", children: [ 521 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center bg-white dark:bg-zinc-900 p-4 rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-sm shrink-0", children: [
522 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 523 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 524 /* @__PURE__ */ jsxRuntimeExports.jsx(Archive, { className: "text-indigo-500" }), 525 " Database & System Backup Manager" 526 ] }), 527 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Configure backup rotation schedules, retention limits, and manage DB dumps & snapshots." }) 528 ] }), 529 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 530 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: loadDbStats, className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg text-zinc-500 transition-colors", title: "View Live DB Stats", children: /* @__PURE__ */ jsxRuntimeExports.jsx(FileText, { size: 20 }) }), 531 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => { 532 loadBackups(); 533 loadSchedule(); 534 }, className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg text-zinc-500 transition-colors", title: "Refresh", children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 20, className: loading ? "animate-spin" : "" }) }), 535 activeTab === "database" ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 536 "button", 537 { 538 onClick: createDbBackup, 539 disabled: creating, 540 className: "flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-bold text-sm shadow-md transition-colors disabled:opacity-50", 541 children: [
542 creating ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 16 }), 543 creating ? backupProgress ? `Dumping... ${formatBytes(backupProgress.size)}` : "Starting Backup..." : "Create DB Dump" 544 ] 545 } 546 ) : activeTab === "cloudflare" ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 547 "button", 548 { 549 onClick: createCfBackup, 550 disabled: creating, 551 className: "flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg font-bold text-sm shadow-md transition-colors disabled:opacity-50", 552 children: [ 553 creating ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Cloud, { size: 16 }), 554 creating ? "Fetching..." : "Create CF Backup" 555 ] 556 } 557 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 558 "button", 559 { 560 onClick: createSettingsBackup, 561 disabled: creating, 562 className: "flex items-center gap-2 px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white rounded-lg font-bold text-sm shadow-md transition-colors disabled:opacity-50", 563 children: [
564 creating ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 16 }), 565 creating ? "Fetching..." : "Create Settings Backup" 566 ] 567 } 568 ) 569 ] }) 570 ] }), 571 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg w-max shrink-0", children: [ 572 /* @__PURE__ */ jsxRuntimeExports.jsx( 573 "button", 574 { 575 onClick: () => { 576 setActiveTab("database"); 577 setSelectedBackup(null); 578 }, 579 className: `px-4 py-2 rounded-md font-bold text-sm transition-colors ${activeTab === "database" ? "bg-white dark:bg-zinc-700 shadow-sm text-indigo-600 dark:text-indigo-400" : "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"}`, 580 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 581 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 16 }), 582 " Database & Code" 583 ] }) 584 } 585 ),
586 /* @__PURE__ */ jsxRuntimeExports.jsx( 587 "button", 588 { 589 onClick: () => { 590 setActiveTab("cloudflare"); 591 setSelectedBackup(null); 592 }, 593 className: `px-4 py-2 rounded-md font-bold text-sm transition-colors ${activeTab === "cloudflare" ? "bg-white dark:bg-zinc-700 shadow-sm text-amber-600 dark:text-amber-400" : "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"}`, 594 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 595 /* @__PURE__ */ jsxRuntimeExports.jsx(Cloud, { size: 16 }), 596 " Cloudflare Configs" 597 ] }) 598 } 599 ), 600 /* @__PURE__ */ jsxRuntimeExports.jsx( 601 "button", 602 { 603 onClick: () => { 604 setActiveTab("settings"); 605 setSelectedBackup(null); 606 }, 607 className: `px-4 py-2 rounded-md font-bold text-sm transition-colors ${activeTab === "settings" ? "bg-white dark:bg-zinc-700 shadow-sm text-teal-600 dark:text-teal-400" : "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"}`, 608 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
609 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 16 }), 610 " Site Settings" 611 ] }) 612 } 613 ) 614 ] }), 615 (activeTab === "database" || activeTab === "settings") && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-sm shrink-0 overflow-hidden", children: [ 616 /* @__PURE__ */ jsxRuntimeExports.jsxs( 617 "div", 618 { 619 className: "flex items-center justify-between p-4 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors", 620 onClick: () => setScheduleExpanded(!scheduleExpanded), 621 children: [ 622 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 623 /* @__PURE__ */ jsxRuntimeExports.jsx(Calendar, { className: "text-indigo-500", size: 18 }), 624 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-base text-zinc-900 dark:text-white", children: "Backup Schedule & Retention Rules" }) 625 ] }),
626 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-500 font-semibold", children: [ 627 scheduleExpanded ? "Hide" : "Show", 628 " Settings" 629 ] }) }) 630 ] 631 } 632 ), 633 scheduleExpanded && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 pt-0 border-t border-zinc-100 dark:border-zinc-800 mt-2", children: [ 634 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4 pb-3 border-b border-zinc-100 dark:border-zinc-800", children: [ 635 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: [ 636 /* @__PURE__ */ jsxRuntimeExports.jsx( 637 "input", 638 { 639 type: "checkbox", 640 checked: activeTab === "database" ? schedule.enabled : settingsSchedule.enabled, 641 onChange: (e) => { 642 if (activeTab === "database") setSchedule({ ...schedule, enabled: e.target.checked }); 643 else setSettingsSchedule({ ...settingsSchedule, enabled: e.target.checked }); 644 }, 645 className: "w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500" 646 } 647 ), 648 "Automated Rotation Active" 649 ] }) }),
650 /* @__PURE__ */ jsxRuntimeExports.jsxs( 651 "button", 652 { 653 onClick: activeTab === "database" ? saveSchedule : saveSettingsSchedule, 654 disabled: savingSchedule, 655 className: "flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-bold text-xs shadow transition-colors disabled:opacity-50", 656 children: [ 657 savingSchedule ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 14 }), 658 "Save Settings" 659 ] 660 } 661 ) 662 ] }), 663 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-4 gap-4 text-sm", children: [ 664 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 665 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "Cron Schedule Expression" }),
666 /* @__PURE__ */ jsxRuntimeExports.jsx( 667 "input", 668 { 669 type: "text", 670 value: activeTab === "database" ? schedule.cronExpression : settingsSchedule.cronExpression, 671 onChange: (e) => { 672 if (activeTab === "database") setSchedule({ ...schedule, cronExpression: e.target.value }); 673 else setSettingsSchedule({ ...settingsSchedule, cronExpression: e.target.value }); 674 }, 675 disabled: activeTab === "database", 676 placeholder: activeTab === "database" ? "Managed by crontab" : "e.g. 0 2 * * *", 677 className: "w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-mono text-xs text-zinc-500 focus:outline-none disabled:cursor-not-allowed" 678 } 679 ), 680 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: activeTab === "database" ? "Schedule is configured via system crontab and runs in the node's local time zone." : "Cron format (e.g. '0 * * * *' for every hour). Checked hourly by background worker." }) 681 ] }),
682 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 683 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "Keep Daily Backups" }), 684 /* @__PURE__ */ jsxRuntimeExports.jsx( 685 "input", 686 { 687 type: "number", 688 min: "1", 689 max: "365", 690 value: activeTab === "database" ? schedule.keepDaily : settingsSchedule.keepDaily, 691 onChange: (e) => { 692 const val = parseInt(e.target.value) || 7; 693 if (activeTab === "database") setSchedule({ ...schedule, keepDaily: val }); 694 else setSettingsSchedule({ ...settingsSchedule, keepDaily: val }); 695 }, 696 className: "w-full px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-bold text-zinc-800 dark:text-zinc-200 focus:outline-none focus:border-indigo-500" 697 } 698 ),
699 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: "Rotation retention limit for daily dumps" }) 700 ] }), 701 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 702 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "Keep Weekly Backups" }), 703 /* @__PURE__ */ jsxRuntimeExports.jsx( 704 "input", 705 { 706 type: "number", 707 min: "1", 708 max: "104", 709 value: activeTab === "database" ? schedule.keepWeekly : settingsSchedule.keepWeekly, 710 onChange: (e) => { 711 const val = parseInt(e.target.value) || 4; 712 if (activeTab === "database") setSchedule({ ...schedule, keepWeekly: val }); 713 else setSettingsSchedule({ ...settingsSchedule, keepWeekly: val }); 714 }, 715 className: "w-full px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-bold text-zinc-800 dark:text-zinc-200 focus:outline-none focus:border-indigo-500" 716 } 717 ),
718 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: "Rotation retention limit for weekly dumps" }) 719 ] }), 720 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 721 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "Keep Monthly Backups" }), 722 /* @__PURE__ */ jsxRuntimeExports.jsx( 723 "input", 724 { 725 type: "number", 726 min: "1", 727 max: "120", 728 value: activeTab === "database" ? schedule.keepMonthly : settingsSchedule.keepMonthly, 729 onChange: (e) => { 730 const val = parseInt(e.target.value) || 12; 731 if (activeTab === "database") setSchedule({ ...schedule, keepMonthly: val }); 732 else setSettingsSchedule({ ...settingsSchedule, keepMonthly: val }); 733 }, 734 className: "w-full px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-bold text-zinc-800 dark:text-zinc-200 focus:outline-none focus:border-indigo-500" 735 } 736 ),
737 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: "Rotation retention limit for monthly dumps" }) 738 ] }) 739 ] }), 740 (activeTab === "database" ? schedule.lastBackupAt : settingsSchedule.lastBackupAt) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 pt-3 border-t border-zinc-100 dark:border-zinc-800 flex items-center justify-between text-xs text-zinc-500", children: [ 741 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 742 (activeTab === "database" ? schedule.lastBackupStatus : settingsSchedule.lastBackupStatus) === "success" ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 16, className: "text-emerald-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { size: 16, className: "text-red-500" }), 743 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 744 "Last Execution: ", 745 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: new Date(activeTab === "database" ? schedule.lastBackupAt : settingsSchedule.lastBackupAt).toLocaleString(void 0, { timeZoneName: "short" }) }) 746 ] }), 747 (activeTab === "database" ? schedule.lastBackupFile : settingsSchedule.lastBackupFile) && /* @__P
747URE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-zinc-400", children: [ 748 "(", 749 activeTab === "database" ? schedule.lastBackupFile : settingsSchedule.lastBackupFile, 750 ")" 751 ] }) 752 ] }), 753 (activeTab === "database" ? schedule.lastBackupSize : settingsSchedule.lastBackupSize) && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 rounded text-zinc-700 dark:text-zinc-300", children: [ 754 "Size: ", 755 activeTab === "database" ? schedule.lastBackupSize : settingsSchedule.lastBackupSize 756 ] }) 757 ] }) 758 ] }) 759 ] }), 760 activeTab === "cloudflare" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-sm shrink-0 overflow-hidden", children: [ 761 /* @__PURE__ */ jsxRuntimeExports.jsxs( 762 "div", 763 { 764 className: "flex items-center justify-between p-4 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors", 765 onClick: () => setCfConfigExpanded(!cfConfigExpanded), 766 children: [
767 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 768 /* @__PURE__ */ jsxRuntimeExports.jsx(Cloud, { className: "text-amber-500", size: 18 }), 769 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-base text-zinc-900 dark:text-white", children: "Cloudflare API Tokens & Credentials" }) 770 ] }), 771 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-500 font-semibold", children: [ 772 cfConfigExpanded ? "Hide" : "Show", 773 " Configuration" 774 ] }) }) 775 ] 776 } 777 ), 778 cfConfigExpanded && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 pt-0 border-t border-zinc-100 dark:border-zinc-800 mt-2", children: [ 779 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4 pb-3 border-b border-zinc-100 dark:border-zinc-800", children: [
780 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500", children: "Configure Cloudflare API credentials stored in System Settings for DNS & Tunnel backups." }), 781 /* @__PURE__ */ jsxRuntimeExports.jsxs( 782 "button", 783 { 784 onClick: saveCfTokens, 785 disabled: savingCfTokens, 786 className: "flex items-center gap-1.5 px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-bold text-xs shadow transition-colors disabled:opacity-50", 787 children: [ 788 savingCfTokens ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 14 }), 789 "Save Credentials" 790 ] 791 } 792 ) 793 ] }), 794 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 text-sm", children: [ 795 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
796 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "DNS Zone Token" }), 797 /* @__PURE__ */ jsxRuntimeExports.jsx( 798 "input", 799 { 800 type: "password", 801 value: cfTokens.dnsToken, 802 onChange: (e) => setCfTokens({ ...cfTokens, dnsToken: e.target.value }), 803 placeholder: "cfat_...", 804 className: "w-full px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-mono text-xs text-zinc-800 dark:text-zinc-200 focus:outline-none focus:border-amber-500" 805 } 806 ), 807 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: "Token for reading Cloudflare Zone DNS records" }) 808 ] }), 809 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 810 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase", children: "Zero Trust / Tunnel Token" }),
811 /* @__PURE__ */ jsxRuntimeExports.jsx( 812 "input", 813 { 814 type: "password", 815 value: cfTokens.tunnelToken, 816 onChange: (e) => setCfTokens({ ...cfTokens, tunnelToken: e.target.value }), 817 placeholder: "cfut_...", 818 className: "w-full px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg font-mono text-xs text-zinc-800 dark:text-zinc-200 focus:outline-none focus:border-amber-500" 819 } 820 ), 821 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-400", children: "Token for reading Zero Trust Tunnel & Access settings" }) 822 ] }) 823 ] }) 824 ] }) 825 ] }), 826 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex gap-4 min-h-0 overflow-hidden", children: [ 827 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-80 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden flex flex-col shrink-0", children: [
828 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 font-bold text-xs uppercase text-zinc-500 flex justify-between items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 829 "Stored Backups (", 830 filteredBackups.length, 831 ")" 832 ] }) }), 833 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-2", children: [ 834 filteredBackups.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-center text-sm text-zinc-500", children: "No backup files found on disk." }), 835 filteredBackups.map((bk) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 836 "div", 837 { 838 onClick: () => setSelectedBackup(bk), 839 className: `p-3 rounded-lg cursor-pointer border transition-all ${(selectedBackup == null ? void 0 : selectedBackup.id) === bk.id ? "bg-indigo-50 dark:bg-indigo-900/20 border-indigo-500" : "bg-white dark:bg-zinc-800/50 border-zinc-200 dark:border-zinc-700 hover:border-zinc-400"}`, 840 children: [
841 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1", children: [ 842 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [ 843 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-bold uppercase px-1.5 py-0.5 rounded ${bk.type === "database" ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300" : bk.type === "settings" ? "bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300" : "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"}`, children: bk.type }), 844 bk.category && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold uppercase px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400", children: bk.category }) 845 ] }), 846 /* @__PURE__ */ jsxRuntimeExports.jsx( 847 "button", 848 { 849 onClick: (e) => deleteBackup(bk.id, e), 850 className: "text-zinc-400 hover:text-red-500 p-1 rounded transition-colors", 851 title: "Delete Backup", 852 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 12 }) 853 } 854 ) 855 ] }),
856 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-xs text-zinc-900 dark:text-white truncate font-mono", children: bk.id }), 857 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[11px] text-zinc-500 flex items-center justify-between mt-1", children: [ 858 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: new Date(bk.timestamp).toLocaleString(void 0, { timeZoneName: "short" }) }), 859 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold", children: bk.size ? formatBytes(bk.size) : `${bk.fileCount || 0} files` }) 860 ] }) 861 ] 862 }, 863 bk.id 864 )) 865 ] }) 866 ] }), 867 (selectedBackup == null ? void 0 : selectedBackup.type) === "snapshot" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-80 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden flex flex-col shrink-0", children: [
868 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 font-bold text-xs uppercase text-zinc-500", children: "Files in Snapshot" }), 869 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto p-2 space-y-1", children: files.length > 0 ? files.map((file, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 870 "div", 871 { 872 onClick: () => setSelectedFile(file), 873 className: `flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer text-xs font-mono transition-colors ${(selectedFile == null ? void 0 : selectedFile.originalPath) === file.originalPath ? "bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-white font-bold" : "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, 874 children: [ 875 /* @__PURE__ */ jsxRuntimeExports.jsx(FileText, { size: 14, className: "shrink-0" }), 876 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", title: file.originalPath, children: file.originalPath }) 877 ] 878 }, 879 i 880 )) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-center text-sm text-zinc-500", children: "Loading snapshot files..." }) }) 881 ] }),
882 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden flex flex-col min-w-0", children: [ 883 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 flex justify-between items-center h-12", children: [ 884 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-xs uppercase text-zinc-500 truncate pr-4", children: selectedFile ? `Preview: ${selectedFile.originalPath}` : selectedBackup ? `Details: ${selectedBackup.id}` : "Backup Details & Preview" }), 885 selectedFile && activeTab === "database" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 886 "button", 887 { 888 onClick: handleRestore, 889 className: "flex items-center gap-2 px-3 py-1 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40 rounded text-xs font-bold transition-colors", 890 children: [
891 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 12 }), 892 " Restore File" 893 ] 894 } 895 ), 896 selectedBackup && activeTab === "settings" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 897 "button", 898 { 899 onClick: handleBrowseSettings, 900 disabled: loading, 901 className: "flex items-center gap-2 px-3 py-1 bg-teal-50 dark:bg-teal-900/20 text-teal-600 dark:text-teal-400 hover:bg-teal-100 dark:hover:bg-teal-900/40 rounded text-xs font-bold transition-colors", 902 children: [ 903 loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 12, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 12 }), 904 " Browse & Restore" 905 ] 906 } 907 ), 908 selectedFile && activeTab === "cloudflare" && (selectedFile.backupFilename === "dns_records.json" || selectedFile.backupFilename.startsWith("tunnel_config_")) && /* @__PURE__ */ jsxRuntimeExports.jsxs( 909 "button", 910 { 911 onClick: openCompareModal, 912 disabled: loading, 913 className: "flex items-center gap-2 px-3 py-1 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40 rounded text-xs font-bold transition-colors", 914 children: [
915 loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 12, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 12 }), 916 " Compare & Restore" 917 ] 918 } 919 ) 920 ] }), 921 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 relative bg-[#1e1e1e] overflow-hidden", children: selectedFile ? /* @__PURE__ */ jsxRuntimeExports.jsx( 922 "textarea", 923 { 924 readOnly: true, 925 value: fileContent, 926 className: "absolute inset-0 w-full h-full bg-transparent text-zinc-300 font-mono text-xs p-4 resize-none outline-none leading-relaxed", 927 spellCheck: false 928 } 929 ) : selectedBackup ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 text-zinc-300 space-y-4", children: [ 930 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 931 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 32, className: "text-indigo-400" }),
932 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 933 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-sm text-zinc-700 dark:text-zinc-300 mb-1", children: selectedBackup.filename }), 934 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-400", children: [ 935 "Created: ", 936 new Date(selectedBackup.timestamp).toLocaleString(void 0, { timeZoneName: "short" }) 937 ] }) 938 ] }) 939 ] }), 940 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4 bg-zinc-800/60 p-4 rounded-lg border border-zinc-700 text-xs", children: [ 941 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 942 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block", children: "Backup Type" }), 943 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-white uppercase", children: selectedBackup.type }) 944 ] }),
945 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 946 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block", children: "Retention Category" }), 947 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-indigo-400 uppercase", children: selectedBackup.category || "N/A" }) 948 ] }), 949 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 950 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block", children: "File Size" }), 951 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-white", children: [ 952 selectedBackup.size ? formatBytes(selectedBackup.size) : "N/A", 953 selectedBackup.fileCount ? ` (${selectedBackup.fileCount} files)` : "" 954 ] }) 955 ] }), 956 selectedBackup.metadata && selectedBackup.type === "database" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 957 /* @__P
957URE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block", children: "Snapshot Data" }), 958 /* @__PURE__ */ jsxRuntimeExports.jsx( 959 "button", 960 { 961 onClick: () => { 962 setDbStats(selectedBackup.metadata.tables); 963 setStatsTitle(`Snapshot: ${selectedBackup.filename}`); 964 setShowStatsModal(true); 965 }, 966 className: "mt-1 text-[11px] px-2.5 py-1 bg-indigo-500/20 text-indigo-300 font-bold rounded hover:bg-indigo-500/40 transition-colors", 967 children: "View Table Stats" 968 } 969 ) 970 ] }), 971 selectedBackup.metadata && selectedBackup.type === "settings" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2 mt-2 pt-2 border-t border-zinc-700/50", children: [ 972 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block mb-1", children: "Sections" }), 973 /* @__P
973URE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-wrap gap-1", children: (_a = selectedBackup.metadata.sections) == null ? void 0 : _a.map((sec) => /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] bg-zinc-700 text-zinc-300 px-1.5 py-0.5 rounded", children: [ 974 sec.tableName, 975 " (", 976 sec.rowCount, 977 ")" 978 ] }, sec.tableName)) }) 979 ] }), 980 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 981 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 block", children: "Status" }), 982 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-emerald-400", children: "Verified On Disk" }) 983 ] }) 984 ] }) 985 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full text-zinc-500", children: [ 986 /* @__PURE__ */ jsxRuntimeExports.jsx(Archive, { size: 48, className: "opacity-20 mb-2" }),
987 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm", children: "Select a backup file to view details or preview contents." }) 988 ] }) }) 989 ] }) 990 ] }), 991 settingsRestoreModalOpen && settingsRestoreData && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl border border-zinc-200 dark:border-zinc-800", children: [ 992 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800 shrink-0", children: [ 993 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 994 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "text-teal-500", size: 20 }), 995 "Browse & Restore Settings: ", 996 selectedBackup == null ? void 0 : selectedBackup.id 997 ] }),
998 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setSettingsRestoreModalOpen(false), className: "text-zinc-500 hover:text-red-500 p-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { size: 20 }) }) 999 ] }), 1000 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [ 1001 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900/50 rounded-lg p-3 text-amber-700 dark:text-amber-400 text-sm", children: [ 1002 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Warning:" }), 1003 " Restoring settings will overwrite the current live configuration in the database for the selected sections. Select the sections you wish to restore below." 1004 ] }), 1005 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: Object.keys(settingsRestoreData).map((section) => { 1006 const records = settingsRestoreData[section]; 1007 const isSelected = selectedSettingsSections.includes(section); 1008 if (!records || records.length === 0) return null; 1009 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1010 "div", 1011 { 1012 onClick: () => toggleSettingsSection(section), 1013 className: `p-3 rounded-xl border-2 cursor-pointer transition-colors flex items-center justify-between ${isSelected ? "border-teal-500 bg-teal-50 dark:bg-teal-900/20" : "border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700"}`, 1014 children: [
1015 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1016 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `font-bold text-sm ${isSelected ? "text-teal-700 dark:text-teal-300" : "text-zinc-900 dark:text-white"}`, children: section }), 1017 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-zinc-500", children: [ 1018 records.length, 1019 " records" 1020 ] }) 1021 ] }), 1022 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 1023 /* @__PURE__ */ jsxRuntimeExports.jsx( 1024 "button", 1025 { 1026 onClick: (e) => { 1027 e.stopPropagation(); 1028 handleBrowseSettingsTable(section, records); 1029 }, 1030 className: "px-2 py-1 bg-teal-500/20 text-teal-600 dark:text-teal-400 hover:bg-teal-500/40 text-[11px] font-bold rounded transition-colors", 1031 children: "Browse Data" 1032 } 1033 ), 1034 isSelected ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { className: "text-teal-500", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-5 h-5 rounded-full border-2 border-zinc-300 dark:border-zinc-600" }) 1035 ] }) 1036 ] 1037 }, 1038 section 1039 ); 1040 }) }) 1041 ] }),
1042 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 flex justify-end gap-3 shrink-0", children: [ 1043 /* @__PURE__ */ jsxRuntimeExports.jsx( 1044 "button", 1045 { 1046 onClick: () => setSettingsRestoreModalOpen(false), 1047 className: "px-4 py-2 font-bold text-sm text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors", 1048 children: "Cancel" 1049 } 1050 ), 1051 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1052 "button", 1053 { 1054 onClick: handleRestoreSettings, 1055 disabled: selectedSettingsSections.length === 0 || loading, 1056 className: "flex items-center gap-2 px-4 py-2 font-bold text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors disabled:opacity-50", 1057 children: [ 1058 loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 16, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }), 1059 "Restore ", 1060 selectedSettingsSections.length, 1061 " Sections" 1062 ] 1063 } 1064 ) 1065 ] }) 1066 ] }) }),
1067 compareModalOpen && diffData && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl w-full max-w-5xl h-[80vh] flex flex-col shadow-2xl border border-zinc-200 dark:border-zinc-800", children: [ 1068 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800 shrink-0", children: [ 1069 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 1070 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "text-amber-500", size: 20 }), 1071 "Compare & Restore: ", 1072 selectedFile == null ? void 0 : selectedFile.backupFilename 1073 ] }), 1074 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setCompareModalOpen(false), className: "text-zinc-500 hover:text-red-500 p-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { size: 20 }) }) 1075 ] }),
1076 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex min-h-0", children: [ 1077 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col border-r border-zinc-200 dark:border-zinc-800 min-w-0", children: [ 1078 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 bg-red-50 dark:bg-red-900/20 border-b border-zinc-200 dark:border-zinc-800 font-bold text-sm text-red-600 dark:text-red-400 flex items-center gap-2", children: [ 1079 /* @__PURE__ */ jsxRuntimeExports.jsx(Archive, { size: 16 }), 1080 " Backup Version (To Restore)" 1081 ] }), 1082 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-auto bg-[#1e1e1e] p-4 text-xs font-mono text-zinc-300", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: JSON.stringify(diffData.backup, null, 2) }) }) 1083 ] }), 1084 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [ 1085 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 bg-emerald-50 dark:bg-emerald-900/20 border-b border-zinc-200 dark:border-zinc-800 font-bold text-sm text-emerald-600 dark:text-emerald-400 flex items-center gap-2", children: [
1086 /* @__PURE__ */ jsxRuntimeExports.jsx(Cloud, { size: 16 }), 1087 " Live Version (Cloudflare)" 1088 ] }), 1089 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-auto bg-[#1e1e1e] p-4 text-xs font-mono text-zinc-300", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: JSON.stringify(diffData.live, null, 2) }) }) 1090 ] }) 1091 ] }), 1092 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 shrink-0 flex justify-between items-center bg-zinc-50 dark:bg-zinc-900/50", children: [ 1093 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-zinc-500", children: "Review the differences. Clicking Restore will apply the backup changes to Cloudflare." }), 1094 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3", children: [ 1095 /* @__PURE__ */ jsxRuntimeExports.jsx( 1096 "button", 1097 { 1098 onClick: () => setCompareModalOpen(false), 1099 className: "px-4 py-2 bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 text-zinc-800 dark:text-zinc-200 rounded-lg font-bold text-sm transition-colors", 1100 children: "Cancel" 1101 } 1102 ),
1103 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1104 "button", 1105 { 1106 onClick: handleRestoreCloudflare, 1107 className: "px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg font-bold text-sm shadow transition-colors flex items-center gap-2", 1108 children: [ 1109 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }), 1110 " Restore Selected Changes" 1111 ] 1112 } 1113 ) 1114 ] }) 1115 ] }) 1116 ] }) }), 1117 showStatsModal && dbStats && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl border border-zinc-200 dark:border-zinc-800", children: [ 1118 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800 shrink-0", children: [
1119 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 1120 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { className: "text-indigo-500", size: 20 }), 1121 statsTitle 1122 ] }), 1123 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowStatsModal(false), className: "text-zinc-500 hover:text-red-500 p-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { size: 20 }) }) 1124 ] }), 1125 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 overflow-y-auto flex-1", children: [ 1126 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 mb-4", children: statsTitle === "Live Database Statistics" ? "This table shows the current contents of the PostgreSQL database, representing exactly what gets packed into the DB Dump backups." : "This table shows the snapshot of the database contents at the time this backup was created." }), 1127 /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-sm border-collapse", children: [
1128 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "bg-zinc-100 dark:bg-zinc-800", children: [ 1129 /* @__PURE__ */ jsxRuntimeExports.jsx( 1130 "th", 1131 { 1132 className: "p-2 border border-zinc-200 dark:border-zinc-700 font-bold text-zinc-700 dark:text-zinc-300 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors", 1133 onClick: () => { 1134 if ((sortConfig == null ? void 0 : sortConfig.key) === "tableName") { 1135 setSortConfig({ key: "tableName", direction: sortConfig.direction === "asc" ? "desc" : "asc" }); 1136 } else { 1137 setSortConfig({ key: "tableName", direction: "asc" }); 1138 } 1139 }, 1140 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1141 "Table Name", 1142 (sortConfig == null ? void 0 : sortConfig.key) === "tableName" && (sortConfig.direction === "asc" ? /* @__P
1142URE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14 })) 1143 ] }) 1144 } 1145 ), 1146 /* @__PURE__ */ jsxRuntimeExports.jsx( 1147 "th", 1148 { 1149 className: "p-2 border border-zinc-200 dark:border-zinc-700 font-bold text-zinc-700 dark:text-zinc-300 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors", 1150 onClick: () => { 1151 if ((sortConfig == null ? void 0 : sortConfig.key) === "size") { 1152 setSortConfig({ key: "size", direction: sortConfig.direction === "asc" ? "desc" : "asc" }); 1153 } else { 1154 setSortConfig({ key: "size", direction: "desc" }); 1155 } 1156 }, 1157 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1158 "Size on Disk", 1159 (sortConfig == null ? void 0 : sortConfig.key) === "size" && (sortConfig.direction === "asc" ? /* @__P
1159URE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14 })) 1160 ] }) 1161 } 1162 ), 1163 /* @__PURE__ */ jsxRuntimeExports.jsx( 1164 "th", 1165 { 1166 className: "p-2 border border-zinc-200 dark:border-zinc-700 font-bold text-zinc-700 dark:text-zinc-300 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors", 1167 onClick: () => { 1168 if ((sortConfig == null ? void 0 : sortConfig.key) === "rowCount") { 1169 setSortConfig({ key: "rowCount", direction: sortConfig.direction === "asc" ? "desc" : "asc" }); 1170 } else { 1171 setSortConfig({ key: "rowCount", direction: "desc" }); 1172 } 1173 }, 1174 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1175 "Estimated Rows", 1176 (sortConfig == null ? void 0 : sortConfig.key) === "rowCount" && (sortConfig.direction === "asc" ? /* @__P
1176URE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14 })) 1177 ] }) 1178 } 1179 ), 1180 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-2 border border-zinc-200 dark:border-zinc-700 font-bold text-zinc-700 dark:text-zinc-300", children: "Action" }) 1181 ] }) }), 1182 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: [...dbStats].sort((a, b) => { 1183 if (!sortConfig) return 0; 1184 if (sortConfig.key === "tableName") { 1185 return sortConfig.direction === "asc" ? a.tableName.localeCompare(b.tableName) : b.tableName.localeCompare(a.tableName); 1186 } 1187 if (sortConfig.key === "rowCount") { 1188 const aVal = parseInt(a.rowCount) || 0; 1189 const bVal = parseInt(b.rowCount) || 0; 1190 return sortConfig.direction === "asc" ? aVal - bVal : bVal - aVal; 1191 } 1192 if (sortConfig.key === "size") { 1193 const parseSize = (size) => { 1194 const match = size.match(/([\d.]+)\s*([a-zA-Z]+)/); 1195 if (!match) return 0; 1196 const val = parseFloat(match[1]); 1197 const u = match[2].toUpperCase(); 1198 const mult = u.includes("GB") ? 1024 * 1024 * 1024 : u.includes("MB") ? 1024 * 1024 : u.includes("KB") ? 1024 : 1; 1199 return val * mult; 1200 }; 1201 const aVal = parseSize(a.size); 1202 const bVal = parseSize(b.size); 1203 return sortConfig.direction === "asc" ? aVal - bVal : bVal - aVal; 1204 } 1205 return 0; 1206 }).map((stat, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "hover:bg-zinc-50 dark:hover:bg-zinc-800/50", children: [
1207 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 border border-zinc-200 dark:border-zinc-700 font-mono text-zinc-800 dark:text-zinc-200", children: stat.tableName }), 1208 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 border border-zinc-200 dark:border-zinc-700 text-indigo-600 dark:text-indigo-400 font-semibold", children: stat.size }), 1209 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400", children: parseInt(stat.rowCount).toLocaleString() }), 1210 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 border border-zinc-200 dark:border-zinc-700", children: statsTitle !== "Live Database Statistics" && selectedBackup && /* @__PURE__ */ jsxRuntimeExports.jsx( 1211 "button", 1212 { 1213 onClick: () => handleBrowseTable(stat.tableName), 1214 className: "px-2 py-1 bg-indigo-500/20 text-indigo-400 hover:bg-indigo-500/40 text-[11px] font-bold rounded transition-colors", 1215 children: "Browse Data" 1216 } 1217 ) }) 1218 ] }, idx)) }) 1219 ] }) 1220 ] }) 1221 ] }) }), 1222 viewingTable && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl w-full max-w-6xl h-[85vh] flex flex-col shadow-2xl border border-zinc-200 dark:border-zinc-800", children: [
1223 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800 shrink-0", children: [ 1224 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 1225 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { className: "text-emerald-500", size: 20 }), 1226 "Browsing Table: ", 1227 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-indigo-400", children: viewingTable }) 1228 ] }), 1229 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-4 flex-1 justify-end mr-4", children: tableData && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-zinc-100 dark:bg-zinc-800 rounded-lg p-1 w-full max-w-md", children: [ 1230 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1231 "select", 1232 { 1233 value: tableSearchField, 1234 onChange: (e) => { 1235 setTableSearchField(e.target.value); 1236 setTableCurrentPage(1); 1237 }, 1238 className: "bg-white dark:bg-zinc-900 text-zinc-800 dark:text-zinc-200 border-none text-xs rounded py-1 px-2 focus:ring-1 focus:ring-indigo-500 max-w-[120px]", 1239 children: [
1240 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "ALL", children: "All Fields" }), 1241 tableData.columns.map((col) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: col, children: col }, col)) 1242 ] 1243 } 1244 ), 1245 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1", children: [ 1246 /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400", size: 14 }), 1247 /* @__PURE__ */ jsxRuntimeExports.jsx( 1248 "input", 1249 { 1250 type: "text", 1251 value: tableSearchQuery, 1252 onChange: (e) => { 1253 setTableSearchQuery(e.target.value); 1254 setTableCurrentPage(1); 1255 }, 1256 placeholder: "Search records...", 1257 className: "w-full bg-transparent border-none text-sm text-zinc-800 dark:text-zinc-200 py-1 pl-8 pr-2 focus:outline-none focus:ring-0" 1258 } 1259 ) 1260 ] }) 1261 ] }) }),
1262 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => { 1263 setViewingTable(null); 1264 setTableData(null); 1265 }, className: "text-zinc-500 hover:text-red-500 p-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleX, { size: 20 }) }) 1266 ] }), 1267 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-auto bg-zinc-50 dark:bg-zinc-900 p-0 relative", children: isTableLoading ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: [ 1268 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin" }), 1269 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-4 text-sm text-zinc-500 animate-pulse", children: "Scanning backup dump on the fly..." }) 1270 ] }) : tableData ? /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-xs border-collapse", children: [
1271 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { className: "bg-zinc-200 dark:bg-zinc-800 shadow-sm", children: tableData.columns.map((col, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-2 border border-zinc-300 dark:border-zinc-700 font-bold text-zinc-700 dark:text-zinc-300 whitespace-nowrap", children: col }, idx)) }) }), 1272 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: filteredTableRows.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { colSpan: tableData.columns.length, className: "p-8 text-center text-zinc-500", children: [ 1273 "No rows found ", 1274 tableSearchQuery && "matching your search ", 1275 "in this backup for table ", 1276 /* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: viewingTable }), 1277 "." 1278 ] }) }) : filteredTableRows.slice((tableCurrentPage - 1) * rowsPerPage, tableCurrentPage * rowsPerPage).map((row, rowIdx) => /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { className: "hover:bg-zinc-100 dark:hover:bg-zinc-800/40", children: tableData.columns.map((col, colIdx) => { 1279 const cellValue = row[col];
1280 const isNull = cellValue === "\\N" || cellValue === null || cellValue === void 0; 1281 const displayValue = isNull ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 italic", children: "null" }) : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue); 1282 return /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 border border-zinc-200 dark:border-zinc-700 text-zinc-800 dark:text-zinc-300 max-w-[200px] truncate", title: typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue || ""), children: displayValue }, colIdx); 1283 }) }, rowIdx)) }) 1284 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center text-zinc-500", children: "Failed to load table data." }) }), 1285 tableData && tableData.limitReached && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 bg-amber-50 dark:bg-amber-900/20 border-t border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-400 text-xs text-center shrink-0 flex items-center justify-center gap-2 font-semibold", children: [
1286 /* @__PURE__ */ jsxRuntimeExports.jsx(Archive, { size: 14 }), 1287 " Showing first 500 rows to prevent browser performance issues." 1288 ] }), 1289 tableData && filteredTableRows.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 border-t border-zinc-200 dark:border-zinc-800 shrink-0 bg-white dark:bg-zinc-900 flex justify-between items-center text-xs", children: [ 1290 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500", children: [ 1291 "Showing ", 1292 (tableCurrentPage - 1) * rowsPerPage + 1, 1293 " to ", 1294 Math.min(tableCurrentPage * rowsPerPage, filteredTableRows.length), 1295 " of ", 1296 filteredTableRows.length, 1297 " entries" 1298 ] }), 1299 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 1300 /* @__PURE__ */ jsxRuntimeExports.jsx( 1301 "button", 1302 { 1303 disabled: tableCurrentPage === 1, 1304 onClick: () => setTableCurrentPage(Math.max(1, tableCurrentPage - 1)), 1305 className: "px-3 py-1 bg-zinc-100 dark:bg-zinc-800 rounded font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50 transition-colors", 1306 children: "Previous" 1307 } 1308 ),
1309 /* @__PURE__ */ jsxRuntimeExports.jsx( 1310 "button", 1311 { 1312 disabled: tableCurrentPage * rowsPerPage >= filteredTableRows.length, 1313 onClick: () => setTableCurrentPage(tableCurrentPage + 1), 1314 className: "px-3 py-1 bg-zinc-100 dark:bg-zinc-800 rounded font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50 transition-colors", 1315 children: "Next" 1316 } 1317 ) 1318 ] }) 1319 ] }), 1320 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 shrink-0 bg-white dark:bg-zinc-900 flex justify-between items-center text-xs text-zinc-500", children: tableData && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 1321 "Loaded ", 1322 tableData.rows.length, 1323 " rows directly from SQL dump. ", 1324 tableSearchQuery && `Showing ${filteredTableRows.length} matching rows.` 1325 ] }) }) 1326 ] }) }) 1327 ] }); 1328}; 1329export { 1330 BackupManager as default 1331};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.