notes/src/components/IntegrityReport.tsx

232 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}