// ---------- diagnostics ---------- // Extension ID display + copy button (added when Transfer Assistant was deleted). // Diagnostics is the canonical home for "what's my extension ID?" info now. (() => { const idEl = document.getElementById("diag-extension-id"); const copyBtn = document.getElementById("diag-copy-extension-id"); if (idEl) idEl.textContent = chrome.runtime.id; if (copyBtn) { copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(chrome.runtime.id); copyBtn.textContent = "Copied"; setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); } catch (_) { copyBtn.textContent = "Copy failed"; setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); } }); } })(); document.getElementById("run-diag").addEventListener("click", (event) => keepActionViewport(event.currentTarget, runDiagnostics) ); // ---------- native messaging RPC log ---------- const NATIVE_LOG_KEY = "rclonejavNativeLog"; function _fmtNativeLogTime(ts) { if (!Number.isFinite(ts)) return "?"; const d = new Date(ts); const pad = (n) => String(n).padStart(2, "0"); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`; } function _fmtBytes(n) { if (!Number.isFinite(n) || n < 0) return "?"; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`; return `${(n / 1024 / 1024).toFixed(2)} MiB`; } async function renderNativeLog() { const out = document.getElementById("native-log-results"); if (!out) return; const errOnly = document.getElementById("native-log-errors-only")?.checked; let entries = []; try { const got = await chrome.storage.local.get(NATIVE_LOG_KEY); entries = Array.isArray(got[NATIVE_LOG_KEY]) ? got[NATIVE_LOG_KEY] : []; } catch (e) { out.innerHTML = `error reading log: ${escapeHtml(e.message || String(e))}`; return; } if (errOnly) entries = entries.filter((e) => !e.ok); if (!entries.length) { out.innerHTML = `${errOnly ? "no errors recorded" : "no RPC calls recorded yet"}`; return; } out.innerHTML = entries.slice(0, 80).map((e) => { const ok = !!e.ok; const color = ok ? "#9be3b3" : "#ff9097"; const action = e.action || "?"; const latency = Number.isFinite(e.latency_ms) ? `${e.latency_ms}ms` : "?"; const size = e.resp_bytes != null ? ` · ${_fmtBytes(e.resp_bytes)}` : ""; const truncated = e.truncated ? ` · TRUNCATED${e.truncated_reason ? " (" + escapeHtml(e.truncated_reason) + ")" : ""}` : ""; const inflight = e.inflight != null ? ` · ${e.inflight} inflight` : ""; const head = `
${escapeHtml(_fmtNativeLogTime(e.ts))} ${ok ? "✓" : "✗"} ${escapeHtml(action)} · ${escapeHtml(latency)}${size}${truncated}${inflight}
`; const tail = !ok ? `
${escapeHtml(e.error_kind || "error")}: ${escapeHtml(e.error || "")}
` : ""; return `
${head}${tail}
`; }).join(""); } document.getElementById("native-log-run")?.addEventListener("click", renderNativeLog); document.getElementById("native-log-errors-only")?.addEventListener("change", renderNativeLog); document.getElementById("native-log-clear")?.addEventListener("click", async () => { if (!confirm("Clear extension-side native messaging log?")) return; await chrome.storage.local.remove(NATIVE_LOG_KEY); renderNativeLog(); }); document.getElementById("host-events-clear")?.addEventListener("click", async (e) => { if (!confirm("Truncate host/logs/rcjav-host-events.log on disk?")) return; const btn = e.currentTarget; const original = btn.textContent; btn.disabled = true; btn.textContent = "Clearing…"; try { const r = await chrome.runtime.sendMessage({ type: "clear-events-log" }); btn.textContent = r?.ok ? "Cleared" : `Failed: ${r?.error || "no response"}`; } catch (err) { btn.textContent = `Failed: ${err.message || err}`; } finally { setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 1500); } }); // Live update if SW writes new entries while the page is open. chrome.storage.onChanged.addListener((changes, area) => { if (area === "local" && NATIVE_LOG_KEY in changes) renderNativeLog(); }); document.getElementById("host-status-run").addEventListener("click", (event) => keepActionViewport(event.currentTarget, runHostStatus) ); document.getElementById("host-repair-run").addEventListener("click", (event) => keepActionViewport(event.currentTarget, runHostRepair) ); document.getElementById("host-verify-run").addEventListener("click", (event) => keepActionViewport(event.currentTarget, runHostStatus) ); document.getElementById("run-all-diag").addEventListener("click", (event) => keepActionViewport(event.currentTarget, async () => { clearNativeRepairCard(); const runtime = await runDiagnostics(); if (runtime && runtime.nativeBlocked) { renderBlockedByNativeIssue(document.getElementById("host-status-results"), "Host registration"); return; } await runHostStatus(); }) ); function renderDiagRows(out, checks, emptyLabel) { out.innerHTML = ""; if (!checks || checks.length === 0) { out.innerHTML = `
!${escapeHtml(emptyLabel)}no checks returned
`; return; } const counts = checks.reduce((acc, c) => { const status = c.status || "warn"; acc[status] = (acc[status] || 0) + 1; return acc; }, {}); const summary = document.createElement("div"); summary.className = "diag-row " + ((counts.fail || 0) ? "fail" : (counts.warn || 0) ? "warn" : "ok"); summary.innerHTML = `#summary${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}`; out.appendChild(summary); for (const c of checks) { const row = document.createElement("div"); row.className = "diag-row " + (c.status || "warn"); const status = c.status || "warn"; const icon = status === "ok" ? "✓" : status === "info" ? "i" : status === "warn" ? "!" : "✗"; row.innerHTML = `${icon}${escapeHtml(c.name)}${formatDiagDetail(c.detail || "")}`; out.appendChild(row); } } function formatDiagDetail(detail) { const text = String(detail || ""); if (!text) return ""; const shouldCollapse = text.length > 120 || text.includes("\n") || (text.match(/[;|]/g) || []).length > 2; if (!shouldCollapse) return escapeHtml(text); const first = text.split(/\r?\n/)[0].slice(0, 110); return `
${escapeHtml(first)}${text.length > first.length ? "…" : ""}
${escapeHtml(text)}
`; } async function runDiagnostics() { const out = document.getElementById("diag-results"); clearNativeRepairCard(); out.innerHTML = '
running…waiting for native host
'; try { const r = await chrome.runtime.sendMessage({ type: "diagnostics" }); if (!r || !r.ok) { await renderNativeMessagingFailure(r); renderBlockedByNativeIssue(out, "Runtime diagnostics"); return { nativeBlocked: true }; } renderDiagRows(out, r.checks || [], "runtime"); return { ok: true }; } catch (err) { out.innerHTML = `
runtime${escapeHtml(err.message || String(err))}
`; return { ok: false }; } } async function runHostStatus() { const out = document.getElementById("host-status-results"); clearNativeRepairCard(); out.innerHTML = '
checking…reading manifest and registry state
'; try { const r = await chrome.runtime.sendMessage({ type: "host-status" }); if (!r || !r.ok) { await renderNativeMessagingFailure(r); renderBlockedByNativeIssue(out, "Native host checks"); return { nativeBlocked: true }; } renderDiagRows(out, r.checks || [], "host status"); return { ok: true }; } catch (err) { out.innerHTML = `
host status${escapeHtml(err.message || String(err))}
`; return { ok: false }; } } async function runHostRepair() { const out = document.getElementById("host-status-results"); clearNativeRepairCard(); out.innerHTML = '
repairing…launching install-host.ps1 (UAC prompt will appear)
'; try { const r = await chrome.runtime.sendMessage({ type: "repair-host" }); if (!r || !r.ok) { if (r?.error_kind) { await renderNativeMessagingFailure(r); renderBlockedByNativeIssue(out, "Registration repair"); } else { out.innerHTML = `
Registration repair${escapeHtml(r?.error || "repair failed")}
`; } return { ok: false }; } out.innerHTML = ""; renderCompletedNativeRepair(r); return { ok: true }; } catch (err) { out.innerHTML = `
Registration repair${escapeHtml(err.message || String(err))}
`; return { ok: false }; } } function clearNativeRepairCard() { const card = document.getElementById("native-repair-card"); const out = document.getElementById("native-repair-results"); const title = document.getElementById("native-repair-title"); if (card) card.style.display = "none"; if (out) out.innerHTML = ""; if (title) title.textContent = "Native host setup"; } function renderCompletedNativeRepair(response) { const card = document.getElementById("native-repair-card"); const out = document.getElementById("native-repair-results"); if (!card || !out) return; card.style.display = ""; const title = document.getElementById("native-repair-title"); if (title) title.textContent = "install-host.ps1 launched"; out.innerHTML = `
Launcher started${escapeHtml(response.message || "install-host.ps1 launched")}
iScript${escapeHtml(response.script_path || "")}
iManifest target${escapeHtml(response.manifest_path || "")}
!Next steps1) Approve the UAC prompt. 2) Wait for the PowerShell window to print "Press Enter to close" and press Enter. 3) Fully close and reopen Brave. 4) Click Verify Registration.
`; } function renderBlockedByNativeIssue(out, title) { out.innerHTML = `
i${escapeHtml(title)}Blocked until this PC registers the native host for the current extension ID. Use the setup card above.
`; } async function getPackagedHostPaths() { try { const resp = await fetch(chrome.runtime.getURL("host/com.rcjav.host.json")); if (!resp.ok) return {}; const manifest = await resp.json(); const bat = manifest.path || ""; const hostDir = bat.replace(/[\\/][^\\/]+$/, ""); return { hostBat: bat, hostDir, registerBat: hostDir ? hostDir + "\\register-host.bat" : "", installPs1: hostDir ? hostDir + "\\install-host.ps1" : "", }; } catch { return {}; } } async function renderNativeMessagingFailure(response) { const card = document.getElementById("native-repair-card"); const out = document.getElementById("native-repair-results"); if (!card || !out) return; card.style.display = ""; const title = document.getElementById("native-repair-title"); if (title) title.textContent = "Register host on this PC"; const error = response?.error || "no response"; const kind = response?.error_kind || (/forbidden/i.test(error) ? "forbidden" : "unknown"); const extensionId = response?.extension_id || chrome.runtime.id; const paths = await getPackagedHostPaths(); const installCommand = paths.installPs1 ? `pwsh -ExecutionPolicy Bypass -File "${paths.installPs1}"` : `pwsh -ExecutionPolicy Bypass -File ".\\host\\install-host.ps1"`; const registerCommand = paths.registerBat ? `"${paths.registerBat}"` : ".\\host\\register-host.bat"; const hostDir = paths.hostDir || ""; const hostFolderUrl = hostDir ? "file:///" + hostDir.replace(/\\/g, "/").replace(/^([A-Za-z]:)/, "$1") : ""; let cause = "This extension cannot launch the native messaging host yet."; let fix = "Run register-host.bat once on this PC, fully restart Brave, then verify registration."; if (kind === "forbidden") { cause = "Brave found the native host but the extension ID is not in its allowlist on this PC."; fix = "Run register-host.bat to refresh the manifest from allowed-extension-ids.json."; } else if (kind === "not_found") { cause = "Brave could not find a registered native messaging host for com.rcjav.host on this PC."; fix = "Run register-host.bat from the extension host folder."; } else if (kind === "disconnected") { cause = "The native host started and then disconnected or crashed."; fix = "After registration is fixed, run Runtime diagnostics again to check Python, rc-jav, and rclone."; } else if (kind === "timeout") { cause = "The native host did not respond before the timeout."; fix = "Restart Brave and check whether a scan or rclone command is stuck."; } const openFolderBtn = hostFolderUrl ? `` : ""; out.innerHTML = `
!Setup requiredNative host registration must be fixed before cache, runtime, and host checks can run.
!Likely cause${escapeHtml(cause)}
iHost message${escapeHtml(error)}
Fix on this PC${escapeHtml(fix)}
1Run register-host
${escapeHtml(registerCommand)}
${escapeHtml(`Double-click ${registerCommand}\nor run the PowerShell alternative:\n${installCommand}\n\nThe script reads the extension ID from allowed-extension-ids.json — no paste step.`)}
${openFolderBtn}
2Restart BraveClose every Brave window/process, reopen Brave, then reload the extension.
3Verify
`; for (const btn of out.querySelectorAll("button[data-copy]")) { btn.addEventListener("click", async () => { await navigator.clipboard.writeText(btn.dataset.copy || ""); btn.textContent = "Copied"; setTimeout(() => { btn.textContent = btn.dataset.copyLabel || "Copy"; }, 1200); }); } for (const btn of out.querySelectorAll("button[data-open-folder]")) { btn.addEventListener("click", async () => { const url = btn.dataset.openFolder; const folderPath = btn.dataset.folderPath || ""; try { // file:// URLs require "Allow access to file URLs" toggled on for the // extension. If Brave silently blocks it (no tab opens), fall back to // clipboard so the user can paste into File Explorer (Win+E). await chrome.tabs.create({ url }); btn.textContent = "Opening…"; setTimeout(() => { btn.textContent = "Open Host Folder"; }, 1500); } catch (_) { try { await navigator.clipboard.writeText(folderPath); } catch {} btn.textContent = "Blocked — path copied"; setTimeout(() => { btn.textContent = "Open Host Folder"; }, 2500); } }); } for (const btn of out.querySelectorAll("button[data-verify-registration]")) { btn.addEventListener("click", runHostStatus); } }