// ---- Library Issues ---- let lastLibraryIssues = null; let _libraryIssuesDirty = false; let _libraryIssueTypeFilter = "all"; let _missingResolutionExtFilter = "all"; function _libraryIssueExportItems(r) { const missingRes = r?.missing_resolution || []; const visibleMissingRes = _missingResolutionExtFilter === "all" ? missingRes : missingRes.filter((e) => e.extension === _missingResolutionExtFilter); const includeAll = _libraryIssueTypeFilter === "all"; return { bracketNames: includeAll ? (r?.bracket_names || []) : [], noHyphenNames: includeAll ? (r?.nohyphen_names || []) : [], resolutionNoncanonical: includeAll || _libraryIssueTypeFilter === "noncanonical" ? (r?.resolution_noncanonical || []) : [], missingResolution: includeAll || _libraryIssueTypeFilter === "missing" ? visibleMissingRes : [], }; } function _safeExportToken(value) { return String(value || "all").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "") || "all"; } function _downloadJson(filename, data) { const blob = new Blob([JSON.stringify(data, null, 2) + "\n"], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } function _libraryIssueKindLabel(entry) { const labels = { resolution_copy_suffix: "copy suffix", resolution_part_suffix: "part suffix", resolution_bare_suffix: "bare res", resolution_placeholder_empty: "empty []", quality_marker_not_resolution: "quality tag", suspicious_bracket_token: "bad bracket", multipart_without_resolution: "part marker", missing_resolution: "missing res", }; const kinds = (entry.issues || []) .map((issue) => labels[issue.kind] || issue.kind) .filter(Boolean); return kinds.length ? kinds.join(" · ") : "Report only"; } function _canRenameIdFixRow(row) { return row && !row.classList.contains("report-only") && ["bracket_id", "nohyphen_id"].includes(row.dataset.issue) && row.dataset.remote && row.dataset.old && row.dataset.new; } function renderLibraryIssues(r) { const out = document.getElementById("library-issues-modal-body"); const statusEl = document.getElementById("library-issues-results"); const renameAllBtn = document.getElementById("library-issues-rename-all"); const exportBtn = document.getElementById("library-issues-export"); const renameStatus = document.getElementById("library-issues-rename-status"); if (!r || !r.ok) { lastLibraryIssues = null; renameAllBtn.disabled = true; exportBtn.disabled = true; out.innerHTML = `
Error: ${escapeHtml(r?.error || "no response")}
`; openModal("library-issues-modal"); return; } lastLibraryIssues = r; const brackets = r.bracket_names || []; const nohyphens = r.nohyphen_names || []; const missingRes = r.missing_resolution || []; const noncanonicalRes = r.resolution_noncanonical || []; const renameableTotal = brackets.length + nohyphens.length; const total = renameableTotal + missingRes.length + noncanonicalRes.length; const showRenameable = _libraryIssueTypeFilter === "all"; const showNoncanonical = _libraryIssueTypeFilter === "all" || _libraryIssueTypeFilter === "noncanonical"; const showMissing = _libraryIssueTypeFilter === "all" || _libraryIssueTypeFilter === "missing"; try { chrome.storage.local.set({ badge_library_issues_count: total }); } catch {} renameAllBtn.disabled = !showRenameable || renameableTotal === 0; renameAllBtn.title = renameableTotal ? "Rename only bracket-wrapped and no-hyphen ID fixes" : "No bracket-wrapped or no-hyphen ID fixes to rename"; exportBtn.disabled = total === 0; renameStatus.textContent = ""; const parts = []; if (!total) { parts.push(`
✓ No library issues found. All filenames are canonical.
`); } else { const typeButtons = [ ["all", "All", total], ["noncanonical", "Noncanonical", noncanonicalRes.length], ["missing", "Missing res", missingRes.length], ].map(([type, label, count]) => ( `` )).join(""); parts.push(`
${total} cache issue${total !== 1 ? "s" : ""} — ${brackets.length} bracket-wrapped, ${nohyphens.length} no-hyphen, ${missingRes.length} missing resolution tag, ${noncanonicalRes.length} noncanonical resolution ${typeButtons}
`); const makeRow = (entry, tagClass, tagLabel) => { const fname = entry.path.split("/").pop(); const dir = entry.path.lastIndexOf("/") !== -1 ? entry.path.slice(0, entry.path.lastIndexOf("/") + 1) : ""; return `
${tagLabel}
${escapeHtml(fname)} → ${escapeHtml(entry.canonical_name)}
${escapeHtml(entry.size_human || "")}
`; }; const makeReportRow = (entry, tagLabel = "no res", tagClass = "missingres") => { const fname = entry.filename || entry.path.split("/").pop(); return `
${escapeHtml(tagLabel)}
${escapeHtml(fname)} ${escapeHtml(entry.path)}
${escapeHtml(entry.size_human || "")} ${escapeHtml(_libraryIssueKindLabel(entry))}
`; }; if (showRenameable && brackets.length) { parts.push(`
Bracket-wrapped IDs (${brackets.length})
`); parts.push(brackets.map((e) => makeRow(e, "bracket", "[ ]")).join("")); } if (showRenameable && nohyphens.length) { parts.push(`
No-hyphen IDs (${nohyphens.length})
`); parts.push(nohyphens.map((e) => makeRow(e, "nohyphen", "no hyphen")).join("")); } if (showNoncanonical && noncanonicalRes.length) { parts.push(`
Resolution present, noncanonical (${noncanonicalRes.length})
`); parts.push(noncanonicalRes.map((e) => makeReportRow(e, "res style", "noncanonres")).join("")); } if (showMissing && missingRes.length) { const summary = r.missing_resolution_summary || {}; const byExt = summary.by_extension || {}; const extEntries = Object.entries(byExt).sort(([a], [b]) => a.localeCompare(b)); const extButtons = [ ["all", "All", missingRes.length], ...extEntries.map(([ext, count]) => [ext, ext, count]), ].map(([ext, label, count]) => ( `` )).join(""); const visibleMissingRes = _libraryIssueExportItems(r).missingResolution; parts.push(`
Missing resolution tag (${missingRes.length}) ${extButtons}
`); parts.push(visibleMissingRes.map((e) => makeReportRow(e)).join("")); } } out.innerHTML = parts.join(""); statusEl.textContent = total ? `${total} library issue(s) found. Review window is open.` : "No library issues found."; openModal("library-issues-modal"); // Per-row rename buttons out.querySelectorAll(".li-rename-btn").forEach((btn) => { btn.addEventListener("click", async () => { const row = btn.closest(".li-row"); if (!_canRenameIdFixRow(row)) return; const remote = row.dataset.remote; const oldPath = row.dataset.old; const newPath = row.dataset.new; btn.disabled = true; btn.textContent = "…"; const res = await chrome.runtime.sendMessage({ type: "rename_file", remote, old_path: oldPath, new_path: newPath, }); const tag = row.querySelector(".li-tag"); if (res?.ok) { tag.className = "li-tag done"; tag.textContent = "✓"; btn.textContent = "Done"; row.querySelector(".li-old").style.textDecoration = "line-through"; _libraryIssuesDirty = true; } else if (res?.conflict) { tag.className = "li-tag conflict"; tag.textContent = "conflict"; btn.textContent = "Skip"; renameStatus.textContent = `Conflict: ${res.error || "target exists"}`; } else { tag.className = "li-tag conflict"; tag.textContent = "error"; btn.textContent = "Error"; renameStatus.textContent = res?.error || "rename failed"; } }); }); out.querySelectorAll(".li-filter-chip").forEach((btn) => { btn.addEventListener("click", () => { if (btn.dataset.typeFilter) { _libraryIssueTypeFilter = btn.dataset.typeFilter || "all"; if (_libraryIssueTypeFilter !== "missing") _missingResolutionExtFilter = "all"; } else { _missingResolutionExtFilter = btn.dataset.extFilter || "all"; _libraryIssueTypeFilter = "missing"; } renderLibraryIssues(lastLibraryIssues); }); }); } document.getElementById("library-issues-run").addEventListener("click", async () => { const out = document.getElementById("library-issues-modal-body"); out.innerHTML = `
Loading library issues from cache…
`; openModal("library-issues-modal"); renderLibraryIssues(await chrome.runtime.sendMessage({ type: "library_issues" })); }); document.getElementById("library-issues-rename-all").addEventListener("click", async () => { const rows = [...document.querySelectorAll("#library-issues-modal-body .li-row")]; const renameStatus = document.getElementById("library-issues-rename-status"); const renameAllBtn = document.getElementById("library-issues-rename-all"); // Collect only legacy ID-fix renames. Resolution hygiene rows are report-only // until they have explicit, reviewed rename proposals. const pending = rows.reduce((acc, row) => { const btn = row.querySelector(".li-rename-btn"); if (!btn || btn.disabled || !_canRenameIdFixRow(row)) return acc; acc.push({ row, remote: row.dataset.remote, old_path: row.dataset.old, new_path: row.dataset.new }); return acc; }, []); if (!pending.length) { renameStatus.textContent = "No ID-fix rows are available to rename."; return; } const previewLimit = 12; const previewLines = pending.slice(0, previewLimit).map(({ old_path, new_path }) => ( `${old_path}\n -> ${new_path}` )); const remaining = pending.length - previewLines.length; const ok = confirm( `Rename ${pending.length} ID-fix file(s)?\n\n` + previewLines.join("\n\n") + (remaining > 0 ? `\n\n...and ${remaining} more.` : "") ); if (!ok) { renameStatus.textContent = "Rename ID fixes cancelled."; return; } renameAllBtn.disabled = true; renameStatus.textContent = `Renaming ${pending.length} ID-fix file(s)…`; const renames = pending.map(({ remote, old_path, new_path }) => ({ remote, old_path, new_path })); const res = await chrome.runtime.sendMessage({ type: "rename_files_batch", renames }); const results = res?.results || []; let done = 0, conflicts = 0, errors = 0; results.forEach((r, i) => { const { row } = pending[i]; const tag = row.querySelector(".li-tag"); const btn = row.querySelector(".li-rename-btn"); if (r.ok) { tag.className = "li-tag done"; tag.textContent = "✓"; btn.disabled = true; btn.textContent = "Done"; row.querySelector(".li-old").style.textDecoration = "line-through"; done++; } else if (r.conflict) { tag.className = "li-tag conflict"; tag.textContent = "conflict"; btn.disabled = false; btn.textContent = "Skip"; conflicts++; } else { tag.className = "li-tag conflict"; tag.textContent = "error"; btn.disabled = false; btn.textContent = "Error"; errors++; } }); const parts = []; if (done) parts.push(`${done} renamed`); if (conflicts) parts.push(`${conflicts} conflict(s)`); if (errors) parts.push(`${errors} error(s)`); renameStatus.textContent = parts.join(" · ") || "Nothing to rename."; renameAllBtn.disabled = false; _libraryIssuesDirty = done > 0; }); document.getElementById("library-issues-export").addEventListener("click", () => { if (!lastLibraryIssues?.ok) return; const { bracketNames, noHyphenNames, resolutionNoncanonical, missingResolution } = _libraryIssueExportItems(lastLibraryIssues); const activeFilter = _missingResolutionExtFilter || "all"; const activeType = _libraryIssueTypeFilter || "all"; const payload = { export_type: "rclone_jav_library_issues", generated_at: new Date().toISOString(), source: "cache", active_issue_type_filter: activeType, active_missing_resolution_filter: activeFilter, counts: { bracket_wrapped: bracketNames.length, no_hyphen: noHyphenNames.length, resolution_noncanonical: resolutionNoncanonical.length, missing_resolution: missingResolution.length, total: bracketNames.length + noHyphenNames.length + resolutionNoncanonical.length + missingResolution.length, full_cache_missing_resolution: lastLibraryIssues.missing_resolution?.length || 0, full_cache_resolution_noncanonical: lastLibraryIssues.resolution_noncanonical?.length || 0, }, bracket_names: bracketNames, nohyphen_names: noHyphenNames, resolution_noncanonical: resolutionNoncanonical, missing_resolution: missingResolution, }; const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const filterToken = _safeExportToken(`${activeType}-${activeType === "missing" ? activeFilter : "all"}`); _downloadJson(`rclone-jav-library-issues-${filterToken}-${stamp}.json`, payload); const renameStatus = document.getElementById("library-issues-rename-status"); renameStatus.textContent = `Exported ${payload.counts.total.toLocaleString()} row(s) as JSON.`; }); function _closeLibraryIssues() { closeModal("library-issues-modal"); if (_libraryIssuesDirty) { _libraryIssuesDirty = false; chrome.runtime.sendMessage({ type: "library_issues" }, (r) => { if (!r || !r.ok) return; const total = (r.bracket_names?.length || 0) + (r.nohyphen_names?.length || 0) + (r.missing_resolution?.length || 0) + (r.resolution_noncanonical?.length || 0); document.getElementById("library-issues-results").textContent = total ? `${total} library issue(s) found. Review window is open.` : "No library issues found."; }); } } for (const id of ["library-issues-modal-close", "library-issues-modal-done"]) { document.getElementById(id).addEventListener("click", _closeLibraryIssues); } document.getElementById("library-issues-modal").addEventListener("click", (e) => { if (e.target.id === "library-issues-modal") _closeLibraryIssues(); }); (function () { const rebuildBtn = document.getElementById("cache-rebuild-run"); const rebuildMode = document.getElementById("cache-rebuild-mode"); const cacheStatusOut = document.getElementById("cache-status-results"); const scanJobOut = document.getElementById("scan-job-results"); let _optScanTimer = null; let _optScanning = false; const _stopOptPoll = () => { if (_optScanTimer) { clearInterval(_optScanTimer); _optScanTimer = null; } }; function _setOptScanningState(scanning) { _optScanning = scanning; rebuildBtn.textContent = scanning ? "✕ Cancel" : "Rebuild Cache"; if (rebuildMode) rebuildMode.disabled = scanning; rebuildBtn.style.background = scanning ? "#3a1a1a" : ""; rebuildBtn.style.borderColor = scanning ? "#722" : ""; rebuildBtn.style.color = scanning ? "#faa" : ""; } function _scanStatus(r) { if (!r || r.no_state) return "idle"; if (r.scanning && !r.done) return "running"; if (r.cancelled) return "cancelled"; if (r.scan_ok === false) return "failed"; if (r.done) return "completed"; return "idle"; } function _formatScanDuration(seconds) { const s = Math.max(0, Math.round(Number(seconds) || 0)); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const rem = s % 60; if (m < 60) return `${m}m ${rem}s`; const h = Math.floor(m / 60); const mm = m % 60; return `${h}h ${mm}m`; } function _renderScanJob(r) { if (!r || r.no_state) { scanJobOut.innerHTML = `no scan job recorded yet`; return; } const status = _scanStatus(r); const pillCls = status === "completed" ? "ok" : status === "failed" ? "fail" : ""; const jobLabel = status === "running" ? "Current Scan Job" : "Last Scan Job"; const mode = r.scan_since ? `incremental ${r.scan_since}` : "full"; const scope = (r.scope && r.scope.length) ? r.scope.join(", ") : "configured scan roots"; const finished = r.finished_at || r.started_at || ""; const when = finished ? new Date(finished).toLocaleString() : ""; let elapsed = r.elapsed_s != null ? _formatScanDuration(r.elapsed_s) : ""; if (!elapsed && r.started_at) { const startedMs = Date.parse(r.started_at); if (Number.isFinite(startedMs)) elapsed = _formatScanDuration((Date.now() - startedMs) / 1000); } const scanPct = Number.isFinite(r.scan_percent) ? `${Number(r.scan_percent).toFixed(1)}%${r.scan_total_known_complete === false ? " known" : ""}` : ""; const eta = r.scanning ? (Number.isFinite(r.scan_eta_s) ? _formatScanDuration(r.scan_eta_s) : "calculating") : (status === "completed" ? "done" : ""); const knownCount = Number.isFinite(r.scan_files_done) && Number.isFinite(r.scan_files_total_known) ? `${Number(r.scan_files_done).toLocaleString()} / ${Number(r.scan_files_total_known).toLocaleString()}` : ""; const meta = [mode, scope].filter(Boolean).join(" · "); const metrics = [ ["Progress", scanPct || "0.0%"], ["ETA", eta || "--"], ["Files", knownCount || "--"], ["Elapsed", elapsed || "0s"], ]; const jobs = (r.remote_jobs && r.remote_jobs.length) ? r.remote_jobs : (r.remotes || []).map((remote, i) => ({ remote, status: remote === r.current_remote ? status : i < (r.current_index || 0) ? "completed" : "queued", files: remote === r.current_remote ? r.files_this_remote : null, total: remote === r.current_remote ? r.files_remote_total : null, })); const jobRoots = jobs.map((j) => j.remote).filter(Boolean); const retiredRoots = _configuredScanRoots.length ? jobRoots.filter((root) => !_configuredScanRoots.includes(root)) : []; const jobRows = jobs.map((j) => { const files = Number.isFinite(j.files) ? Number(j.files).toLocaleString() : "?"; const total = Number.isFinite(j.total) ? Number(j.total).toLocaleString() : ""; const pct = Number.isFinite(j.files) && Number.isFinite(j.total) && j.total > 0 ? Math.min(100, Math.round((j.files / j.total) * 100)) : null; const detail = [ j.label, j.incremental ? "incremental" : "", `${files}${total ? ` / ${total}` : ""} files`, Number.isFinite(j.skipped) && j.skipped ? `${j.skipped} skipped` : "", ].filter(Boolean).join(" · "); return `
${escapeHtml(j.remote || "?")} ${escapeHtml(j.status || "queued")} ${pct != null ? `${pct}%` : ""}
${escapeHtml(detail)}
${pct != null ? `
` : ""}
`; }).join(""); scanJobOut.innerHTML = `
${escapeHtml(jobLabel)} ${when ? `${escapeHtml(when)}` : ""}
${escapeHtml(status)} ${escapeHtml(meta || "scan job")}
${metrics.length ? `
${metrics.map(([label, value]) => ` ${escapeHtml(label)}${escapeHtml(value)} `).join("")}
` : ""} ${retiredRoots.length ? `
Historical scan roots not in current config: ${escapeHtml(retiredRoots.join(", "))}. They are shown because this job was recorded before the scan roots changed.
` : ""} ${r.error ? `
${escapeHtml(r.error)}
` : ""} ${jobRows || `
waiting for remote progress...
`} `; } const _pollOptProgress = () => { chrome.runtime.sendMessage({ type: "scan-progress" }, (r) => { if (chrome.runtime.lastError || !r || !r.ok) return; _renderScanJob(r); if (r.done || !r.scanning) { _stopOptPoll(); _setOptScanningState(false); if (r.cancelled) { return; } else if (r.scan_ok !== false) { setTimeout(() => document.getElementById("cache-status-run").click(), 500); } return; } }); }; async function _refreshScanJob() { try { const cache = await chrome.runtime.sendMessage({ type: "cache-status" }); if (cache && cache.ok) rememberConfiguredScanRoots(cache); } catch {} _pollOptProgress(); } async function _startOptScan(scanRoots = [], forceSince = null) { const out = scanJobOut; if (_optScanning) { // Cancel in-progress scan rebuildBtn.disabled = true; rebuildBtn.textContent = "Cancelling…"; chrome.runtime.sendMessage({ type: "scan-cancel" }, () => { rebuildBtn.disabled = false; // State will update on next poll tick }); return; } // forceSince overrides dropdown (used by per-remote Refresh to stay incremental) const scanSince = forceSince !== null ? forceSince : (rebuildMode ? rebuildMode.value : ""); const scope = scanRoots.length ? `refresh ${scanRoots.join(", ")}` : "all configured scan roots"; const label = scanSince ? `incrementally update files changed in the last ${scanSince}` : "fully rebuild"; const button = scanRoots.length ? "Refresh" : "Rebuild"; if (!confirm(`${button} cache now?\n\nScope: ${scope}\nMode: ${label}\n\nThis can take several minutes.`)) return; cacheStatusOut.innerHTML = `starting scan…`; out.innerHTML = ""; try { const r = await chrome.runtime.sendMessage({ type: "run-scan", scanSince, scanRoots }); if (!r || !r.ok) { out.innerHTML = `scan failed: ${escapeHtml(r?.error || "no response")}`; return; } _setOptScanningState(true); _pollOptProgress(); _optScanTimer = setInterval(_pollOptProgress, 1500); } catch (err) { out.innerHTML = `scan failed: ${escapeHtml(err.message || String(err))}`; } } rebuildBtn.addEventListener("click", () => _startOptScan()); function _renderNonJavPanel(items, remote) { const panel = document.createElement("div"); panel.className = "nonjav-panel"; panel.dataset.remote = remote; const deleteEnabled = document.getElementById("enableDelete")?.checked; const delBtnHtml = deleteEnabled ? `` : `Enable deletion in settings to delete`; panel.innerHTML = `
${escapeHtml(remote)} · ${items.length} non-JAV file${items.length !== 1 ? "s" : ""} ${delBtnHtml}
${items.map(f => `
${escapeHtml(f.ext || "?")} ${escapeHtml(f.path)} ${deleteEnabled ? `` : ""}
`).join("")}
`; // Delete one panel.addEventListener("click", async (e) => { const btn = e.target.closest(".nonjav-del-one"); if (btn) { const item = btn.closest(".nonjav-item"); const path = item?.dataset.fullPath; if (!path) return; if (!confirm(`Delete?\n${path}`)) return; btn.disabled = true; const r = await chrome.runtime.sendMessage({ type: "delete-skipped", paths: [path] }); if (r?.ok) { item.classList.add("deleted"); item.querySelector(".nonjav-del-one")?.remove(); _updateNonJavDelAll(panel); } else { btn.disabled = false; panel.querySelector(".nonjav-status").textContent = "Error: " + (r?.error || "failed"); } return; } const delAll = e.target.closest(".nonjav-del-all"); if (delAll) { const allItems = [...panel.querySelectorAll(".nonjav-item:not(.deleted)")]; const paths = allItems.map(i => i.dataset.fullPath).filter(Boolean); if (!paths.length) return; if (!confirm(`Delete all ${paths.length} non-JAV file(s) from ${remote}?`)) return; delAll.disabled = true; const statusEl = panel.querySelector(".nonjav-status"); statusEl.textContent = `Deleting ${paths.length} file(s)…`; const r = await chrome.runtime.sendMessage({ type: "delete-skipped", paths }); const ok = r?.deleted_count || 0; const fail = r?.failed_count || 0; if (ok) { // Mark successfully deleted items const deletedPaths = new Set( (r.results || []).filter(x => x.ok).map(x => x.path) ); allItems.forEach(i => { if (deletedPaths.has(i.dataset.fullPath)) { i.classList.add("deleted"); i.querySelector(".nonjav-del-one")?.remove(); } }); _updateNonJavDelAll(panel); } statusEl.textContent = fail ? `Deleted ${ok}, failed ${fail}. Check deletion settings.` : `Deleted ${ok} file(s).`; } }); return panel; } function _updateNonJavDelAll(panel) { const remaining = panel.querySelectorAll(".nonjav-item:not(.deleted)").length; const btn = panel.querySelector(".nonjav-del-all"); if (btn) { btn.textContent = `Delete All (${remaining})`; btn.disabled = remaining === 0; } } cacheStatusOut.addEventListener("click", (event) => { const showSkipped = event.target.closest(".cache-show-skipped"); if (showSkipped) { const remote = showSkipped.dataset.remote; // Toggle: if panel already open, close it const existing = cacheStatusOut.querySelector(`.nonjav-panel[data-remote="${CSS.escape(remote)}"]`); if (existing) { existing.remove(); showSkipped.textContent = showSkipped.textContent.replace("▴", "▾"); return; } showSkipped.textContent = showSkipped.textContent.replace("▾", "▴"); // Find skipped items from last cache status result const items = (_cacheSkippedByRemote?.get(remote)) || []; const panel = _renderNonJavPanel(items, remote); // Insert after the row containing this button showSkipped.closest("div")?.after(panel); return; } const reextract = event.target.closest(".cache-reextract"); if (reextract) { const original = reextract.textContent; reextract.disabled = true; reextract.textContent = "Re-extracting…"; (async () => { try { const r = await chrome.runtime.sendMessage({ type: "reextract-ids" }); if (!r || !r.ok) { reextract.textContent = original; reextract.disabled = false; const note = document.createElement("div"); note.style.cssText = "color:#faa;margin-top:6px;font-size:11px;"; note.textContent = `Re-extract failed: ${r?.error || "no response"}`; reextract.after(note); return; } const note = document.createElement("div"); note.style.cssText = "color:#afa;margin-top:6px;font-size:11px;"; note.textContent = `Re-extracted ${r.total ?? 0} IDs · ${r.changed ?? 0} changed · ${r.unchanged ?? 0} unchanged · ${r.dropped ?? 0} dropped. Re-run Check Cache to refresh this view.`; reextract.replaceWith(note); } catch (err) { reextract.textContent = original; reextract.disabled = false; const note = document.createElement("div"); note.style.cssText = "color:#faa;margin-top:6px;font-size:11px;"; note.textContent = `Re-extract failed: ${err?.message || String(err)}`; reextract.after(note); } })(); return; } const refresh = event.target.closest(".cache-refresh-remote"); if (refresh) { const remote = refresh.dataset.remote || ""; if (!remote) return; // Per-remote Refresh is always incremental — inherit dropdown value if it's a // duration (not "Full Rebuild"), otherwise default to 24h. const dropdownVal = rebuildMode ? rebuildMode.value : ""; const refreshSince = dropdownVal || "24h"; _startOptScan([remote], refreshSince); return; } }); document.getElementById("scan-job-clear").addEventListener("click", async () => { if (!confirm("Clear recorded scan job history?\n\nThis only clears the Scan Job panel state. It does not change cache.json.")) return; scanJobOut.textContent = "clearing scan job history..."; const r = await chrome.runtime.sendMessage({ type: "scan-clear" }); if (!r || !r.ok) { scanJobOut.innerHTML = `clear failed: ${escapeHtml(r?.error || "no response")}`; return; } _renderScanJob({ ok: true, no_state: true }); }); // If Options is opened while a scan is already running, attach to it instead // of showing an idle Rebuild button. _refreshScanJob(); chrome.runtime.sendMessage({ type: "scan-progress" }, (r) => { if (chrome.runtime.lastError || !r || !r.ok) return; _renderScanJob(r); if (!r.scanning) return; _setOptScanningState(true); _optScanTimer = setInterval(_pollOptProgress, 1500); }); })();