diff --git a/web-ui/src/lib/api/precedent-library.ts b/web-ui/src/lib/api/precedent-library.ts index 9a46fc5..b9e8a2d 100644 --- a/web-ui/src/lib/api/precedent-library.ts +++ b/web-ui/src/lib/api/precedent-library.ts @@ -187,13 +187,37 @@ export function usePrecedents(filters: ListFilters = {}) { } /** A precedent is "active" while text/halacha extraction is in flight or - * queued for the local MCP worker. Used by the auto-refresh poller and - * by the row UI to disable destructive actions. */ + * legitimately queued for the local MCP worker. Used by the auto-refresh + * poller and by the row UI to disable destructive actions. + * + * Once a status is "completed" or "failed", the row is NEVER active — + * even if the corresponding `*_requested_at` timestamp still has a value. + * The worker is supposed to NULL it on success but in practice doesn't + * always, and treating those rows as active leaves them permanently + * undeletable. */ export function isPrecedentActive(p: Precedent): boolean { + // Text extraction if (p.extraction_status === "processing") return true; + + // Halacha extraction if (p.halacha_extraction_status === "processing") return true; - if (p.halacha_extraction_requested_at !== null) return true; - if (p.metadata_extraction_requested_at !== null) return true; + if ( + p.halacha_extraction_status === "pending" && + p.halacha_extraction_requested_at !== null + ) { + return true; + } + + // Metadata extraction has no status column — only the timestamp. + // Treat as active only when extraction hasn't yet fully completed + // (otherwise stale timestamps linger after success). + if ( + p.metadata_extraction_requested_at !== null && + p.extraction_status !== "completed" + ) { + return true; + } + return false; }