The scan page now says which models are unmatched or not scanned yet (type, folder, why Civitai had nothing) and what each scan did, model by model; rows open the model, scan it, look it up again or search Civitai. The progress box shows the counts and, when floating, a way to the result. The activity log records a scan when it ends with its result, and model edits (from -> to) and deletions (files moved) in detail. - scraper.py reports one JSON line per changed model (--report-file); a failing model no longer ends the scan. Unmatched info keeps why (not_found / network / offline); online scans look network/offline ones up again. After two models in a row without an answer the rest of the scan infers from the files (--civitai-down for the next folders) instead of waiting out every timeout. - civitai_client.py: the network side moved out of scraper.py; tells a 404 from Civitai being unreachable (CivitaiUnreachable). - api/scan_report.py: ScanJob turns a scan's reports into one result, kept as the last scan (/anomalous/last_scan) and logged; /anomalous/scan_model opens a listed model. - /anomalous/scan_all takes picked or listed models as `targets`, so one scan has one result; scan_summary lists unmatched and new models with their reason. - Activity middleware takes details a handler attaches (request[ACTIVITY_DETAIL]). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
2.1 KiB
JavaScript
45 lines
2.1 KiB
JavaScript
/**
|
|
* Follows scans started anywhere (the scan page, a single model, another tab): shows their
|
|
* progress and result, turns the rail's scan button into a spinner, and reloads the models when done.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { updateScanProgress, failScanProgress } from './scan_progress.js';
|
|
import { fetchLastScan, setScanButtonState, showScanResult } from './scan_runner.js';
|
|
|
|
const POLL_MS = 3000;
|
|
|
|
/** Starts polling; returns `isScanning()` for the toolbox's scan button. */
|
|
export function watchScans(owner) {
|
|
let scanning = false;
|
|
setInterval(async () => {
|
|
try {
|
|
let active = null;
|
|
if (owner.currentType) {
|
|
const params = new URLSearchParams({ type: owner.currentType, path_idx: owner.currentPathIdx || 0, subfolder: owner.currentSubfolder || '/' });
|
|
const local = await (await fetch(`/anomalous/scan_status?${params}`)).json();
|
|
if (local.scanning) active = local;
|
|
else if (local.interrupted) failScanProgress(t('scanProgressInterrupted'));
|
|
}
|
|
const global = await (await fetch('/anomalous/global_scan_status')).json();
|
|
if (global.scanning) active = global;
|
|
else if (global.interrupted) failScanProgress(t('scanProgressInterrupted'));
|
|
|
|
if (active) updateScanProgress(active);
|
|
const button = document.getElementById('anomalous-scan-btn');
|
|
if (active && !scanning) {
|
|
scanning = true;
|
|
if (button) setScanButtonState(button, true);
|
|
} else if (!active && scanning) {
|
|
scanning = false;
|
|
if (button) setScanButtonState(button, false);
|
|
showScanResult(owner, await fetchLastScan());
|
|
owner.loadModels();
|
|
owner.onScanFinished?.();
|
|
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
|
|
}
|
|
} catch (_) { /* the server may be restarting; the next poll tries again */ }
|
|
}, POLL_MS);
|
|
return () => scanning;
|
|
}
|