feat(recipes): report rematch results with aggregate logs and toast feedback

This commit is contained in:
Will Miao
2026-08-09 14:37:23 +08:00
parent d9d362c9c9
commit 68fa0f29c7
18 changed files with 828 additions and 141 deletions

View File

@@ -489,12 +489,56 @@ export class GlobalContextMenu extends BaseContextMenu {
progressUI?.updateProgress(percent, p.recipe_name, `${loadingMessage} (${p.current}/${p.total})`);
} else if (p.status === 'completed') {
isComplete = true;
progressUI?.complete(translate(
'globalContextMenu.rematchRecipes.success',
{ count: p.rematched },
`Rematched ${p.rematched} recipes.`
));
showToast('globalContextMenu.rematchRecipes.success', { count: p.rematched }, 'success');
// Newer backends report unified matched_entries /
// matched_recipes; fall back to legacy `rematched`
// (recipe count) for older ones.
const entries = p.matched_entries ?? p.rematched ?? 0;
const recipes = p.matched_recipes ?? p.rematched ?? 0;
const failures = p.errors || 0;
const unresolved = p.unresolved_entries ?? 0;
if (entries > 0) {
const successKey = failures > 0
? 'globalContextMenu.rematchRecipes.successErrors'
: 'globalContextMenu.rematchRecipes.success';
const successText = failures > 0
? `Matched ${entries} entries across ${recipes} recipes, ${failures} failed.`
: `Matched ${entries} entries across ${recipes} recipes.`;
progressUI?.complete(translate(
successKey,
{ count: recipes, recipes, entries, failures },
successText
));
showToast(successKey, { count: recipes, recipes, entries, failures }, failures > 0 ? 'warning' : 'success');
} else if (failures > 0) {
// Nothing matched and at least one recipe
// errored — "no rematch needed" would be
// actively misleading here.
progressUI?.complete(translate(
'globalContextMenu.rematchRecipes.allFailed',
{ total: p.total, recipes, entries, failures },
`Rematch failed for ${failures} of ${p.total} recipes.`
));
showToast('globalContextMenu.rematchRecipes.allFailed', { total: p.total, recipes, entries, failures }, 'error');
} else if (unresolved > 0) {
// Entries existed but have no local model —
// expected for models deleted from Civitai;
// informational, not an error.
const unresolvedRecipes = p.unresolved_recipes ?? 0;
progressUI?.complete(translate(
'globalContextMenu.rematchRecipes.noMatch',
{ entries: unresolved, recipes: unresolvedRecipes, total: p.total, failures },
`No local match found for ${unresolved} entries in ${unresolvedRecipes} recipes.`
));
showToast('globalContextMenu.rematchRecipes.noMatch', { entries: unresolved, recipes: unresolvedRecipes, total: p.total, failures }, 'info');
} else {
// Everything was skipped (nothing to do).
progressUI?.complete(translate(
'globalContextMenu.rematchRecipes.success',
{ count: recipes, recipes, entries, failures },
`Matched ${entries} entries across ${recipes} recipes.`
));
showToast('globalContextMenu.rematchRecipes.success', { count: recipes, recipes, entries, failures }, 'success');
}
// Refresh recipes page if active
if (window.recipesPage) {
window.recipesPage.refresh();
@@ -503,12 +547,14 @@ export class GlobalContextMenu extends BaseContextMenu {
throw new Error(p.error || 'Rematch failed');
} else if (p.status === 'cancelled') {
isComplete = true;
const cancelledEntries = p.matched_entries ?? p.rematched ?? 0;
const cancelledRecipes = p.matched_recipes ?? p.rematched ?? 0;
progressUI?.complete(translate(
'globalContextMenu.rematchRecipes.cancelled',
{ count: p.rematched },
`Rematch cancelled. ${p.rematched} recipes were rematched.`
{ count: cancelledRecipes, recipes: cancelledRecipes, entries: cancelledEntries },
`Rematch cancelled. ${cancelledRecipes} recipes updated (${cancelledEntries} entries).`
));
showToast('globalContextMenu.rematchRecipes.cancelled', { count: p.rematched }, 'info');
showToast('globalContextMenu.rematchRecipes.cancelled', { count: cancelledRecipes, recipes: cancelledRecipes, entries: cancelledEntries }, 'info');
}
} else if (progressResponse.status === 404) {
// Progress might have finished quickly and been cleaned up

View File

@@ -352,12 +352,16 @@ export class RecipeContextMenu extends BaseContextMenu {
const result = await response.json();
if (result.success) {
// The rematch backend reports `rematched` (not `repaired`)
if (result.rematched > 0) {
const matchedEntries = result.matched_entries || result.rematched || 0;
const failures = result.errors || 0;
if (matchedEntries > 0) {
const toastKey = failures > 0
? 'toast.recipes.rematchCompleteErrors'
: 'toast.recipes.rematchComplete';
showToast(
'toast.recipes.rematchComplete',
{ rematched: result.rematched, skipped: result.skipped || 0, total: 1 },
'success'
toastKey,
{ rematched: matchedEntries, skipped: result.skipped || 0, total: 1, entries: matchedEntries, recipes: 1, failures },
failures > 0 ? 'warning' : 'success'
);
const detailResponse = await fetch(`/api/lm/recipe/${recipeId}`);
if (detailResponse.ok) {
@@ -366,6 +370,14 @@ export class RecipeContextMenu extends BaseContextMenu {
state.virtualScroller.updateSingleItem(filePath, updatedRecipe);
}
}
} else if (result.unresolved_entries > 0) {
// Entries existed but have no local model — expected for
// models deleted from Civitai; informational, not an error.
showToast(
'toast.recipes.rematchUnmatched',
{ entries: result.unresolved_entries, recipes: 1, total: 1 },
'info'
);
} else {
showToast('toast.recipes.rematchSkipped', { total: 1 }, 'info');
}

View File

@@ -898,9 +898,17 @@ export class BulkManager {
if (result.success) {
const total = result.total || filePaths.length;
// The rematch backend reports `rematched` (not `repaired`)
// Unified counters from the backend; legacy fields fall back
// for older backends: `rematched` (entry count) for
// matched_entries, `total` (selection size) for
// matched_recipes.
const rematched = result.rematched || 0;
const skipped = result.skipped || 0;
const matchedRecipes = result.matched_recipes || result.total || 0;
const matchedEntries = result.matched_entries || rematched;
const failures = result.errors || 0;
const unresolvedEntries = result.unresolved_entries || 0;
const unresolvedRecipes = result.unresolved_recipes || 0;
const recipes = result.recipes || [];
for (const recipe of recipes) {
@@ -912,11 +920,31 @@ export class BulkManager {
}
}
if (rematched > 0) {
if (matchedEntries > 0) {
const hasFailures = failures > 0;
const toastKey = hasFailures
? 'toast.recipes.rematchCompleteErrors'
: 'toast.recipes.rematchComplete';
showToast(
'toast.recipes.rematchComplete',
{ rematched, skipped, total },
'success'
toastKey,
{ rematched, skipped, total, entries: matchedEntries, recipes: matchedRecipes, failures },
hasFailures ? 'warning' : 'success'
);
} else if (failures > 0) {
// Nothing matched and at least one recipe errored —
// "no rematch needed" would be actively misleading here.
showToast(
'toast.recipes.rematchAllFailed',
{ total, failures },
'error'
);
} else if (unresolvedEntries > 0) {
// Entries existed but have no local model — expected for
// models deleted from Civitai; informational, not an error.
showToast(
'toast.recipes.rematchUnmatched',
{ entries: unresolvedEntries, recipes: unresolvedRecipes, total },
'info'
);
} else {
showToast(