notes/src/components/IntegrityReport.tsx

233 lines
9.5 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from "react";
import { useVault } from "../App";
import {
scanIntegrity,
computeChecksums,
verifyChecksums,
findOrphanAttachments,
createBackup,
listBackups,
restoreBackup,
type IntegrityIssue,
type ChecksumMismatch,
type OrphanAttachment,
type BackupEntry,
} from "../lib/commands";
export default function IntegrityReport({ onClose }: { onClose: () => void }) {
const { vaultPath } = useVault();
const [issues, setIssues] = useState<IntegrityIssue[]>([]);
const [mismatches, setMismatches] = useState<ChecksumMismatch[]>([]);
const [orphans, setOrphans] = useState<OrphanAttachment[]>([]);
const [backups, setBackups] = useState<BackupEntry[]>([]);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState<"scan" | "checksums" | "orphans" | "backups">("scan");
const [status, setStatus] = useState("");
useEffect(() => {
runScan();
loadBackups();
}, [vaultPath]);
const runScan = async () => {
if (!vaultPath) return;
setLoading(true);
setStatus("Scanning vault...");
try {
const result = await scanIntegrity(vaultPath);
setIssues(result);
setStatus(`Found ${result.length} issue(s)`);
} catch (e) {
setStatus(`Scan failed: ${e}`);
}
setLoading(false);
};
const runChecksumVerify = async () => {
if (!vaultPath) return;
setLoading(true);
setStatus("Computing checksums...");
try {
await computeChecksums(vaultPath);
const result = await verifyChecksums(vaultPath);
setMismatches(result);
setStatus(result.length === 0 ? "All checksums valid ✓" : `${result.length} mismatch(es) found`);
} catch (e) {
setStatus(`Checksum verification failed: ${e}`);
}
setLoading(false);
};
const runOrphanScan = async () => {
if (!vaultPath) return;
setLoading(true);
setStatus("Scanning attachments...");
try {
const result = await findOrphanAttachments(vaultPath);
setOrphans(result);
setStatus(result.length === 0 ? "No orphan attachments ✓" : `${result.length} orphan(s) found`);
} catch (e) {
setStatus(`Orphan scan failed: ${e}`);
}
setLoading(false);
};
const handleCreateBackup = async () => {
if (!vaultPath) return;
setLoading(true);
setStatus("Creating backup...");
try {
const name = await createBackup(vaultPath);
setStatus(`Backup created: ${name}`);
loadBackups();
} catch (e) {
setStatus(`Backup failed: ${e}`);
}
setLoading(false);
};
const loadBackups = async () => {
if (!vaultPath) return;
try {
const list = await listBackups(vaultPath);
setBackups(list);
} catch { /* ignore */ }
};
const handleRestore = async (name: string) => {
if (!vaultPath) return;
if (!confirm(`Restore from ${name}? This will overwrite current files.`)) return;
setLoading(true);
try {
const count = await restoreBackup(vaultPath, name);
setStatus(`Restored ${count} files from ${name}`);
} catch (e) {
setStatus(`Restore failed: ${e}`);
}
setLoading(false);
};
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1048576).toFixed(1)} MB`;
};
const severityIcon = (s: string) => s === "error" ? "🔴" : s === "warning" ? "🟡" : "🔵";
return (
<div className="integrity-report">
<div className="integrity-header">
<h3>🛡 Integrity Report</h3>
<button className="panel-close-btn" onClick={onClose}></button>
</div>
<div className="integrity-tabs">
{(["scan", "checksums", "orphans", "backups"] as const).map((tab) => (
<button
key={tab}
className={`integrity-tab ${activeTab === tab ? "active" : ""}`}
onClick={() => {
setActiveTab(tab);
if (tab === "checksums") runChecksumVerify();
if (tab === "orphans") runOrphanScan();
}}
>
{tab === "scan" ? "🔍 Scan" : tab === "checksums" ? "🔐 Checksums"
: tab === "orphans" ? "📎 Orphans" : "💾 Backups"}
</button>
))}
</div>
{status && <div className="integrity-status">{loading ? "⏳ " : ""}{status}</div>}
<div className="integrity-body">
{activeTab === "scan" && (
<>
<button className="integrity-action-btn" onClick={runScan} disabled={loading}>Re-scan Vault</button>
{issues.length === 0 && !loading && (
<div className="integrity-empty"> No issues found vault is clean</div>
)}
{issues.map((issue, i) => (
<div key={i} className="integrity-issue">
<span className="integrity-severity">{severityIcon(issue.severity)}</span>
<div className="integrity-issue-body">
<span className="integrity-issue-path">{issue.path}</span>
<span className="integrity-issue-desc">{issue.description}</span>
</div>
</div>
))}
</>
)}
{activeTab === "checksums" && (
<>
<button className="integrity-action-btn" onClick={runChecksumVerify} disabled={loading}>
Recompute & Verify
</button>
{mismatches.length === 0 && !loading && (
<div className="integrity-empty"> All checksums match</div>
)}
{mismatches.map((m, i) => (
<div key={i} className="integrity-issue">
<span className="integrity-severity"></span>
<div className="integrity-issue-body">
<span className="integrity-issue-path">{m.path}</span>
<span className="integrity-issue-desc">
Expected: {m.expected.slice(0, 12)} Got: {m.actual.slice(0, 12)}
</span>
</div>
</div>
))}
</>
)}
{activeTab === "orphans" && (
<>
<button className="integrity-action-btn" onClick={runOrphanScan} disabled={loading}>
Rescan Orphans
</button>
{orphans.length === 0 && !loading && (
<div className="integrity-empty"> No orphan attachments</div>
)}
{orphans.map((o, i) => (
<div key={i} className="integrity-issue">
<span className="integrity-severity">📎</span>
<div className="integrity-issue-body">
<span className="integrity-issue-path">{o.path}</span>
<span className="integrity-issue-desc">{formatSize(o.size)}</span>
</div>
</div>
))}
</>
)}
{activeTab === "backups" && (
<>
<button className="integrity-action-btn" onClick={handleCreateBackup} disabled={loading}>
Create Backup Now
</button>
{backups.length === 0 && !loading && (
<div className="integrity-empty">No backups yet</div>
)}
{backups.map((b, i) => (
<div key={i} className="integrity-issue">
<span className="integrity-severity">💾</span>
<div className="integrity-issue-body">
<span className="integrity-issue-path">{b.name}</span>
<span className="integrity-issue-desc">
{b.created} · {formatSize(b.size)}
</span>
</div>
<button className="integrity-restore-btn" onClick={() => handleRestore(b.name)}>
Restore
</button>
</div>
))}
</>
)}
</div>
</div>
);
}