feat: make catalog-pages dedup a choosable step in Fix alles
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m3s

The maintenance "Fix alles" now has a checkbox to include or skip the
duplicate catalog_pages merge, so admins can choose whether to run it.
removeDuplicates takes an includePages flag and fixEverything threads it
through to the action.
This commit is contained in:
openhands committed 2026-08-24 18:35:00 +02:00
1 parent 6dced0fb54
commit 3a292a44f1
3 files changed
+44 -10

No files matched your search

+4 -4
View File
@@ -99,12 +99,12 @@ export async function applyAlignIdsAction(): Promise<
} }
} }
export async function fixEverythingAction(): Promise< export async function fixEverythingAction(input?: {
ActionResult<maintenance.FixAllResult> dedupePages?: boolean;
> { }): Promise<ActionResult<maintenance.FixAllResult>> {
try { try {
await guard(); await guard();
return actionOk(await maintenance.fixEverything()); return actionOk(await maintenance.fixEverything(input ?? {}));
} catch (e) { } catch (e) {
return handleActionError(e); return handleActionError(e);
} }
@@ -22,6 +22,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
interface Health { interface Health {
itemsBaseTotal: number; itemsBaseTotal: number;
@@ -38,6 +39,7 @@ export function CatalogMaintenancePanel() {
const [health, setHealth] = useState<Health | null>(null); const [health, setHealth] = useState<Health | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState<string | null>(null); const [busy, setBusy] = useState<string | null>(null);
const [dedupePages, setDedupePages] = useState(true);
const [alignPreview, setAlignPreview] = useState<{ const [alignPreview, setAlignPreview] = useState<{
moves: number; moves: number;
inserted: number; inserted: number;
@@ -286,12 +288,38 @@ export function CatalogMaintenancePanel() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="mb-3 flex items-center gap-2 rounded-md border p-2">
<Checkbox
id="dedupe-pages"
checked={dedupePages}
onCheckedChange={(v) => setDedupePages(v === true)}
/>
<label
htmlFor="dedupe-pages"
className="text-sm text-muted-foreground"
>
Also merge duplicate catalog pages (double links)
</label>
</div>
<Button <Button
variant="destructive" variant="destructive"
disabled={busy !== null} disabled={busy !== null}
onClick={() => onClick={async () => {
void run("all", fixEverythingAction, "All repairs applied") setBusy("all");
try {
const res = await fixEverythingAction({ dedupePages });
if (res.ok) {
toast.success("All repairs applied");
await refresh();
} else {
toast.error(res.error ?? "Failed");
} }
} catch (e) {
toast.error(String(e));
} finally {
setBusy(null);
}
}}
> >
Fix everything now Fix everything now
</Button> </Button>
+10 -4
View File
@@ -388,9 +388,13 @@ async function dedupCatalogTable(table: string): Promise<number> {
* product on the same page). The page merge runs before the row dedup so items * product on the same page). The page merge runs before the row dedup so items
* moved onto the survivor are still de-duplicated within the page afterwards. * moved onto the survivor are still de-duplicated within the page afterwards.
*/ */
export async function removeDuplicates(): Promise<DedupResult> { export async function removeDuplicates(
includePages = true,
): Promise<DedupResult> {
const ib = await removeDuplicateItemsBase(); const ib = await removeDuplicateItemsBase();
const pageDedup = await dedupeCatalogPages(); const pageDedup = includePages
? await dedupeCatalogPages()
: { groups: 0, removed: 0, remapped: 0 };
const catalogRemoved = await dedupCatalogTable("catalog_items"); const catalogRemoved = await dedupCatalogTable("catalog_items");
const catalogBcRemoved = await dedupCatalogTable("catalog_items_bc"); const catalogBcRemoved = await dedupCatalogTable("catalog_items_bc");
return { return {
@@ -630,12 +634,14 @@ export interface FixAllResult {
} }
/** Run every repair in a sensible order, then return before/after health. */ /** Run every repair in a sensible order, then return before/after health. */
export async function fixEverything(): Promise<FixAllResult> { export async function fixEverything(
opts: { dedupePages?: boolean } = {},
): Promise<FixAllResult> {
const healthBefore = await getFurniHealth(); const healthBefore = await getFurniHealth();
const sprite = await fixSpriteIds(); const sprite = await fixSpriteIds();
const offers = await fixCatalogOffers(); const offers = await fixCatalogOffers();
const reconcile = await reconcileIds(); const reconcile = await reconcileIds();
const dedup = await removeDuplicates(); const dedup = await removeDuplicates(opts.dedupePages ?? true);
const align = await forceItemsBaseIdsToFurnidata(true); const align = await forceItemsBaseIdsToFurnidata(true);
const healthAfter = await getFurniHealth(); const healthAfter = await getFurniHealth();
return { healthBefore, sprite, offers, reconcile, dedup, align, healthAfter }; return { healthBefore, sprite, offers, reconcile, dedup, align, healthAfter };