diff --git a/scripts/test-apply.mjs b/scripts/test-apply.mjs new file mode 100644 index 0000000..eb86734 --- /dev/null +++ b/scripts/test-apply.mjs @@ -0,0 +1,16 @@ +import crypto from "node:crypto"; +const GW = "http://172.28.144.1:8088"; +const SECRET = "p9Xv7Lm2Qq8Nz4TfKc1Hs6YwRe3Ud0BafwefWEFw324234QEWF"; +const PID = "90571220-e6b2-44f3-b5a2-ed65999bf525"; +const b64 = (b) => Buffer.from(b).toString("base64url"); +const now = Math.floor(Date.now() / 1000); +const h = b64(JSON.stringify({ alg: "HS256", typ: "JWT" })); +const p = b64(JSON.stringify({ sub: "00000000-0000-0000-0000-0000000000aa", tenant_id: "00000000-0000-0000-0000-0000000000bb", tenant_slug: "flatrender", is_admin: "true", role: "Admin", iss: "flatrender-identity", aud: "flatrender", exp: now + 3600, iat: now })); +const sig = crypto.createHmac("sha256", SECRET).update(`${h}.${p}`).digest("base64url"); +const H = { "Content-Type": "application/json", Authorization: `Bearer ${h}.${p}.${sig}` }; + +const scan = { source: "ae-jsx", scenes: [{ key: "c1", title: "Scene 1", scene_type: "Normal", elements: [{ key: "frl_c1t1", title: "frl_c1t1", type: "Text", default_value: "hello" }], colors: [] }], shared_colors: [] }; + +const res = await fetch(`${GW}/v1/projects/${PID}/scan/apply`, { method: "POST", headers: H, body: JSON.stringify({ scan, options: { overwrite_existing: true } }) }); +console.log("apply status:", res.status); +console.log("body:", (await res.text()).slice(0, 400)); diff --git a/services/content/FlatRender.ContentSvc/Application/Services/AepImportService.cs b/services/content/FlatRender.ContentSvc/Application/Services/AepImportService.cs index b440d4f..f36645c 100644 --- a/services/content/FlatRender.ContentSvc/Application/Services/AepImportService.cs +++ b/services/content/FlatRender.ContentSvc/Application/Services/AepImportService.cs @@ -24,7 +24,10 @@ public class AepImportService(ContentDbContext db) // Load ALL scenes incl. soft-deleted: the UNIQUE(project_id,key) constraint // covers deleted rows too, so a re-scan must REVIVE a soft-deleted match // rather than insert a duplicate (which would violate the constraint). + // IgnoreQueryFilters() is REQUIRED — Scene has a global DeletedAt==null filter + // that would otherwise hide the soft-deleted rows we need to revive. var existing = await db.Scenes + .IgnoreQueryFilters() .Where(s => s.ProjectId == projectId) .Include(s => s.ContentElements) .Include(s => s.ColorElements) diff --git a/services/node-agent/internal/runner/aecrash.go b/services/node-agent/internal/runner/aecrash.go new file mode 100644 index 0000000..a7feaed --- /dev/null +++ b/services/node-agent/internal/runner/aecrash.go @@ -0,0 +1,30 @@ +package runner + +import ( + "os" + "path/filepath" +) + +// ClearAECrashState removes After Effects' session crash-recovery marker +// (SCRPriorState.json) from every AE prefs version dir. AE checks this file at +// startup; if it indicates an unclean prior session it shows the blocking +// "Crash Repair Options" dialog — which would hang a headless afterfx/aerender +// launch. Deleting it (vs. wiping all prefs) keeps the node's prefs intact. +// +// Safe no-op when APPDATA is unset (non-Windows / dev). +func ClearAECrashState() { + appData := os.Getenv("APPDATA") + if appData == "" { + return + } + base := filepath.Join(appData, "Adobe", "After Effects") + entries, err := os.ReadDir(base) + if err != nil { + return + } + for _, e := range entries { + if e.IsDir() { + _ = os.Remove(filepath.Join(base, e.Name(), "SCRPriorState.json")) + } + } +} diff --git a/services/node-agent/internal/runner/scan.go b/services/node-agent/internal/runner/scan.go index b6c923e..150e376 100644 --- a/services/node-agent/internal/runner/scan.go +++ b/services/node-agent/internal/runner/scan.go @@ -33,6 +33,7 @@ func WriteScanScript(workDir string) (string, error) { // afterfx -r runs the script and the script calls app.quit(); we still poll for the // output file because afterfx can return before the file is flushed. func RunScan(ctx context.Context, afterfxPath, aepPath, workDir, outPath, mode string) ([]byte, error) { + ClearAECrashState() // avoid the "Crash Repair Options" dialog hanging a headless launch scriptPath, err := WriteScanScript(workDir) if err != nil { return nil, fmt.Errorf("write scan script: %w", err)