fix(import): decouple scan from HTTP connection, prevent duplicate imports
- Add scan_manager: background asyncio task + Redis event store so scans survive UI navigation; SSE stream reads from Redis and is reconnectable - Replace SSE-tied scan endpoint with POST /nc-scan/start + GET /nc-scan/stream - Fix frontend: AbortController + useEffect cleanup cancels stream on unmount without stopping the server-side scan - Add unique constraint on audio_versions.nc_file_path (migration 0009) to prevent duplicate imports from concurrent scans; handle IntegrityError gracefully in nc_scan with rollback + skip - Fix API health check: use plain python instead of uv (not in dev image) - Optimize Taskfile: fix duplicate dev:restart, add dev:fresh/dev:rebuild/ dev:status, migrate uses run --rm, check includes typecheck Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "../api/client";
|
||||
@@ -278,6 +278,11 @@ function StorageSection({ bandId, band, amAdmin }: { bandId: string; band: Band;
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanProgress, setScanProgress] = useState<string | null>(null);
|
||||
const [scanMsg, setScanMsg] = useState<string | null>(null);
|
||||
const scanAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Cancel the stream reader on unmount so the component doesn't update after
|
||||
// the user navigates away. The background scan on the server keeps running.
|
||||
useEffect(() => () => { scanAbortRef.current?.abort(); }, []);
|
||||
|
||||
const { data: storageConfigs, isLoading: storageLoading } = useQuery({
|
||||
queryKey: ["storage", bandId],
|
||||
@@ -310,8 +315,33 @@ function StorageSection({ bandId, band, amAdmin }: { bandId: string; band: Band;
|
||||
async function startScan() {
|
||||
if (scanning) return;
|
||||
setScanning(true); setScanMsg(null); setScanProgress("Starting scan…");
|
||||
|
||||
// Start the background scan on the server
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/bands/${bandId}/nc-scan/stream`, { credentials: "include" });
|
||||
const startResp = await fetch(`/api/v1/bands/${bandId}/nc-scan/start`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (startResp.status === 409) {
|
||||
setScanProgress("Scan already running…");
|
||||
// Fall through to stream — a scan is already in progress
|
||||
} else if (!startResp.ok) {
|
||||
throw new Error(`HTTP ${startResp.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setScanMsg(err instanceof Error ? err.message : "Failed to start scan");
|
||||
setScanning(false); setScanProgress(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to the event stream. Aborting this does NOT stop the server scan.
|
||||
const controller = new AbortController();
|
||||
scanAbortRef.current = controller;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/bands/${bandId}/nc-scan/stream`, {
|
||||
credentials: "include",
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -338,8 +368,12 @@ function StorageSection({ bandId, band, amAdmin }: { bandId: string; band: Band;
|
||||
} else if (ev.type === "error") setScanMsg(`Scan error: ${ev.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) { setScanMsg(err instanceof Error ? err.message : "Scan failed"); }
|
||||
finally { setScanning(false); setScanProgress(null); }
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return; // clean unmount, scan still running
|
||||
setScanMsg(err instanceof Error ? err.message : "Scan failed");
|
||||
} finally {
|
||||
setScanning(false); setScanProgress(null);
|
||||
}
|
||||
}
|
||||
|
||||
const canConnect = ncUrl.trim() && ncUsername.trim() && ncPassword;
|
||||
|
||||
Reference in New Issue
Block a user