feat(bulk): consensus ML suggestions + frontend polish
Bulk editor now loads consensus ML suggestions across the current selection via a new POST /api/suggestions/bulk endpoint, powered by get_bulk_suggestions() in tag_suggestions.py. A tag is surfaced only if it was suggested for or already applied to >= 80% of the selection; coverage counts include images that already have the tag so a near- universal tag isn't penalized for dropping out of suggestion lists. Accept-only chips (no reject) match the rest of the suggestions UX. Frontend polish in the same commit: - Showcase session dedup (exclude seen ids, reset on second lap) and aspect-ratio-aware placeholder heights for better column balancing - Modal touch-swipe navigation on the image wrapper, auto-focus of the tag input on desktop (>=768px), and autocomplete flip-up when the dropdown would cover the Suggestions section - Settings template inline styles replaced with utility classes - Inline series-editor script extracted to gallery-series-editor.js Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+237
-63
@@ -19,14 +19,14 @@
|
||||
<div class="settings-grid-full">
|
||||
<div class="settings-container">
|
||||
<div class="settings-section">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
||||
<h2 style="margin: 0;">System Overview</h2>
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span id="statsUpdatedAt" style="font-size: 0.75rem; color: var(--text-muted);"></span>
|
||||
<button id="refreshStatsBtn" class="btn secondary-btn" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;" title="Refresh stats">Refresh</button>
|
||||
<div class="settings-section-header">
|
||||
<h2>System Overview</h2>
|
||||
<div class="settings-header-actions">
|
||||
<span id="statsUpdatedAt" class="text-muted-xs"></span>
|
||||
<button id="refreshStatsBtn" class="btn secondary-btn btn-compact" title="Refresh stats">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(5, 1fr);">
|
||||
<div class="stats-grid stats-grid-5">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="statTotalImages">-</span>
|
||||
<span class="stat-label">Total Images</span>
|
||||
@@ -60,7 +60,7 @@
|
||||
<p class="settings-description">Monitor and control the Celery-based import queue system.</p>
|
||||
|
||||
<div id="queueStatusSection">
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(6, 1fr);">
|
||||
<div class="stats-grid stats-grid-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queuePending">-</span>
|
||||
<span class="stat-label">Pending</span>
|
||||
@@ -88,14 +88,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Age breakdown tooltip -->
|
||||
<div id="ageBreakdown" style="display: none; margin-top: 0.5rem; padding: 0.5rem; background: rgba(255,255,255,0.05); border-radius: 4px; font-size: 0.8rem;">
|
||||
<span id="ageBreakdownTitle" style="color: var(--text-muted);"></span>:
|
||||
<div id="ageBreakdown" class="age-breakdown" style="display: none;">
|
||||
<span id="ageBreakdownTitle" class="text-muted"></span>:
|
||||
<span id="ageBreakdownToday">-</span> today,
|
||||
<span id="ageBreakdownWeek">-</span> this week,
|
||||
<span id="ageBreakdownOlder">-</span> older
|
||||
</div>
|
||||
|
||||
<div id="activeBatchInfo" class="import-stats" style="margin-top: 1rem; display: none;">
|
||||
<div id="activeBatchInfo" class="import-stats mt-md" style="display: none;">
|
||||
<h3>Active Batch</h3>
|
||||
<p>Processing <strong id="batchDir">-</strong></p>
|
||||
<div class="batch-progress">
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-actions" style="margin-top: 1rem; display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<div class="queue-actions">
|
||||
<button id="triggerImportBtn" class="btn primary-btn">Quick Scan</button>
|
||||
<button id="triggerDeepScanBtn" class="btn warning-btn" title="Full reprocessing: re-extracts metadata, runs pHash comparison, merges archive tags">Deep Scan</button>
|
||||
<button id="retryFailedBtn" class="btn warning-btn">Retry Failed Tasks</button>
|
||||
@@ -114,17 +114,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Task Cleanup Controls -->
|
||||
<div class="cleanup-controls" style="margin-top: 1rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border-radius: 6px;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<span style="color: var(--text-muted); font-size: 0.85rem;">Clear tasks:</span>
|
||||
<select id="clearStatusSelect" class="form-input" style="width: auto; min-width: 140px;">
|
||||
<div class="cleanup-controls">
|
||||
<div class="cleanup-controls-row">
|
||||
<span class="text-muted-sm">Clear tasks:</span>
|
||||
<select id="clearStatusSelect" class="form-input select-compact">
|
||||
<option value="complete,skipped">Complete + Skipped</option>
|
||||
<option value="complete">Complete only</option>
|
||||
<option value="skipped">Skipped only</option>
|
||||
<option value="failed">Failed only</option>
|
||||
<option value="complete,skipped,failed">All finished</option>
|
||||
</select>
|
||||
<select id="clearAgeSelect" class="form-input" style="width: auto; min-width: 120px;">
|
||||
<select id="clearAgeSelect" class="form-input select-compact-sm">
|
||||
<option value="0">All</option>
|
||||
<option value="1">Older than 1 day</option>
|
||||
<option value="7" selected>Older than 7 days</option>
|
||||
@@ -132,17 +132,17 @@
|
||||
</select>
|
||||
<button id="clearTasksBtn" class="btn secondary-btn">Clear</button>
|
||||
</div>
|
||||
<p style="color: var(--text-muted); font-size: 0.75rem; margin-top: 0.5rem;">
|
||||
<p class="cleanup-controls-note">
|
||||
Note: Failed/skipped tasks older than 7 days are auto-cleaned daily.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Task List with Filters -->
|
||||
<div id="taskListSection" style="margin-top: 1.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<h3 style="margin: 0;">Import Tasks</h3>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||||
<select id="taskStatusFilter" class="form-input" style="width: auto; min-width: 120px; padding: 0.4rem;">
|
||||
<div id="taskListSection" class="mt-lg">
|
||||
<div class="task-list-toolbar">
|
||||
<h3>Import Tasks</h3>
|
||||
<div class="task-list-filters">
|
||||
<select id="taskStatusFilter" class="form-input select-compact-sm">
|
||||
<option value="">All statuses</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="skipped">Skipped</option>
|
||||
@@ -151,57 +151,57 @@
|
||||
<option value="queued">Queued</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
<input type="text" id="taskSearchInput" class="form-input" placeholder="Search..." style="width: 150px; padding: 0.4rem;">
|
||||
<button id="taskSearchBtn" class="btn secondary-btn" style="padding: 0.4rem 0.75rem;">Search</button>
|
||||
<input type="text" id="taskSearchInput" class="form-input task-list-search" placeholder="Search...">
|
||||
<button id="taskSearchBtn" class="btn secondary-btn btn-compact-md">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="taskListInfo" style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem;">
|
||||
<div id="taskListInfo" class="task-list-info">
|
||||
Showing <span id="taskListShowing">0</span> of <span id="taskListTotal">0</span> tasks
|
||||
</div>
|
||||
|
||||
<div class="tasks-table-wrapper" style="max-height: 400px; overflow-y: auto;">
|
||||
<div class="tasks-table-wrapper">
|
||||
<table class="tasks-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40%;">File</th>
|
||||
<th style="width: 10%;">Type</th>
|
||||
<th style="width: 10%;">Status</th>
|
||||
<th style="width: 30%;">Error/Reason</th>
|
||||
<th style="width: 10%;">Time</th>
|
||||
<th class="col-file">File</th>
|
||||
<th class="col-type">Type</th>
|
||||
<th class="col-status">Status</th>
|
||||
<th class="col-error">Error/Reason</th>
|
||||
<th class="col-time">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="taskListBody">
|
||||
<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">Loading...</td></tr>
|
||||
<tr><td colspan="5" class="table-message-cell">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
|
||||
<div class="table-actions">
|
||||
<button id="loadMoreTasksBtn" class="btn secondary-btn" style="display: none;">Load More</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Detail Modal -->
|
||||
<div id="errorDetailModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-content error-detail-content">
|
||||
<div class="modal-header">
|
||||
<h3>Task Details</h3>
|
||||
<button class="modal-close" onclick="document.getElementById('errorDetailModal').style.display='none'">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div class="error-detail-row">
|
||||
<strong>File:</strong>
|
||||
<div id="errorDetailPath" style="word-break: break-all; font-family: monospace; font-size: 0.85rem; background: rgba(0,0,0,0.2); padding: 0.5rem; border-radius: 4px; margin-top: 0.25rem;"></div>
|
||||
<div id="errorDetailPath" class="error-detail-path"></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div class="error-detail-row">
|
||||
<strong>Status:</strong> <span id="errorDetailStatus"></span>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div class="error-detail-row">
|
||||
<strong>Error/Reason:</strong>
|
||||
<div id="errorDetailMessage" style="word-break: break-word; font-family: monospace; font-size: 0.85rem; background: rgba(255,100,100,0.1); padding: 0.5rem; border-radius: 4px; margin-top: 0.25rem; white-space: pre-wrap;"></div>
|
||||
<div id="errorDetailMessage" class="error-detail-message"></div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem; font-size: 0.85rem; color: var(--text-muted);">
|
||||
<div class="error-detail-meta">
|
||||
<span><strong>Retries:</strong> <span id="errorDetailRetries"></span></span>
|
||||
<span><strong>Created:</strong> <span id="errorDetailCreated"></span></span>
|
||||
</div>
|
||||
@@ -265,7 +265,7 @@
|
||||
<p class="form-help">Controls how similar images must be to be considered duplicates.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn primary-btn" style="width: 100%;">Save Filter Settings</button>
|
||||
<button type="submit" class="btn primary-btn btn-fullwidth">Save Filter Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,12 +305,12 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn danger-btn" style="width: 100%;" disabled id="deleteByTagBtn">Delete Images</button>
|
||||
<button type="submit" class="btn danger-btn btn-fullwidth" disabled id="deleteByTagBtn">Delete Images</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-container" style="margin-top: 1rem;">
|
||||
<div class="settings-container mt-md">
|
||||
<div class="settings-section">
|
||||
<h2>Clean Up Existing Images</h2>
|
||||
<p class="settings-description">Scan and remove images that don't meet filter criteria.</p>
|
||||
@@ -343,10 +343,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="scanFilterBtn" class="btn primary-btn" style="width: 100%;">Scan for Matching Images</button>
|
||||
<button type="button" id="scanFilterBtn" class="btn primary-btn btn-fullwidth">Scan for Matching Images</button>
|
||||
</div>
|
||||
|
||||
<div id="cleanupResults" style="display: none; margin-top: 1rem;">
|
||||
<div id="cleanupResults" class="mt-md" style="display: none;">
|
||||
<div class="import-stats">
|
||||
<h3>Scan Results</h3>
|
||||
<p><strong id="cleanupCount">0</strong> images match the filter criteria.</p>
|
||||
@@ -354,27 +354,27 @@
|
||||
|
||||
<div id="cleanupPreview" class="cleanup-preview"></div>
|
||||
|
||||
<div id="cleanupActions" style="display: none; margin-top: 1rem;">
|
||||
<div id="cleanupActions" class="mt-md" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="form-label checkbox-label">
|
||||
<input type="checkbox" id="cleanupSelectAll">
|
||||
Select all for deletion
|
||||
</label>
|
||||
</div>
|
||||
<p class="form-help" style="margin-bottom: 0.75rem;">Selected: <strong id="cleanupSelectedCount">0</strong> images</p>
|
||||
<button type="button" id="deleteSelectedBtn" class="btn danger-btn" style="width: 100%;" disabled>Delete Selected Images</button>
|
||||
<p class="form-help mb-sm">Selected: <strong id="cleanupSelectedCount">0</strong> images</p>
|
||||
<button type="button" id="deleteSelectedBtn" class="btn danger-btn btn-fullwidth" disabled>Delete Selected Images</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duplicate Detection -->
|
||||
<div class="settings-container" style="margin-top: 1rem;">
|
||||
<div class="settings-container mt-md">
|
||||
<div class="settings-section">
|
||||
<h2>Duplicate Detection</h2>
|
||||
<p class="settings-description">Scan for visually similar images using perceptual hash comparison. The first image in each group has the highest resolution and will be kept.</p>
|
||||
|
||||
<div class="form-card" style="margin-bottom: 1rem;">
|
||||
<div class="form-card mb-md">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Hamming Distance Threshold</label>
|
||||
<select id="dupThreshold" class="form-input">
|
||||
@@ -386,21 +386,21 @@
|
||||
<p class="form-help">Lower values find only very similar images. Higher values find more potential duplicates but may include false positives.</p>
|
||||
</div>
|
||||
|
||||
<button type="button" id="scanDuplicatesBtn" class="btn primary-btn" style="width: 100%;">Scan for Duplicates</button>
|
||||
<button type="button" id="scanDuplicatesBtn" class="btn primary-btn btn-fullwidth">Scan for Duplicates</button>
|
||||
</div>
|
||||
|
||||
<div id="dupResults" style="display: none;">
|
||||
<div class="import-stats" style="margin-bottom: 1rem;">
|
||||
<div class="import-stats mb-md">
|
||||
<h3>Scan Results</h3>
|
||||
<p>Found <strong id="dupGroupCount">0</strong> group(s) of potential duplicates.</p>
|
||||
</div>
|
||||
|
||||
<div id="dupGroups"></div>
|
||||
|
||||
<div id="dupActions" style="margin-top: 1rem; display: none;">
|
||||
<p class="form-help" style="margin-bottom: 0.75rem;">
|
||||
<div id="dupActions" class="mt-md" style="display: none;">
|
||||
<p class="form-help mb-sm">
|
||||
Selected for deletion: <strong id="dupSelectedCount">0</strong> image(s)
|
||||
<span style="color: var(--text-muted);">(The highest resolution image in each group is protected)</span>
|
||||
<span class="text-muted">(The highest resolution image in each group is protected)</span>
|
||||
</p>
|
||||
<button type="button" id="deleteDuplicatesBtn" class="btn danger-btn" disabled>Delete Selected Duplicates</button>
|
||||
</div>
|
||||
@@ -423,14 +423,14 @@
|
||||
<h2>Maintenance Tools</h2>
|
||||
<p class="settings-description">Background operations and worker management.</p>
|
||||
|
||||
<div class="queue-actions" style="margin-top: 0.75rem; display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<div class="queue-actions-compact">
|
||||
<button id="regenMissingThumbsBtn" class="btn secondary-btn">Regenerate Missing Thumbnails</button>
|
||||
<button id="reapplyArtistTagsBtn" class="btn secondary-btn">Reapply Artist Tags from Paths</button>
|
||||
<button id="cleanupOrphanedTagsBtn" class="btn secondary-btn">Cleanup Orphaned Tags</button>
|
||||
</div>
|
||||
|
||||
<div id="celeryWorkerInfo" style="margin-top: 1rem;">
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem;">
|
||||
<div id="celeryWorkerInfo" class="mt-md">
|
||||
<p class="text-muted-sm">
|
||||
Workers: <strong id="workerCount">-</strong> |
|
||||
Active Tasks: <strong id="activeTasks">-</strong>
|
||||
</p>
|
||||
@@ -447,7 +447,7 @@
|
||||
<button type="submit" class="btn secondary-btn">Run ML backfill</button>
|
||||
</form>
|
||||
|
||||
<p class="settings-hint" style="margin-top: 1em;">
|
||||
<p class="settings-hint mt-paragraph">
|
||||
Recompute centroids for all eligible tags (character, fandom, and general/topic) that have at least
|
||||
the minimum number of reference images. Run this after the initial backfill, or any time you've
|
||||
applied many tags manually and want the suggestions to catch up.
|
||||
@@ -456,7 +456,7 @@
|
||||
<button type="submit" class="btn secondary-btn">Recompute all centroids</button>
|
||||
</form>
|
||||
|
||||
<p class="settings-hint" style="margin-top: 1em;">
|
||||
<p class="settings-hint mt-paragraph">
|
||||
Attach each character's fandom to every image already tagged with that character.
|
||||
Additive only — existing fandom tags are never removed. Run after you've set a
|
||||
fandom on an existing character, or whenever you suspect drift.
|
||||
@@ -810,6 +810,180 @@
|
||||
.tasks-table .error-cell:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* === Utility classes === */
|
||||
.mt-md { margin-top: 1rem; }
|
||||
.mt-sm { margin-top: 0.75rem; }
|
||||
.mt-lg { margin-top: 1.5rem; }
|
||||
.mt-paragraph { margin-top: 1em; }
|
||||
.mb-md { margin-bottom: 1rem; }
|
||||
.mb-sm { margin-bottom: 0.75rem; }
|
||||
|
||||
.text-muted-xs { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.text-muted-sm { font-size: 0.85rem; color: var(--text-muted); }
|
||||
|
||||
.btn-compact {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
min-width: auto;
|
||||
}
|
||||
.btn-compact-md {
|
||||
padding: 0.4rem 0.75rem;
|
||||
min-width: auto;
|
||||
}
|
||||
.btn-fullwidth { width: 100%; min-width: 0; }
|
||||
|
||||
.select-compact {
|
||||
width: auto;
|
||||
min-width: 140px;
|
||||
}
|
||||
.select-compact-sm {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
/* === Section headers === */
|
||||
.settings-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.settings-section-header h2,
|
||||
.settings-section-header h3 {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.settings-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* === Stats grid variants === */
|
||||
.stats-grid-5 { grid-template-columns: repeat(5, 1fr); }
|
||||
.stats-grid-6 { grid-template-columns: repeat(6, 1fr); }
|
||||
|
||||
/* === Age breakdown info panel === */
|
||||
.age-breakdown {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* === Import queue layout === */
|
||||
.queue-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.queue-actions-compact {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* === Cleanup controls === */
|
||||
.cleanup-controls {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.cleanup-controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cleanup-controls-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* === Task list === */
|
||||
.task-list-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.task-list-toolbar h3 { margin: 0; }
|
||||
.task-list-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.task-list-search {
|
||||
width: 150px;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.task-list-info {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tasks-table-wrapper {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tasks-table .col-file { width: 40%; }
|
||||
.tasks-table .col-type { width: 10%; }
|
||||
.tasks-table .col-status { width: 10%; }
|
||||
.tasks-table .col-error { width: 30%; }
|
||||
.tasks-table .col-time { width: 10%; }
|
||||
.table-message-cell {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.table-actions {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* === Error detail modal === */
|
||||
.error-detail-content { max-width: 600px; }
|
||||
.error-detail-row { margin-bottom: 1rem; }
|
||||
.error-detail-path,
|
||||
.error-detail-message {
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.error-detail-path {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.error-detail-message {
|
||||
word-break: break-word;
|
||||
background: rgba(255, 100, 100, 0.1);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.error-detail-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* === Empty state (used by JS-injected content) === */
|
||||
.empty-state-centered {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -894,7 +1068,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">No tasks found</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="table-message-cell">No tasks found</td></tr>';
|
||||
}
|
||||
|
||||
// Update info
|
||||
@@ -1578,7 +1752,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
resultsEl.style.display = 'block';
|
||||
|
||||
if (cleanupImages.length === 0) {
|
||||
previewEl.innerHTML = '<p style="text-align: center; color: var(--text-muted); padding: 1rem;">No images match the filter criteria.</p>';
|
||||
previewEl.innerHTML = '<p class="empty-state-centered">No images match the filter criteria.</p>';
|
||||
actionsEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -1737,7 +1911,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
resultsEl.style.display = 'block';
|
||||
|
||||
if (duplicateGroups.length === 0) {
|
||||
groupsEl.innerHTML = '<p style="text-align: center; color: var(--text-muted); padding: 1rem;">No duplicate images found.</p>';
|
||||
groupsEl.innerHTML = '<p class="empty-state-centered">No duplicate images found.</p>';
|
||||
actionsEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user