feat: replace Cancel with Pause/Resume — survives server restarts
- scanner.py: replace cancel_requested with pause_requested throughout; pause during walk drains in-flight futures gracefully then saves state; phash phase processes in 500-image chunks with pause check between each; _save_pause_state() persists files_indexed/phashes_done/last_phase to DB; init_db() already detects killed-mid-scan (running→paused) on startup - main.py: add POST /api/scan/pause and POST /api/scan/resume endpoints; /api/scan/cancel kept as alias; scan_status now returns folder_path, files_indexed, phashes_done; scan_reset clears all new fields - index.html: "Cancel" → "⏸ Pause" button; new #paused-area banner shows folder, files indexed, phashes done with "▶ Resume" and "Full reset" buttons; updateScanUI handles paused status; pauseScan()/resumeScan() JS functions added; chip gains .paused amber style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
#scan-chip.complete { border-color: var(--success); color: var(--success); }
|
||||
#scan-chip.error { border-color: var(--danger); color: var(--danger); }
|
||||
#scan-chip.cancelled { border-color: var(--warning); color: var(--warning); }
|
||||
#scan-chip.paused { border-color: var(--warning); color: var(--warning); }
|
||||
#topbar-stats { margin-left: auto; display: flex; gap: 20px; font-size: 12px; color: var(--text-dim); }
|
||||
#topbar-stats span b { color: var(--text); }
|
||||
|
||||
@@ -242,6 +243,20 @@
|
||||
/* ── Rescan buttons ── */
|
||||
#rescan-area { display: none; margin-top: 16px; }
|
||||
#rescan-area.show { display: block; }
|
||||
|
||||
#paused-area { display: none; margin-top: 16px; }
|
||||
#paused-area.show { display: block; }
|
||||
.pause-banner {
|
||||
display: flex; align-items: flex-start; gap: 12px;
|
||||
background: rgba(226,164,58,.1);
|
||||
border: 1px solid rgba(226,164,58,.35);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pause-icon { font-size: 22px; line-height: 1; }
|
||||
.pause-title { font-weight: 600; color: var(--warning); margin-bottom: 4px; }
|
||||
.pause-details { font-size: 12px; color: var(--text-dim); line-height: 1.6; }
|
||||
.rescan-info { font-size: 12px; color: var(--text-dim); margin-bottom: 10px; }
|
||||
.rescan-buttons {
|
||||
display: flex;
|
||||
@@ -765,7 +780,21 @@
|
||||
<span class="phase-pill" data-phase="grouping">Grouping</span>
|
||||
</div>
|
||||
<div class="mt8">
|
||||
<button class="btn-secondary btn-sm" onclick="cancelScan()">Cancel</button>
|
||||
<button class="btn-secondary btn-sm" onclick="pauseScan()">▮▮ Pause</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="paused-area">
|
||||
<div class="pause-banner">
|
||||
<div class="pause-icon">▮▮</div>
|
||||
<div class="pause-info">
|
||||
<div class="pause-title">Scan paused</div>
|
||||
<div id="pause-details" class="pause-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<button class="btn-primary btn-sm" onclick="resumeScan()">▶ Resume</button>
|
||||
<button class="btn-danger btn-sm" onclick="confirmFullReset()">Full reset ⚠</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1058,9 +1087,11 @@ function updateScanUI(s) {
|
||||
chip.classList.add(s.status);
|
||||
|
||||
const isRunning = s.status === 'running';
|
||||
const isPaused = s.status === 'paused';
|
||||
el('progress-area').classList.toggle('show', isRunning);
|
||||
el('first-scan-ui').style.display = (s.scan_id || isRunning) ? 'none' : '';
|
||||
el('rescan-area').classList.toggle('show', !isRunning && !!s.scan_id);
|
||||
el('paused-area').classList.toggle('show', isPaused);
|
||||
el('first-scan-ui').style.display = (s.scan_id || isRunning || isPaused) ? 'none' : '';
|
||||
el('rescan-area').classList.toggle('show', !isRunning && !isPaused && !!s.scan_id);
|
||||
|
||||
if (isRunning) {
|
||||
el('progress-msg').textContent = s.message || '';
|
||||
@@ -1081,7 +1112,16 @@ function updateScanUI(s) {
|
||||
});
|
||||
}
|
||||
|
||||
if (s.scan_id && !isRunning) {
|
||||
if (isPaused) {
|
||||
const parts = [];
|
||||
if (s.folder_path) parts.push(`Folder: ${s.folder_path}`);
|
||||
if (s.files_indexed) parts.push(`${fmt(s.files_indexed)} files indexed`);
|
||||
if (s.phashes_done) parts.push(`${fmt(s.phashes_done)} phashes computed`);
|
||||
if (s.message) parts.push(s.message);
|
||||
el('pause-details').textContent = parts.join(' · ') || 'Progress saved';
|
||||
}
|
||||
|
||||
if (s.scan_id && !isRunning && !isPaused) {
|
||||
// populate rescan folder from last scan
|
||||
el('rescan-folder-input').value = el('folder-input').value || '/photos';
|
||||
}
|
||||
@@ -1114,11 +1154,20 @@ async function startScan(mode) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelScan() {
|
||||
async function pauseScan() {
|
||||
try {
|
||||
await api('POST', '/api/scan/cancel');
|
||||
showToast('Cancelling scan...');
|
||||
} catch(e) {}
|
||||
await api('POST', '/api/scan/pause');
|
||||
showToast('Pausing scan — finishing in-flight work...');
|
||||
} catch(e) { showToast('Error: ' + e.message, 3000); }
|
||||
}
|
||||
|
||||
async function resumeScan() {
|
||||
try {
|
||||
await api('POST', '/api/scan/resume');
|
||||
state.scanStatus = 'running';
|
||||
showToast('Resuming scan...');
|
||||
startPoller();
|
||||
} catch(e) { showToast('Error: ' + e.message, 4000); }
|
||||
}
|
||||
|
||||
function confirmFullReset() {
|
||||
@@ -1548,6 +1597,7 @@ async function init() {
|
||||
try {
|
||||
const s = await api('GET', '/api/scan/status');
|
||||
updateScanUI(s);
|
||||
state.scanStatus = s.status;
|
||||
if (s.status === 'running') startPoller();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user