Refactor UI, settings, and code quality across all categories

- Fix Replace Cover: routes now read preview_path from form POST instead of session (session writes from background threads were lost)
- Fix batch generation: submit all jobs immediately, poll all in parallel via Promise.all
- Fix label NameError in character generate route
- Fix style detail missing characters context
- Selected Preview pane: click any image to select it; data-preview-path on all images across all 8 detail templates
- Gallery → Library rename across all index page headings and navbar
- Settings: add configurable LoRA/checkpoint directories; default checkpoint selector moved from navbar to settings page
- Consolidate 6 get_available_*_loras() into single get_available_loras(category) reading from Settings
- ComfyUI tooltip shows currently loaded checkpoint name
- Remove navbar checkpoint bar
- Phase 4 cleanup: remove dead _queue_generation(), add session.modified, standardize log prefixes, rename action_type → action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-05 22:48:28 +00:00
parent da55b0889b
commit a38915b354
21 changed files with 754 additions and 765 deletions

View File

@@ -17,7 +17,7 @@
<div class="card mb-4">
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
{% if character.image_path %}
<img src="{{ url_for('static', filename='uploads/' + character.image_path) }}" alt="{{ character.name }}" class="img-fluid">
<img src="{{ url_for('static', filename='uploads/' + character.image_path) }}" alt="{{ character.name }}" class="img-fluid" data-preview-path="{{ character.image_path }}">
{% else %}
<span class="text-muted">No Image Attached</span>
{% endif %}
@@ -44,35 +44,20 @@
</div>
</div>
{% if preview_image %}
<div class="card mb-4 border-success">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<span>Latest Preview</span>
<form action="{{ url_for('replace_cover_from_preview', slug=character.slug) }}" method="post" class="m-0">
<button type="submit" class="btn btn-sm btn-outline-light">Replace Cover</button>
</form>
</div>
<div class="card-body p-0">
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) }}" alt="Preview" class="img-fluid">
</div>
</div>
</div>
{% else %}
<div class="card mb-4 border-secondary d-none" id="preview-card">
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
<span>Latest Preview</span>
<div class="card mb-4 {% if preview_image %}border-success{% else %}border-secondary d-none{% endif %}" id="preview-card">
<div class="card-header {% if preview_image %}bg-success{% else %}bg-secondary{% endif %} text-white d-flex justify-content-between align-items-center" id="preview-card-header">
<span>Selected Preview</span>
<form action="{{ url_for('replace_cover_from_preview', slug=character.slug) }}" method="post" class="m-0" id="replace-cover-form">
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" disabled>Replace Cover</button>
<input type="hidden" name="preview_path" id="preview-path" value="{{ preview_image or '' }}">
<button type="submit" class="btn btn-sm btn-outline-light" id="replace-cover-btn" {% if not preview_image %}disabled{% endif %}>Replace Cover</button>
</form>
</div>
<div class="card-body p-0">
<div class="img-container" style="height: auto; min-height: 400px; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#imageModal" onclick="showImage(this.querySelector('img').src)">
<img id="preview-img" src="" alt="Preview" class="img-fluid">
<img id="preview-img" src="{{ url_for('static', filename='uploads/' + preview_image) if preview_image else '' }}" alt="Preview" class="img-fluid">
</div>
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
@@ -103,7 +88,7 @@
<h1 class="mb-0">{{ character.name }}</h1>
<a href="{{ url_for('edit_character', slug=character.slug) }}" class="btn btn-sm btn-link text-decoration-none">Edit Profile</a>
</div>
<a href="/" class="btn btn-outline-secondary">Back to Gallery</a>
<a href="/" class="btn btn-outline-secondary">Back to Library</a>
</div>
<!-- Outfit Switcher -->
@@ -227,9 +212,27 @@
const progressLabel = document.getElementById('progress-label');
const previewCard = document.getElementById('preview-card');
const previewImg = document.getElementById('preview-img');
const previewPath = document.getElementById('preview-path');
const replaceBtn = document.getElementById('replace-cover-btn');
const previewHeader = document.getElementById('preview-card-header');
let currentJobId = null;
let currentAction = null;
function selectPreview(relativePath, imageUrl) {
if (!relativePath) return;
previewImg.src = imageUrl;
previewPath.value = relativePath;
replaceBtn.disabled = false;
previewCard.classList.remove('d-none');
previewHeader.classList.replace('bg-secondary', 'bg-success');
previewCard.classList.replace('border-secondary', 'border-success');
}
// Clicking any image with data-preview-path selects it into the preview pane
document.addEventListener('click', e => {
const img = e.target.closest('img[data-preview-path]');
if (img) selectPreview(img.dataset.previewPath, img.src);
});
async function waitForJob(jobId) {
return new Promise((resolve, reject) => {
@@ -254,55 +257,35 @@
}
form.addEventListener('submit', async (e) => {
// Only intercept generate actions
const submitter = e.submitter;
if (!submitter || submitter.value !== 'preview') {
return;
}
if (!submitter || submitter.value !== 'preview') return;
e.preventDefault();
currentAction = submitter.value;
const formData = new FormData(form);
formData.append('action', currentAction);
// UI Reset
formData.append('action', 'preview');
progressContainer.classList.remove('d-none');
progressBar.style.width = '100%';
progressBar.textContent = '';
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressLabel.textContent = 'Queuing…';
try {
const response = await fetch(form.getAttribute('action'), {
method: 'POST',
body: formData,
method: 'POST', body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
progressContainer.classList.add('d-none');
return;
}
if (data.error) { alert('Error: ' + data.error); return; }
currentJobId = data.job_id;
progressLabel.textContent = 'Queued…';
// Wait for the background worker to finish
const jobResult = await waitForJob(currentJobId);
currentJobId = null;
// Image is already saved — just display it
if (jobResult.result && jobResult.result.image_url) {
previewImg.src = jobResult.result.image_url;
if (previewCard) previewCard.classList.remove('d-none');
const replaceBtn = document.getElementById('replace-cover-btn');
if (replaceBtn) replaceBtn.disabled = false;
if (jobResult.result?.image_url) {
selectPreview(jobResult.result.relative_path, jobResult.result.image_url);
}
} catch (err) {
console.error(err);
alert('Generation failed: ' + err.message);
@@ -312,8 +295,7 @@
}
});
});
// Image modal function
function showImage(src) {
document.getElementById('modalImage').src = src;
}