feat(recipes): add reconnect remediation paths for missing recipe LoRAs

- Snapshot pre-rematch entry state (reconnectSnapshot) so rematched
  entries can be undone via the existing restore flow
- Bulk missing-LoRA downloads mark unresolvable failures hash-invalid,
  flipping those entries from download to reconnect candidacy
- Recipe modal always offers a reconnect action next to download for
  missing LoRA entries
- Rematch runs collect an opt-in relaxed-matching choice (also reconnect
  missing models by file name) via a pre-run options dialog on the
  global, bulk and single-recipe entries
- L4 (filename-level) matches are listed in a results dialog with
  per-entry undo
This commit is contained in:
Will Miao
2026-09-09 06:59:54 +08:00
parent e747946f7a
commit 1b5cbbbaa0
33 changed files with 2103 additions and 76 deletions
@@ -125,4 +125,54 @@
</button>
</div>
</div>
</div>
<!-- Recipe Rematch Options Modal -->
<div id="rematchOptionsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>{{ t('modals.rematchOptions.title') }}</h2>
<span class="close" onclick="rematchModalManager.cancelOptions()">&times;</span>
</div>
<div class="modal-body">
<p class="confirmation-message" id="rematchOptionsMessage"></p>
<label class="rematch-option-card" for="rematchOptionsRelaxed">
<input type="checkbox" id="rematchOptionsRelaxed">
<span class="rematch-option-checkmark" aria-hidden="true"></span>
<span class="rematch-option-text">
<span class="rematch-option-title">{{ t('modals.rematchOptions.relaxedLabel') }}</span>
<span class="rematch-option-caveat">
<i class="fas fa-info-circle" aria-hidden="true"></i>
{{ t('modals.rematchOptions.relaxedDescription') }}
</span>
</span>
</label>
</div>
<div class="modal-actions">
<button class="secondary-btn" onclick="rematchModalManager.cancelOptions()">{{ t('common.actions.cancel') }}</button>
<button class="primary-btn" id="rematchOptionsConfirmBtn" onclick="rematchModalManager.confirmOptions()">
<i class="fas fa-sync-alt"></i>
{{ t('modals.rematchOptions.confirmButton') }}
</button>
</div>
</div>
</div>
<!-- Recipe Rematch L4 Results Modal -->
<div id="rematchResultsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>{{ t('modals.rematchResults.title') }}</h2>
<span class="close" onclick="modalManager.closeModal('rematchResultsModal')">&times;</span>
</div>
<div class="modal-body">
<p class="confirmation-message">{{ t('modals.rematchResults.message') }}</p>
<div class="rematch-results-preview">
<ul class="rematch-results-list" id="rematchResultsList"></ul>
</div>
</div>
<div class="modal-actions">
<button class="secondary-btn" onclick="modalManager.closeModal('rematchResultsModal')">{{ t('common.actions.close') }}</button>
</div>
</div>
</div>