diff --git a/locales/de.json b/locales/de.json index d60d7252..d4e5c30e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -216,9 +216,6 @@ "label": "Rezepte lokalen Modellen neu zuordnen", "loading": "Rezepte werden lokalen Modellen neu zugeordnet...", "success": "{entries} Einträge in {recipes} Rezepten zugeordnet", - "successErrors": "{entries} Einträge in {recipes} Rezepten zugeordnet, {failures} fehlgeschlagen", - "allFailed": "Zuordnung fehlgeschlagen für {failures} von {total} Rezepten", - "noMatch": "Keine lokale Übereinstimmung für {entries} Einträge in {recipes} Rezepten gefunden", "cancelled": "Zuordnung abgebrochen. {recipes} Rezepte aktualisiert ({entries} Einträge)", "error": "Zuordnung der Rezepte fehlgeschlagen: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "Neu zuordnen" }, "rematchResults": { - "title": "Ergebnisse der neuen Zuordnung — Übereinstimmungen per Dateiname", - "message": "Diese Einträge wurden per Dateiname neu verbunden und verknüpfen möglicherweise eine andere Version eines Modells. Überprüfen Sie sie und machen Sie falsche Zuordnungen rückgängig.", "undo": "Rückgängig", "undone": "Rückgängig gemacht", "undoFailed": "Rückgängigmachen der Neuordnung fehlgeschlagen: {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "Lokale Beispielbilder", "message": "Keine lokalen Beispielbilder für dieses Modell gefunden. Ansichtsoptionen:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "Ordner konnte nicht durchsucht werden: {message}", "batchImportDirectorySelected": "Verzeichnis ausgewählt: {path}", "noRecipesSelected": "Keine Rezepte ausgewählt", - "rematchComplete": "{entries} Einträge in {recipes} Rezepten zugeordnet", - "rematchCompleteErrors": "{entries} Einträge in {recipes} Rezepten zugeordnet, {failures} fehlgeschlagen", - "rematchAllFailed": "Zuordnung fehlgeschlagen für {failures} von {total} ausgewählten Rezepten", - "rematchUnmatched": "Keine lokale Übereinstimmung für {entries} Einträge in {recipes} Rezepten gefunden", "rematchSkipped": "Keine Zuordnung für die {total} ausgewählten Rezepte erforderlich", "rematchFailed": "Zuordnung der ausgewählten Rezepte fehlgeschlagen: {message}", "reimporting": "Rezept wird aus Quelle neu importiert...", diff --git a/locales/en.json b/locales/en.json index a11412e2..b6c9e93b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -216,9 +216,6 @@ "label": "Rematch recipes to local models", "loading": "Rematching recipes to local models...", "success": "Matched {entries} entries across {recipes} recipes", - "successErrors": "Matched {entries} entries across {recipes} recipes, {failures} failed", - "allFailed": "Rematch failed for {failures} of {total} recipes", - "noMatch": "No local match found for {entries} entries in {recipes} recipes", "cancelled": "Rematch cancelled. {recipes} recipes updated ({entries} entries).", "error": "Recipe rematch failed: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "Rematch" }, "rematchResults": { - "title": "Rematch Results — Filename Matches", - "message": "These entries were reconnected by file name and may link a different version of a model. Review them and undo any that are wrong.", "undo": "Undo", "undone": "Undone", "undoFailed": "Failed to undo rematch: {message}" }, + "rematchSummary": { + "title": "Rematch Summary", + "successMessage": "Matched {entries} entries", + "failed": "Rematch failed", + "completedWithWarnings": "Rematch completed — review recommended", + "cancelledNote": "Run cancelled before completion — counts are partial.", + "statMatched": "Matched entries", + "statReview": "Needs review", + "statUnresolved": "Unresolved", + "statErrors": "Errors", + "reviewSection": "Filename matches to review ({count})", + "columnRecipe": "Recipe", + "columnEntry": "Entry", + "columnFile": "Matched file", + "columnUndo": "Undo", + "copyReport": "Copy Report", + "close": "Close", + "scope_global": "All recipes", + "scope_bulk": "Selected recipes", + "scope_single": "Single recipe" + }, "exampleAccess": { "title": "Local Example Images", "message": "No local example images found for this model. View options:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportDirectorySelected": "Directory selected: {path}", "noRecipesSelected": "No recipes selected", - "rematchComplete": "Matched {entries} entries across {recipes} recipes", - "rematchCompleteErrors": "Matched {entries} entries across {recipes} recipes, {failures} failed", - "rematchAllFailed": "Rematch failed for {failures} of {total} selected recipes", - "rematchUnmatched": "No local match found for {entries} entries in {recipes} recipes", "rematchSkipped": "No rematch needed for any of the {total} selected recipes", "rematchFailed": "Failed to rematch selected recipes: {message}", "reimporting": "Re-importing recipe from source...", diff --git a/locales/es.json b/locales/es.json index 2cf32a98..003f83d9 100644 --- a/locales/es.json +++ b/locales/es.json @@ -216,9 +216,6 @@ "label": "Reasociar recetas con modelos locales", "loading": "Reasociando recetas con modelos locales...", "success": "{entries} entradas asociadas en {recipes} recetas", - "successErrors": "{entries} entradas asociadas en {recipes} recetas, {failures} fallidas", - "allFailed": "Falló la reasociación de {failures} de {total} recetas", - "noMatch": "No se encontró coincidencia local para {entries} entradas en {recipes} recetas", "cancelled": "Reasociación cancelada. {recipes} recetas actualizadas ({entries} entradas)", "error": "Falló la reasociación de recetas: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "Reasociar" }, "rematchResults": { - "title": "Resultados de la reasociación — Coincidencias por nombre de archivo", - "message": "Estas entradas se reconectaron por nombre de archivo y pueden enlazar una versión diferente de un modelo. Revísalas y deshaz las que sean incorrectas.", "undo": "Deshacer", "undone": "Deshecho", "undoFailed": "No se pudo deshacer la reasociación: {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "Imágenes de ejemplo locales", "message": "No se encontraron imágenes de ejemplo locales para este modelo. Opciones de visualización:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "No se pudo examinar el directorio: {message}", "batchImportDirectorySelected": "Directorio seleccionado: {path}", "noRecipesSelected": "No se han seleccionado recetas", - "rematchComplete": "{entries} entradas asociadas en {recipes} recetas", - "rematchCompleteErrors": "{entries} entradas asociadas en {recipes} recetas, {failures} fallidas", - "rematchAllFailed": "Falló la reasociación de {failures} de {total} recetas seleccionadas", - "rematchUnmatched": "No se encontró coincidencia local para {entries} entradas en {recipes} recetas", "rematchSkipped": "Ninguna de las {total} recetas seleccionadas necesita reasociación", "rematchFailed": "Falló la reasociación de las recetas seleccionadas: {message}", "reimporting": "Reimportando receta desde origen...", diff --git a/locales/fr.json b/locales/fr.json index 9d1fd5dd..7aceeb6d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -216,9 +216,6 @@ "label": "Réassocier les Recipes aux modèles locaux", "loading": "Réassociation des Recipes aux modèles locaux...", "success": "{entries} entrées associées dans {recipes} Recipes", - "successErrors": "{entries} entrées associées dans {recipes} Recipes, {failures} échecs", - "allFailed": "Échec de la réassociation de {failures} Recipes sur {total}", - "noMatch": "Aucune correspondance locale trouvée pour {entries} entrées dans {recipes} Recipes", "cancelled": "Réassociation annulée. {recipes} Recipes mises à jour ({entries} entrées)", "error": "Échec de la réassociation des Recipes : {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "Réassocier" }, "rematchResults": { - "title": "Résultats de la réassociation — Correspondances par nom de fichier", - "message": "Ces entrées ont été reconnectées par nom de fichier et peuvent associer une version différente d'un modèle. Vérifiez-les et annulez celles qui sont incorrectes.", "undo": "Annuler", "undone": "Annulé", "undoFailed": "Échec de l'annulation de la réassociation : {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "Images d'exemple locales", "message": "Aucune image d'exemple locale trouvée pour ce modèle. Options d'affichage :", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "Échec de la navigation dans le dossier : {message}", "batchImportDirectorySelected": "Dossier sélectionné : {path}", "noRecipesSelected": "Aucune Recipe sélectionnée", - "rematchComplete": "{entries} entrées associées dans {recipes} Recipes", - "rematchCompleteErrors": "{entries} entrées associées dans {recipes} Recipes, {failures} échecs", - "rematchAllFailed": "Échec de la réassociation de {failures} Recipes sélectionnées sur {total}", - "rematchUnmatched": "Aucune correspondance locale trouvée pour {entries} entrées dans {recipes} Recipes", "rematchSkipped": "Aucune des {total} Recipes sélectionnées ne nécessite de réassociation", "rematchFailed": "Échec de la réassociation des Recipes sélectionnées : {message}", "reimporting": "Ré-import de la Recipe depuis la source...", diff --git a/locales/he.json b/locales/he.json index f990f61e..b33d12e4 100644 --- a/locales/he.json +++ b/locales/he.json @@ -216,9 +216,6 @@ "label": "התאמה מחדש של מתכונים למודלים מקומיים", "loading": "מתבצעת התאמה מחדש של מתכונים למודלים מקומיים...", "success": "הותאמו {entries} פריטים ב־{recipes} מתכונים", - "successErrors": "הותאמו {entries} פריטים ב־{recipes} מתכונים, {failures} נכשלו", - "allFailed": "ההתאמה נכשלה עבור {failures} מתוך {total} מתכונים", - "noMatch": "לא נמצאה התאמה מקומית עבור {entries} פריטים ב־{recipes} מתכונים", "cancelled": "ההתאמה בוטלה. עודכנו {recipes} מתכונים ({entries} פריטים)", "error": "ההתאמה מחדש של המתכונים נכשלה: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "התאם מחדש" }, "rematchResults": { - "title": "תוצאות ההתאמה מחדש — התאמות לפי שם קובץ", - "message": "הפריטים האלה חוברו מחדש לפי שם קובץ וייתכן שהם מקושרים לגרסה אחרת של מודל. סקור אותם ובטל כל אחד שאינו נכון.", "undo": "בטל", "undone": "בוטל", "undoFailed": "ביטול ההתאמה מחדש נכשל: {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "תמונות דוגמה מקומיות", "message": "לא נמצאו תמונות דוגמה מקומיות למודל זה. אפשרויות צפייה:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "לא ניתן היה לעיין בתיקייה: {message}", "batchImportDirectorySelected": "נבחרה תיקייה: {path}", "noRecipesSelected": "לא נבחרו מתכונים", - "rematchComplete": "הותאמו {entries} פריטים ב־{recipes} מתכונים", - "rematchCompleteErrors": "הותאמו {entries} פריטים ב־{recipes} מתכונים, {failures} נכשלו", - "rematchAllFailed": "ההתאמה נכשלה עבור {failures} מתוך {total} מתכונים שנבחרו", - "rematchUnmatched": "לא נמצאה התאמה מקומית עבור {entries} פריטים ב־{recipes} מתכונים", "rematchSkipped": "אין צורך בהתאמה עבור {total} המתכונים שנבחרו", "rematchFailed": "ההתאמה מחדש של המתכונים שנבחרו נכשלה: {message}", "reimporting": "מייבא מתכון מחדש מהמקור...", diff --git a/locales/ja.json b/locales/ja.json index 75bd6e66..8b47cbf3 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -216,9 +216,6 @@ "label": "レシピをローカルモデルに再マッチング", "loading": "レシピをローカルモデルに再マッチングしています...", "success": "{recipes} 件のレシピで {entries} エントリをマッチングしました", - "successErrors": "{recipes} 件のレシピで {entries} エントリをマッチングしました({failures} 件失敗)", - "allFailed": "{total} 件中 {failures} 件のレシピの再マッチングに失敗しました", - "noMatch": "{recipes} 件のレシピで {entries} エントリのローカルマッチが見つかりませんでした", "cancelled": "再マッチングをキャンセルしました。{recipes} 件のレシピを更新({entries} エントリ)", "error": "レシピの再マッチングに失敗しました:{message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "再マッチング" }, "rematchResults": { - "title": "再マッチング結果 — ファイル名によるマッチ", - "message": "これらのエントリはファイル名で再接続されたため、モデルの別バージョンに関連付けられている可能性があります。内容を確認し、誤っているものは元に戻してください。", "undo": "元に戻す", "undone": "元に戻しました", "undoFailed": "再マッチングを元に戻せませんでした:{message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "ローカル例画像", "message": "このモデルのローカル例画像が見つかりませんでした。表示オプション:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "フォルダを参照できませんでした: {message}", "batchImportDirectorySelected": "選択されたフォルダ: {path}", "noRecipesSelected": "レシピが選択されていません", - "rematchComplete": "{recipes} 件のレシピで {entries} エントリをマッチングしました", - "rematchCompleteErrors": "{recipes} 件のレシピで {entries} エントリをマッチングしました({failures} 件失敗)", - "rematchAllFailed": "選択した {total} 件中 {failures} 件のレシピの再マッチングに失敗しました", - "rematchUnmatched": "{recipes} 件のレシピで {entries} エントリのローカルマッチが見つかりませんでした", "rematchSkipped": "選択した {total} 件のレシピは再マッチングの必要がありませんでした", "rematchFailed": "選択したレシピの再マッチングに失敗しました:{message}", "reimporting": "ソースからレシピを再インポート中...", diff --git a/locales/ko.json b/locales/ko.json index 8d5fa2ed..584ec950 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -216,9 +216,6 @@ "label": "레시피를 로컬 모델에 다시 매칭", "loading": "레시피를 로컬 모델에 다시 매칭하는 중...", "success": "{recipes}개 레시피에서 {entries}개 항목이 매칭되었습니다", - "successErrors": "{recipes}개 레시피에서 {entries}개 항목이 매칭되었습니다. {failures}개 실패", - "allFailed": "{total}개 레시피 중 {failures}개 재매칭 실패", - "noMatch": "{recipes}개 레시피에서 {entries}개 항목의 로컬 매칭을 찾지 못했습니다", "cancelled": "재매칭이 취소되었습니다. {recipes}개 레시피 업데이트됨({entries}개 항목)", "error": "레시피 재매칭 실패: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "재매칭" }, "rematchResults": { - "title": "재매칭 결과 — 파일 이름 매칭", - "message": "이 항목들은 파일 이름으로 다시 연결되었으며 모델의 다른 버전에 연결되었을 수 있습니다. 검토 후 잘못된 항목은 실행 취소하세요.", "undo": "실행 취소", "undone": "실행 취소됨", "undoFailed": "재매칭 실행 취소 실패: {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "로컬 예시 이미지", "message": "이 모델의 로컬 예시 이미지를 찾을 수 없습니다. 보기 옵션:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "폴더를 찾아보지 못했습니다: {message}", "batchImportDirectorySelected": "선택한 폴더: {path}", "noRecipesSelected": "선택한 레시피가 없습니다", - "rematchComplete": "{recipes}개 레시피에서 {entries}개 항목이 매칭되었습니다", - "rematchCompleteErrors": "{recipes}개 레시피에서 {entries}개 항목이 매칭되었습니다. {failures}개 실패", - "rematchAllFailed": "선택한 {total}개 레시피 중 {failures}개 재매칭 실패", - "rematchUnmatched": "{recipes}개 레시피에서 {entries}개 항목의 로컬 매칭을 찾지 못했습니다", "rematchSkipped": "선택한 {total}개 레시피는 재매칭이 필요하지 않습니다", "rematchFailed": "선택한 레시피 재매칭 실패: {message}", "reimporting": "소스에서 레시피를 다시 가져오는 중...", diff --git a/locales/ru.json b/locales/ru.json index 99bcd654..dc48da9d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -216,9 +216,6 @@ "label": "Повторное сопоставление рецептов с локальными моделями", "loading": "Повторное сопоставление рецептов с локальными моделями...", "success": "Сопоставлено записей: {entries} в рецептах: {recipes}", - "successErrors": "Сопоставлено записей: {entries} в рецептах: {recipes}, ошибок: {failures}", - "allFailed": "Не удалось сопоставить: {failures} из {total} рецептов", - "noMatch": "Не найдено локального сопоставления для {entries} записей в {recipes} рецептах", "cancelled": "Сопоставление отменено. Обновлено рецептов: {recipes} (записей: {entries})", "error": "Не удалось выполнить сопоставление рецептов: {message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "Сопоставить" }, "rematchResults": { - "title": "Результаты сопоставления — совпадения по имени файла", - "message": "Эти записи были переподключены по имени файла и могут ссылаться на другую версию модели. Проверьте их и отмените неверные.", "undo": "Отменить", "undone": "Отменено", "undoFailed": "Не удалось отменить сопоставление: {message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "Локальные примеры изображений", "message": "Локальные примеры изображений для этой модели не найдены. Варианты просмотра:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "Не удалось открыть папку: {message}", "batchImportDirectorySelected": "Выбрана папка: {path}", "noRecipesSelected": "Рецепты не выбраны", - "rematchComplete": "Сопоставлено записей: {entries} в рецептах: {recipes}", - "rematchCompleteErrors": "Сопоставлено записей: {entries} в рецептах: {recipes}, ошибок: {failures}", - "rematchAllFailed": "Не удалось сопоставить: {failures} из {total} выбранных рецептов", - "rematchUnmatched": "Не найдено локального сопоставления для {entries} записей в {recipes} рецептах", "rematchSkipped": "Ни один из {total} выбранных рецептов не требует сопоставления", "rematchFailed": "Не удалось сопоставить выбранные рецепты: {message}", "reimporting": "Переимпорт рецепта из источника...", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9939be78..a3926e2b 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -216,9 +216,6 @@ "label": "将配方重新匹配到本地模型", "loading": "正在将配方重新匹配到本地模型...", "success": "已匹配 {entries} 个条目,涉及 {recipes} 个配方", - "successErrors": "已匹配 {entries} 个条目,涉及 {recipes} 个配方,{failures} 个失败", - "allFailed": "{failures}/{total} 个配方重新匹配失败", - "noMatch": "在 {recipes} 个配方中未找到 {entries} 个条目的本地匹配", "cancelled": "已取消重新匹配。{recipes} 个配方已更新({entries} 个条目)。", "error": "配方重新匹配失败:{message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "重新匹配" }, "rematchResults": { - "title": "重新匹配结果——文件名匹配", - "message": "这些条目已按文件名重新关联,可能链接到了模型的其他版本。请检查它们,并撤销任何错误的关联。", "undo": "撤销", "undone": "已撤销", "undoFailed": "撤销重新匹配失败:{message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "本地示例图片", "message": "未找到此模型的本地示例图片。可选操作:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "浏览目录失败:{message}", "batchImportDirectorySelected": "已选择目录:{path}", "noRecipesSelected": "未选择任何配方", - "rematchComplete": "已匹配 {entries} 个条目,涉及 {recipes} 个配方", - "rematchCompleteErrors": "已匹配 {entries} 个条目,涉及 {recipes} 个配方,{failures} 个失败", - "rematchAllFailed": "{failures}/{total} 个所选配方重新匹配失败", - "rematchUnmatched": "在 {recipes} 个配方中未找到 {entries} 个条目的本地匹配", "rematchSkipped": "{total} 个所选配方均无需重新匹配", "rematchFailed": "重新匹配所选配方失败:{message}", "reimporting": "正在从源重新导入配方...", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index fc4ca9d5..2a31a0a2 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -216,9 +216,6 @@ "label": "將配方重新匹配到本地模型", "loading": "正在將配方重新匹配到本地模型...", "success": "已匹配 {entries} 個條目,涉及 {recipes} 個配方", - "successErrors": "已匹配 {entries} 個條目,涉及 {recipes} 個配方,{failures} 個失敗", - "allFailed": "{failures}/{total} 個配方重新匹配失敗", - "noMatch": "在 {recipes} 個配方中找不到 {entries} 個條目的本地匹配", "cancelled": "已取消重新匹配。{recipes} 個配方已更新({entries} 個條目)。", "error": "配方重新匹配失敗:{message}" }, @@ -1511,12 +1508,31 @@ "confirmButton": "重新匹配" }, "rematchResults": { - "title": "重新匹配結果——檔案名稱匹配", - "message": "這些條目已依檔案名稱重新關聯,可能連結到模型的不同版本。請檢閱這些條目,並撤銷任何錯誤的項目。", "undo": "撤銷", "undone": "已撤銷", "undoFailed": "撤銷重新匹配失敗:{message}" }, + "rematchSummary": { + "title": "[TODO: Translate] Rematch Summary", + "successMessage": "[TODO: Translate] Matched {entries} entries", + "failed": "[TODO: Translate] Rematch failed", + "completedWithWarnings": "[TODO: Translate] Rematch completed — review recommended", + "cancelledNote": "[TODO: Translate] Run cancelled before completion — counts are partial.", + "statMatched": "[TODO: Translate] Matched entries", + "statReview": "[TODO: Translate] Needs review", + "statUnresolved": "[TODO: Translate] Unresolved", + "statErrors": "[TODO: Translate] Errors", + "reviewSection": "[TODO: Translate] Filename matches to review ({count})", + "columnRecipe": "[TODO: Translate] Recipe", + "columnEntry": "[TODO: Translate] Entry", + "columnFile": "[TODO: Translate] Matched file", + "columnUndo": "[TODO: Translate] Undo", + "copyReport": "[TODO: Translate] Copy Report", + "close": "[TODO: Translate] Close", + "scope_global": "[TODO: Translate] All recipes", + "scope_bulk": "[TODO: Translate] Selected recipes", + "scope_single": "[TODO: Translate] Single recipe" + }, "exampleAccess": { "title": "本機範例圖片", "message": "此模型未找到本機範例圖片。可選擇:", @@ -2239,10 +2255,6 @@ "batchImportBrowseFailed": "瀏覽目錄失敗:{message}", "batchImportDirectorySelected": "已選擇目錄:{path}", "noRecipesSelected": "未選取任何配方", - "rematchComplete": "已匹配 {entries} 個條目,涉及 {recipes} 個配方", - "rematchCompleteErrors": "已匹配 {entries} 個條目,涉及 {recipes} 個配方,{failures} 個失敗", - "rematchAllFailed": "{failures}/{total} 個所選配方重新匹配失敗", - "rematchUnmatched": "在 {recipes} 個配方中找不到 {entries} 個條目的本地匹配", "rematchSkipped": "{total} 個所選配方均無需重新匹配", "rematchFailed": "重新匹配所選配方失敗:{message}", "reimporting": "正在從來源重新匯入配方...", diff --git a/static/css/components/modal/_base.css b/static/css/components/modal/_base.css index 941ab4b1..bd37b52b 100644 --- a/static/css/components/modal/_base.css +++ b/static/css/components/modal/_base.css @@ -697,78 +697,40 @@ button:disabled, flex-shrink: 0; } -/* Recipe Rematch L4 Results Modal */ -#rematchResultsModal .modal-body { - padding: var(--space-3); +/* Recipe Rematch Summary Modal (dynamically built by RematchSummaryModal.js; + stat cards / failure table / summary header come from + metadata-refresh-result.css and download-batch-summary.css). */ +.rematch-summary-modal { + max-width: 700px; } -#rematchResultsModal .confirmation-message { - color: var(--text-color); - margin-bottom: var(--space-3); - font-size: 1em; - line-height: 1.5; -} - -#rematchResultsModal .rematch-results-preview { - background: var(--surface-subtle); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); - padding: var(--space-2) var(--space-3); -} - -#rematchResultsModal .rematch-results-list { - list-style: none; - padding: 0; - margin: 0; - max-height: 320px; - overflow-y: auto; -} - -#rematchResultsModal .rematch-results-row { +.rematch-cancelled-note { display: flex; - align-items: center; - justify-content: space-between; + align-items: flex-start; gap: var(--space-2); - padding: var(--space-2) 0; - border-bottom: 1px solid var(--border-color); - font-size: 0.9em; + margin: 0 0 var(--space-2) 0; + font-size: var(--text-sm); + color: var(--color-warning); } -#rematchResultsModal .rematch-results-row:last-child { - border-bottom: none; +.rematch-cancelled-note i { + margin-top: 2px; + flex-shrink: 0; } -#rematchResultsModal .rematch-results-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - flex: 1; +/* Review section heading uses the accent (review, not failure) instead of + the failure-section error color. */ +.rematch-review-section h4 { + color: var(--lora-accent); } -#rematchResultsModal .rematch-results-entry { - font-weight: 500; - color: var(--text-color); - overflow-wrap: anywhere; +#rematchSummaryModal .rematch-undo-btn { + padding: var(--space-1) var(--space-2); + font-size: var(--text-xs); + white-space: nowrap; } -#rematchResultsModal .rematch-results-file { - color: var(--text-muted); - overflow-wrap: anywhere; -} - -#rematchResultsModal .rematch-results-recipe { - font-size: 0.85em; - opacity: 0.7; - color: var(--text-muted); - overflow-wrap: anywhere; -} - -#rematchResultsModal .rematch-results-row.undone .rematch-results-info { +#rematchSummaryModal tr.undone td:not(.rematch-undo-cell) { text-decoration: line-through; opacity: 0.6; } - -#rematchResultsModal .rematch-results-undo { - flex-shrink: 0; -} diff --git a/static/js/components/ContextMenu/GlobalContextMenu.js b/static/js/components/ContextMenu/GlobalContextMenu.js index 130f70d5..36e5fad4 100644 --- a/static/js/components/ContextMenu/GlobalContextMenu.js +++ b/static/js/components/ContextMenu/GlobalContextMenu.js @@ -5,6 +5,7 @@ import { state } from '../../state/index.js'; import { getCompleteApiConfig, getCurrentModelType } from '../../api/apiConfig.js'; import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; import { rematchModalManager } from '../../managers/RematchModalManager.js'; +import { showRematchSummary } from '../RematchSummaryModal.js'; export class GlobalContextMenu extends BaseContextMenu { constructor() { @@ -425,58 +426,37 @@ export class GlobalContextMenu extends BaseContextMenu { 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). + const l4Matches = Array.isArray(p.l4_matches) ? p.l4_matches : []; + // Complete no-op (nothing matched, nothing + // unresolved, no errors) keeps the lightweight + // toast; anything else opens the post-run summary + // modal. + const isNoop = entries === 0 && unresolved === 0 && failures === 0; + if (isNoop) { 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'); + } else { + progressUI?.complete(); + showRematchSummary({ + scope: 'global', + total: p.total || 0, + matchedRecipes: recipes, + matchedEntries: entries, + unresolvedRecipes: p.unresolved_recipes ?? 0, + unresolvedEntries: unresolved, + skipped: p.skipped || 0, + errors: failures, + l4Matches, + }); } // Refresh recipes page if active if (window.recipesPage) { window.recipesPage.refresh(); } - // Filename-level (L4) matches are imprecise — - // always surface them for review/undo. - if (Array.isArray(p.l4_matches) && p.l4_matches.length > 0) { - rematchModalManager.showResultsModal(p.l4_matches); - } } else if (p.status === 'error') { throw new Error(p.error || 'Rematch failed'); } else if (p.status === 'cancelled') { @@ -488,7 +468,23 @@ export class GlobalContextMenu extends BaseContextMenu { { count: cancelledRecipes, recipes: cancelledRecipes, entries: cancelledEntries }, `Rematch cancelled. ${cancelledRecipes} recipes updated (${cancelledEntries} entries).` )); - showToast('globalContextMenu.rematchRecipes.cancelled', { count: cancelledRecipes, recipes: cancelledRecipes, entries: cancelledEntries }, 'info'); + // A cancelled run still reports partial results + // via the summary modal (marked as cancelled). + showRematchSummary({ + scope: 'global', + cancelled: true, + total: p.total || 0, + matchedRecipes: cancelledRecipes, + matchedEntries: cancelledEntries, + unresolvedRecipes: p.unresolved_recipes ?? 0, + unresolvedEntries: p.unresolved_entries ?? 0, + skipped: p.skipped || 0, + errors: p.errors || 0, + l4Matches: Array.isArray(p.l4_matches) ? p.l4_matches : [], + }); + if (window.recipesPage) { + window.recipesPage.refresh(); + } } } else if (progressResponse.status === 404) { // Progress might have finished quickly and been cleaned up diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index 87353652..c11e5ef5 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -7,6 +7,7 @@ import { updateRecipeMetadata } from '../../api/recipeApi.js'; import { state } from '../../state/index.js'; import { moveManager } from '../../managers/MoveManager.js'; import { rematchModalManager } from '../../managers/RematchModalManager.js'; +import { showRematchSummary } from '../RematchSummaryModal.js'; import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js'; export class RecipeContextMenu extends BaseContextMenu { @@ -326,15 +327,14 @@ export class RecipeContextMenu extends BaseContextMenu { if (result.success) { const matchedEntries = result.matched_entries || result.rematched || 0; const failures = result.errors || 0; + const unresolvedEntries = result.unresolved_entries || 0; + const l4Matches = Array.isArray(result.l4_matches) ? result.l4_matches : []; + // Complete no-op (nothing matched, nothing unresolved, no + // errors) keeps the lightweight toast; anything else opens + // the post-run summary modal. + const isNoop = matchedEntries === 0 && unresolvedEntries === 0 && failures === 0; + if (matchedEntries > 0) { - const toastKey = failures > 0 - ? 'toast.recipes.rematchCompleteErrors' - : 'toast.recipes.rematchComplete'; - showToast( - 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) { const updatedRecipe = await detailResponse.json(); @@ -342,21 +342,22 @@ export class RecipeContextMenu extends BaseContextMenu { state.virtualScroller.updateSingleItem(filePath, updatedRecipe); } } - // Filename-level (L4) matches are imprecise — always - // surface them for review/undo. - if (Array.isArray(result.l4_matches) && result.l4_matches.length > 0) { - rematchModalManager.showResultsModal(result.l4_matches); - } - } 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 { + } + + if (isNoop) { showToast('toast.recipes.rematchSkipped', { total: 1 }, 'info'); + } else { + showRematchSummary({ + scope: 'single', + total: 1, + matchedRecipes: result.matched_recipes || (matchedEntries > 0 ? 1 : 0), + matchedEntries, + unresolvedRecipes: result.unresolved_recipes || 0, + unresolvedEntries, + skipped: result.skipped || 0, + errors: failures, + l4Matches, + }); } } else { throw new Error(result.error || 'Rematch failed'); diff --git a/static/js/components/RematchSummaryModal.js b/static/js/components/RematchSummaryModal.js new file mode 100644 index 00000000..16752586 --- /dev/null +++ b/static/js/components/RematchSummaryModal.js @@ -0,0 +1,338 @@ +import { translate } from '../utils/i18nHelpers.js'; +import { showToast } from '../utils/uiHelpers.js'; + +/** + * Escape HTML entities in a string to prevent injection when interpolating + * into innerHTML (same approach as DownloadBatchSummaryModal). + * @param {string} str - The string to escape + * @returns {string} - The escaped string + */ +function _escapeHtml(str) { + if (str === null || str === undefined) return ''; + const div = document.createElement('div'); + div.textContent = String(str); + return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); +} + +/** + * Resolve the 3-state summary header (mirrors the batch download/import + * summary semantics). + * + * - error: nothing matched and at least one recipe errored + * - warning: errors, unresolved entries, filename-level (L4) matches to + * review, or a cancelled run + * - success: otherwise + */ +function _resolveHeader({ matchedEntries, errors, unresolvedEntries, l4Count, cancelled }) { + if (matchedEntries === 0 && errors > 0) { + return { + state: 'error', + icon: 'fa-times-circle', + text: translate('modals.rematchSummary.failed', {}, 'Rematch failed'), + }; + } + if (errors > 0 || unresolvedEntries > 0 || l4Count > 0 || cancelled) { + return { + state: 'warning', + icon: 'fa-exclamation-circle', + text: translate('modals.rematchSummary.completedWithWarnings', {}, 'Rematch completed — review recommended'), + }; + } + return { + state: 'success', + icon: 'fa-check-circle', + text: translate('modals.rematchSummary.successMessage', { entries: matchedEntries }, `Matched ${matchedEntries} entries`), + }; +} + +/** + * Build a plain-text report of the rematch run. `undoneIndexes` carries the + * L4 rows undone so far, so the report reflects the undo status at copy time. + */ +function _buildReportText({ scope, cancelled, total, matchedRecipes, matchedEntries, unresolvedRecipes, unresolvedEntries, skipped, errors, l4Matches, undoneIndexes }) { + const scopeFallbacks = { + global: 'All recipes', + bulk: 'Selected recipes', + single: 'Single recipe', + }; + const scopeLabel = translate( + `modals.rematchSummary.scope_${scope}`, + {}, + scopeFallbacks[scope] || scope + ); + const lines = [ + '=== Recipe Rematch Report ===', + `Date: ${new Date().toLocaleString()}`, + `Scope: ${scopeLabel}`, + `Cancelled: ${cancelled ? 'yes' : 'no'}`, + `Total recipes: ${total}`, + `Matched recipes: ${matchedRecipes}`, + `Matched entries: ${matchedEntries}`, + `Needs review (filename matches): ${l4Matches.length}`, + `Unresolved entries: ${unresolvedEntries} (in ${unresolvedRecipes} recipes)`, + `Skipped: ${skipped}`, + `Errors: ${errors}`, + '', + ]; + if (l4Matches.length > 0) { + lines.push('--- Filename matches (L4) ---'); + l4Matches.forEach((match, i) => { + const undone = undoneIndexes.has(i) ? ' [undone]' : ''; + lines.push(`${i + 1}. [${match.recipe_id}] ${match.entry} -> ${match.file_name}${undone}`); + }); + lines.push(''); + } + lines.push('===================='); + return lines.join('\n'); +} + +/** + * Handle a successful clipboard write: confirm via toast and briefly swap the + * trigger button to a "Copied!" state (mirrors the batch summary modal). + */ +function _onCopyReportSuccess(btn) { + showToast('toast.api.copiedToClipboard', {}, 'success'); + if (btn) { + const origHTML = btn.innerHTML; + btn.innerHTML = ' Copied!'; + setTimeout(() => { btn.innerHTML = origHTML; }, 2000); + } +} + +/** + * Fallback for environments without the async Clipboard API (e.g. insecure + * contexts over LAN http): copy via a hidden textarea and execCommand. + */ +function _copyReportWithExecCommand(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + showToast('toast.api.copiedToClipboard', {}, 'success'); +} + +function _copyReport(btn, reportArgs) { + const text = _buildReportText(reportArgs); + if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { + navigator.clipboard.writeText(text) + .then(() => _onCopyReportSuccess(btn)) + .catch(() => _copyReportWithExecCommand(text)); + } else { + _copyReportWithExecCommand(text); + } +} + +/** + * Undo a single L4 match via the existing restore endpoints (moved from + * RematchModalManager). Checkpoint restore needs only recipe_id; lora + * restore additionally takes lora_index. + */ +async function _undoMatch(match) { + const isCheckpoint = match.type === 'checkpoint'; + const body = isCheckpoint + ? { recipe_id: match.recipe_id } + : { recipe_id: match.recipe_id, lora_index: match.lora_index }; + const response = await fetch( + isCheckpoint + ? '/api/lm/recipe/checkpoint/restore' + : '/api/lm/recipe/lora/restore', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + } + ); + const result = await response.json(); + if (!response.ok || !result.success) { + throw new Error(result.error || 'Restore failed'); + } +} + +/** + * Show the post-run rematch summary modal. Mirrors the batch download + * summary lifecycle: the modal element is appended directly to + * document.body and removed on close; it is not registered with + * ModalManager. + * + * @param {Object} options + * @param {'global'|'bulk'|'single'} options.scope - Which entry point ran + * @param {boolean} options.cancelled - Whether the run was cancelled + * @param {number} options.total - Recipes scanned + * @param {number} options.matchedRecipes - Recipes updated + * @param {number} options.matchedEntries - Entries reconnected + * @param {number} options.unresolvedRecipes - Recipes with unresolved entries + * @param {number} options.unresolvedEntries - Candidate entries with no local match + * @param {number} options.skipped - Recipes left untouched + * @param {number} options.errors - Per-recipe errors + * @param {Array} options.l4Matches - Filename-level matches for review/undo + * ({ recipe_id, type, entry, file_name, lora_index? }) + */ +export function showRematchSummary({ + scope = 'global', + cancelled = false, + total = 0, + matchedRecipes = 0, + matchedEntries = 0, + unresolvedRecipes = 0, + unresolvedEntries = 0, + skipped = 0, + errors = 0, + l4Matches = [], +} = {}) { + const matches = Array.isArray(l4Matches) ? l4Matches : []; + const undoneIndexes = new Set(); + const header = _resolveHeader({ + matchedEntries, + errors, + unresolvedEntries, + l4Count: matches.length, + cancelled, + }); + + const matchRows = matches.map((match, i) => ` +