From 786996b7acebcde89baddabd86cf62286bc00b34 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Thu, 3 Sep 2026 13:41:55 -0500 Subject: [PATCH] fix(admin): keep Restore original working after REMBG_URL is unset (#281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DraftQueue background-removal control gated both "Remove background" and "Restore original" on the same `backgroundRemoval` flag, which only reflects whether a sidecar is currently configured. Restoring is a pure database swap and never calls the sidecar, so once photos had already been cut out and REMBG_URL was later removed from the stack, the admin was left looking at a cut-out photo with no control at all and no way back to the original short of a hand-written SQL UPDATE — directly breaking the "the original is always restorable" invariant the feature is built on. DraftCard now computes `enabled` per photo as `backgroundRemoval || image.original_image_path !== null`, so Restore original stays available whenever a photo has an original regardless of whether the sidecar is configured, while Remove background still requires a configured sidecar. Also corrected the docstring on `DraftQueueResponse.backgroundRemoval` in draftsApi.ts, which claimed the flag hides "the control" generically — it only ever governed the remove-background control. Co-Authored-By: Claude Opus 5 --- frontend/src/admin/DraftQueue.tsx | 12 +++++++++++- frontend/src/admin/draftsApi.ts | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/admin/DraftQueue.tsx b/frontend/src/admin/DraftQueue.tsx index 96f729e..07d65c5 100644 --- a/frontend/src/admin/DraftQueue.tsx +++ b/frontend/src/admin/DraftQueue.tsx @@ -42,6 +42,16 @@ function priceLabel(source: PriceSource): string { * * The label is the state. `original_image_path` is the only thing consulted, * so there is no second flag that could disagree with what the button does. + * + * `enabled` is computed by the caller as `backgroundRemoval || cutOut`, not as + * `backgroundRemoval` alone. The two branches this button offers need opposite + * things: removing calls the sidecar and has nothing to do without one, but + * restoring is a pure database swap that needs no sidecar at all. Gating both + * on `backgroundRemoval` hides Restore the moment REMBG_URL is unset — which + * happens for real when the sidecar is decommissioned after photos were + * already cut out — leaving a cut-out photo with no control and no way back to + * the original short of hand-editing the database. That breaks the invariant + * this whole feature rests on: the original is always restorable. */ function DraftPhoto({ image, @@ -148,7 +158,7 @@ function DraftCard({ key={image.id} image={image} itemId={draft.item_id} - enabled={backgroundRemoval} + enabled={backgroundRemoval || image.original_image_path !== null} onChanged={onChanged} /> ))} diff --git a/frontend/src/admin/draftsApi.ts b/frontend/src/admin/draftsApi.ts index aaa3555..b5a5eaa 100644 --- a/frontend/src/admin/draftsApi.ts +++ b/frontend/src/admin/draftsApi.ts @@ -41,8 +41,11 @@ export interface DraftQueueResponse { drafts: Draft[]; /** * Whether a background-removal sidecar is configured. False hides the - * control rather than showing one that would answer 502 — an environment - * without a sidecar is a working environment. + * remove-background control rather than showing one that would answer 502 + * — an environment without a sidecar is a working environment. It does + * *not* hide Restore original: that endpoint is a pure database swap and + * needs no sidecar, so DraftQueue shows it whenever a photo has an + * original to restore, regardless of this flag. */ backgroundRemoval: boolean; }