diff --git a/docs/superpowers/specs/2026-09-04-inventory-background-removal-design.md b/docs/superpowers/specs/2026-09-04-inventory-background-removal-design.md index dadcec9..f99d310 100644 --- a/docs/superpowers/specs/2026-09-04-inventory-background-removal-design.md +++ b/docs/superpowers/specs/2026-09-04-inventory-background-removal-design.md @@ -55,10 +55,12 @@ export interface RestoreSummary { total: number; /** How many were put back. Images that were never cut out are skipped, not counted. */ restored: number; + /** Whether it stopped early because one of them failed. */ + failed: boolean; } ``` -It has no `failed`, because restoring cannot fail the way removing can: it is a database swap with no sidecar involved, and an image that was never cut out is skipped rather than being an error. +It carries `failed` too, for the same reason `RemovalSummary` does. Restoring is a database swap with no sidecar involved, so it fails far less often than removing does — but a database error partway through a multi-photo restore is still a real possibility, and rethrowing it would turn a partial success into an opaque 500 that discards how far the restore got. An image that was never cut out is skipped rather than being an error either way. ### The endpoints @@ -74,11 +76,11 @@ Non-200 is reserved for not being able to try at all, which here means only an u One button per item, in the "Existing Images" block, beside the per-thumbnail delete buttons rather than on them. -Its label comes from the same single source of truth the review queue uses: **Restore originals** when every image already carries an `original_image_path`, **Remove backgrounds** otherwise. There is no second flag and no stored state — the images already say which they are. +Remove and Restore are two independently-gated buttons, not two labels for one button — there is no second flag and no stored state, the images already say which they are, but a mixed item genuinely needs both offered at once. -The "otherwise" deliberately covers the mixed case, which is not hypothetical: it is exactly what a partial failure leaves behind. With two of four cut out the button reads **Remove backgrounds**, which is the action that finishes the job, and pressing it skips the two that already succeeded. A button offering to restore at that point would be offering the wrong half of the work. +**Remove backgrounds** is rendered when the server reports the feature configured *and* at least one image is not yet cut out. An unconfigured environment shows no Remove button rather than one that reports zero of four done every time. -Rendered only when the server reports the feature configured, exactly as the review queue's control is. An unconfigured environment shows no button rather than one that reports zero of four done every time. +**Restore originals** is rendered whenever at least one image on the item already carries an `original_image_path` — regardless of whether the feature is currently configured. That is deliberate, not an oversight: the mixed state a partial removal leaves behind is not hypothetical, and neither is `REMBG_URL` being unset after some photos were already cut out. Either way, gating Restore on `backgroundRemoval` would strand those cut-out photos with no way back. On a partly cut-out item both buttons appear together, and that is correct — Remove finishes the job on what is left, Restore undoes what is already done. On a partial result the admin is told plainly — "2 of 4 photos done" — with the button still there to try again. @@ -93,7 +95,7 @@ The obvious-looking reuse is wrong. The review queue's control is per photo and | What happens | Result | |---|---| | Unreadable or absent item id | 404. Nothing touched. | -| Feature not configured | No button. The endpoint still answers, reporting `total` with `removed: 0`. | +| Feature not configured | No Remove button. Restore is still offered, and still works, whenever an image is already cut out. The remove-backgrounds endpoint still answers, reporting `total` with `removed: 0`. | | Sidecar will not answer | 200, `failed: true`, `removed: 0`. Nothing was touched. The reason is logged. | | A file cannot be read | 200, `failed: true`, `removed` short of `total`. Photos done before it keep their cut-outs. | | Some succeeded, then one failed | 200, `failed: true`, `removed` short of `total`. The admin retries; the second pass skips what already worked. |