fix(scan): Fix-mode scanner + dialog suppression + cancel/timer + importer revive
Build backend images / build content-svc (push) Failing after 1m25s
Build backend images / build file-svc (push) Failing after 1m10s
Build backend images / build gateway (push) Failing after 56s
Build backend images / build identity-svc (push) Failing after 53s
Build backend images / build notification-svc (push) Failing after 57s
Build backend images / build render-svc (push) Failing after 48s
Build backend images / build studio-svc (push) Failing after 1m5s
Build backend images / build content-svc (push) Failing after 1m25s
Build backend images / build file-svc (push) Failing after 1m10s
Build backend images / build gateway (push) Failing after 56s
Build backend images / build identity-svc (push) Failing after 53s
Build backend images / build notification-svc (push) Failing after 57s
Build backend images / build render-svc (push) Failing after 48s
Build backend images / build studio-svc (push) Failing after 1m5s
- scan.jsx: app.beginSuppressDialogs() + clean quit (no AE hang on font/footage dialogs); FIX-mode branch parses frl_c(x)t/m(y) layer names → scenes by c(x); flexible/mockup keep comp-based walk; FR_SCAN_MODE selects. - render-svc: scan job carries project mode; cancel endpoint + node watchdog that kills AE on cancel; parseObjectURL handles minio:// (bucket in host); scan with no template fails cleanly; status guards so late results can't un-cancel. - content importer: revive soft-deleted scenes instead of duplicate-inserting (fixes scenes_project_id_key unique violation); orphan diff ignores deleted. - admin: scan dialog gets project-type picker + elapsed timer + Cancel button. - node-agent: AE-2026 wiring (host port 5010, host-reachable presign endpoint), FR_SCAN_MODE plumbing. docs/aep-template-convention.md: per-type naming + bundles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,9 +41,27 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
const [diff, setDiff] = useState<ImportDiff | null>(null);
|
||||
const [removeOrphans, setRemoveOrphans] = useState(false);
|
||||
const [overwrite, setOverwrite] = useState(true);
|
||||
const [mode, setMode] = useState("fix"); // project type → drives scan.jsx parsing convention
|
||||
const pollRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const jobRef = useRef<string | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
const fail = (m: string) => { setErr(m); setStep("error"); };
|
||||
const stopTimers = () => {
|
||||
if (pollRef.current) clearTimeout(pollRef.current);
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
};
|
||||
const fail = (m: string) => { stopTimers(); setErr(m); setStep("error"); };
|
||||
|
||||
const cancelScan = async () => {
|
||||
stopTimers();
|
||||
const jid = jobRef.current;
|
||||
jobRef.current = null;
|
||||
if (jid) {
|
||||
try { await fetch(`/api/admin/resource/template-scan-jobs/${jid}/cancel`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }); } catch {}
|
||||
}
|
||||
setErr("اسکن لغو شد."); setStep("error");
|
||||
};
|
||||
|
||||
// shared: send a scan to content for a dry-run diff
|
||||
const preview = useCallback(async (scanResult: unknown) => {
|
||||
@@ -60,6 +78,7 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
|
||||
// Quick scan — headless Go parser (no AE)
|
||||
const runQuick = async () => {
|
||||
jobRef.current = null; // quick scan is synchronous — no timer/cancel
|
||||
setStep("scanning"); setErr(null); setStatusMsg("در حال خواندن ساختار پروژه از فایل AEP…");
|
||||
const r = await fetch(`/api/admin/resource/template-scans/${projectId}/quick`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
|
||||
const d = await r.json().catch(() => null);
|
||||
@@ -69,19 +88,21 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
|
||||
// Full scan — queue an AE job on a render node, then poll
|
||||
const runFull = async () => {
|
||||
setStep("scanning"); setErr(null); setStatusMsg("در حال ارسال کار اسکن به نود افترافکت…");
|
||||
const r = await fetch(`/api/admin/resource/template-scans/${projectId}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
|
||||
setStep("scanning"); setErr(null); setElapsed(0); setStatusMsg("در حال ارسال کار اسکن به نود افترافکت…");
|
||||
const r = await fetch(`/api/admin/resource/template-scans/${projectId}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode }) });
|
||||
const d = await r.json().catch(() => null);
|
||||
if (!r.ok || !d?.id) { fail(d?.error ?? "ایجاد کار اسکن ناموفق بود"); return; }
|
||||
const jobId = d.id;
|
||||
jobRef.current = jobId;
|
||||
timerRef.current = setInterval(() => setElapsed((e) => e + 1), 1000);
|
||||
const started = Date.now();
|
||||
const poll = async () => {
|
||||
const jr = await fetch(`/api/admin/resource/template-scan-jobs/${jobId}`, { cache: "no-store" });
|
||||
const job = await jr.json().catch(() => null);
|
||||
if (!jr.ok) { fail(job?.error ?? "خطا در دریافت وضعیت اسکن"); return; }
|
||||
if (job.status === "done") { await preview(job.result); return; }
|
||||
if (job.status === "error") { fail("اسکن روی نود ناموفق بود: " + (job.error ?? "")); return; }
|
||||
if (Date.now() - started > 6 * 60 * 1000) { fail("اسکن طول کشید — آیا یک نود افترافکت آنلاین است؟"); return; }
|
||||
if (job.status === "done") { stopTimers(); jobRef.current = null; await preview(job.result); return; }
|
||||
if (job.status === "error" || job.status === "cancelled") { fail("اسکن روی نود ناموفق بود: " + (job.error ?? "")); return; }
|
||||
if (Date.now() - started > 10 * 60 * 1000) { fail("اسکن طول کشید — آیا یک نود افترافکت آنلاین است؟"); return; }
|
||||
setStatusMsg(job.status === "running" ? "در حال اجرای اسکریپت اسکن در افترافکت…" : "در صف اجرا روی نود…");
|
||||
pollRef.current = setTimeout(poll, 3000);
|
||||
};
|
||||
@@ -99,7 +120,7 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
setDiff(d); setStep("done"); onApplied();
|
||||
};
|
||||
|
||||
const close = () => { if (pollRef.current) clearTimeout(pollRef.current); onClose(); };
|
||||
const close = () => { stopTimers(); jobRef.current = null; onClose(); };
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-stretch justify-center bg-black/70 p-2 sm:p-6" dir="rtl" onClick={close}>
|
||||
@@ -118,9 +139,18 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
<div className="text-sm font-medium text-white">اسکن سریع (بدون افترافکت)</div>
|
||||
<div className="mt-0.5 text-xs text-gray-500">فقط نام صحنهها و مدتها را از فایل AEP میخواند. فوری، بدون نیاز به نود. رنگها/فونتها بعداً با اسکن کامل پر میشوند.</div>
|
||||
</button>
|
||||
<div className="rounded-lg border border-[#262b40] p-3">
|
||||
<label className="mb-1 block text-xs text-gray-400">نوع پروژه (برای اسکن کامل)</label>
|
||||
<select className="w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-2.5 py-1.5 text-sm text-gray-100 outline-none focus:border-indigo-500" value={mode} onChange={(e) => setMode(e.target.value)}>
|
||||
<option value="fix">Fix — صحنهها از نام لایهها (frl_c1t1)</option>
|
||||
<option value="flexible">Flexible — هر صحنه یک کامپوزیشن</option>
|
||||
<option value="mockup">Mockup</option>
|
||||
<option value="musicvisualizer">Music Visualizer</option>
|
||||
</select>
|
||||
</div>
|
||||
<button className="w-full rounded-lg border border-[#262b40] p-3 text-right hover:bg-[#161a2e]" onClick={runFull}>
|
||||
<div className="text-sm font-medium text-white">اسکن کامل (روی نود افترافکت)</div>
|
||||
<div className="mt-0.5 text-xs text-gray-500">صحنهها، عناصر (frl_/frd_)، فونتها، چینش و رنگها (frshare) را کامل میخواند. نیازمند یک نود افترافکت آنلاین است.</div>
|
||||
<div className="mt-0.5 text-xs text-gray-500">صحنهها، عناصر، فونتها، چینش و رنگها (frshare) را کامل میخواند. نیازمند یک نود افترافکت آنلاین است.</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -129,6 +159,12 @@ export function ProjectScanImport({ projectId, onClose, onApplied }: {
|
||||
<div className="flex flex-col items-center gap-3 py-10 text-center">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent" />
|
||||
<p className="text-sm text-gray-300">{statusMsg}</p>
|
||||
{step === "scanning" && jobRef.current && (
|
||||
<>
|
||||
<p className="text-xs text-gray-500">زمان سپریشده: {elapsed.toLocaleString("fa-IR")} ثانیه</p>
|
||||
<button className="rounded-lg border border-red-500/30 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10" onClick={cancelScan}>لغو اسکن</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user