ta-organizerr/ui/src/components/RecoveryModal.svelte
2026-02-04 16:09:05 -05:00

223 lines
9.1 KiB
Svelte

<script lang="ts">
import { createEventDispatcher, onMount, onDestroy } from "svelte";
const dispatch = createEventDispatcher();
let activeTab: "unindexed" | "rescue" | "redundant" | "lost" = "unindexed";
let scanning = false;
let status = "idle";
let results: any = { unindexed: [], rescue: [], redundant: [], lost: [] };
let pollInterval: ReturnType<typeof setInterval>;
async function startScan() {
scanning = true;
try {
await fetch("/api/recovery/scan", {
method: "POST",
});
pollResults();
} catch (e) {
alert("Scan start failed: " + e);
scanning = false;
}
}
function pollResults() {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const res = await fetch("/api/recovery/poll");
const data = await res.json();
status = data.status;
if (data.status === "done") {
results = data.results || results;
scanning = false;
clearInterval(pollInterval);
} else if (data.status === "error") {
scanning = false;
clearInterval(pollInterval);
alert("Scan error: " + data.results);
}
} catch (e) {
console.error(e);
}
}, 2000);
}
onDestroy(() => {
if (pollInterval) clearInterval(pollInterval);
});
async function recoverFile(path: string, isBatch = false) {
// Implementation mirrors existing JS logic
if (!isBatch && !confirm("Recover this file?")) return;
try {
const res = await fetch("/api/recovery/start", {
method: "POST",
body: JSON.stringify({ filepath: path }),
headers: { "Content-Type": "application/json" },
});
const d = await res.json();
if (!isBatch) {
alert(d.message);
startScan();
} // Refresh
} catch (e) {
alert(e);
}
}
async function deleteFile(path: string) {
if (!confirm("Delete file? This cannot be undone.")) return;
try {
const res = await fetch("/api/recovery/delete", {
method: "POST",
body: JSON.stringify({ filepath: path }),
headers: { "Content-Type": "application/json" },
});
const d = await res.json();
if (d.success) {
alert("Deleted.");
startScan();
} else alert("Error: " + d.error);
} catch (e) {
alert(e);
}
}
</script>
<div
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
>
<div
class="bg-cyber-card border border-neon-cyan/30 rounded-xl w-full max-w-5xl h-[80vh] flex flex-col shadow-[0_0_50px_rgba(0,243,255,0.1)]"
>
<!-- Header -->
<div
class="p-5 border-b border-gray-800 flex justify-between items-center"
>
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<i class="bi bi-bandaid text-neon-pink"></i> Advanced Recovery
</h2>
<button
on:click={() => dispatch("close")}
class="text-gray-500 hover:text-white"
aria-label="Close"><i class="bi bi-x-lg"></i></button
>
</div>
<!-- Controls -->
<div class="p-4 bg-gray-900/50 flex justify-between items-center">
<button
class="btn-primary px-6 py-2 rounded font-bold text-black bg-neon-cyan hover:bg-white transition-colors"
on:click={startScan}
disabled={scanning}
>
{scanning ? "Scanning..." : "Run System Scan"}
</button>
<div class="text-xs text-mono text-gray-500">Status: {status}</div>
</div>
<!-- Tabs -->
<div class="flex border-b border-gray-800 px-4">
{#each ["unindexed", "rescue", "redundant", "lost"] as tab}
<button
class="px-4 py-3 text-sm font-semibold capitalize border-b-2 transition-colors flex items-center gap-2
{activeTab === tab
? 'border-neon-cyan text-neon-cyan'
: 'border-transparent text-gray-500 hover:text-gray-300'}"
on:click={() => (activeTab = tab as any)}
>
{tab}
<span class="bg-gray-800 text-xs px-1.5 rounded-full"
>{results[tab]?.length || 0}</span
>
</button>
{/each}
</div>
<!-- Content -->
<div class="flex-grow overflow-y-auto p-4 bg-black/30">
{#if scanning && (!results[activeTab] || results[activeTab].length === 0)}
<div
class="flex items-center justify-center h-full text-neon-cyan animate-pulse"
>
Scanning...
</div>
{:else}
<table class="w-full text-left text-xs text-gray-300 font-mono">
<thead>
<tr class="text-gray-500 border-b border-gray-800">
<th class="p-3">Video ID</th>
<th class="p-3">Filename / Path</th>
<th class="p-3">Size/Info</th>
<th class="p-3 text-right">Action</th>
</tr>
</thead>
<tbody>
{#each results[activeTab] || [] as item}
<tr
class="border-b border-gray-800/50 hover:bg-white/5"
>
<td class="p-3 text-neon-pink"
>{item.video_id}</td
>
<td
class="p-3 truncate max-w-[300px]"
title={item.path}
>{item.filename || item.path}</td
>
<td class="p-3"
>{item.size_mb
? item.size_mb + " MB"
: item.ta_source || "-"}</td
>
<td class="p-3 text-right">
{#if activeTab === "unindexed"}
<button
class="text-neon-green hover:underline"
on:click={() =>
recoverFile(item.path)}
>Recover</button
>
{:else if activeTab === "redundant"}
<button
class="text-red-500 hover:underline"
on:click={() =>
deleteFile(item.path)}
>Delete</button
>
{:else if activeTab === "lost"}
<button
class="text-neon-yellow hover:underline mr-2"
>Force</button
>
<button
class="text-red-500 hover:underline"
on:click={() =>
deleteFile(item.path)}
>Delete</button
>
{:else}
<button
class="text-neon-pink hover:underline"
>Rescue</button
>
{/if}
</td>
</tr>
{/each}
{#if !results[activeTab]?.length}
<tr
><td
colspan="4"
class="p-10 text-center text-gray-600"
>No items found.</td
></tr
>
{/if}
</tbody>
</table>
{/if}
</div>
</div>
</div>