Files
ext-rclone-jav/src/options/options-diagnostics.js
T
admin 2d6a95682f Sync working tree before initial Gitea push
- File reorg: popup/options/bulk-check moved to src/ subdirs
- Shared modules: src/shared/id-extract.js, src/options/options-shared.js
- Host updates: rcjav-host.py + register/install scripts
- .gitignore expanded
2026-05-26 22:42:15 +02:00

344 lines
18 KiB
JavaScript

// ---------- 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 = `<span style="color:#faa;">error reading log:</span> ${escapeHtml(e.message || String(e))}`;
return;
}
if (errOnly) entries = entries.filter((e) => !e.ok);
if (!entries.length) {
out.innerHTML = `<span style="color:#777;">${errOnly ? "no errors recorded" : "no RPC calls recorded yet"}</span>`;
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 ? ` · <span style="color:#ffd784;">TRUNCATED${e.truncated_reason ? " (" + escapeHtml(e.truncated_reason) + ")" : ""}</span>` : "";
const inflight = e.inflight != null ? ` · ${e.inflight} inflight` : "";
const head = `<div><span style="color:#888;">${escapeHtml(_fmtNativeLogTime(e.ts))}</span> <span style="color:${color};">${ok ? "✓" : "✗"} ${escapeHtml(action)}</span> · ${escapeHtml(latency)}${size}${truncated}${inflight}</div>`;
const tail = !ok
? `<div style="color:#aaa;margin-left:14px;"><span style="color:#888;">${escapeHtml(e.error_kind || "error")}:</span> ${escapeHtml(e.error || "")}</div>`
: "";
return `<div class="activity-entry">${head}${tail}</div>`;
}).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 = `<div class="diag-row warn"><span class="icon">!</span><span class="name">${escapeHtml(emptyLabel)}</span><span class="detail">no checks returned</span></div>`;
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 = `<span class="icon">#</span><span class="name">summary</span><span class="detail">${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}</span>`;
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 = `<span class="icon">${icon}</span><span class="name">${escapeHtml(c.name)}</span><span class="detail">${formatDiagDetail(c.detail || "")}</span>`;
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 `<details><summary>${escapeHtml(first)}${text.length > first.length ? "…" : ""}</summary><pre>${escapeHtml(text)}</pre></details>`;
}
async function runDiagnostics() {
const out = document.getElementById("diag-results");
clearNativeRepairCard();
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">running…</span><span class="detail">waiting for native host</span></div>';
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">runtime</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
return { ok: false };
}
}
async function runHostStatus() {
const out = document.getElementById("host-status-results");
clearNativeRepairCard();
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">checking…</span><span class="detail">reading manifest and registry state</span></div>';
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">host status</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
return { ok: false };
}
}
async function runHostRepair() {
const out = document.getElementById("host-status-results");
clearNativeRepairCard();
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">repairing…</span><span class="detail">launching install-host.ps1 (UAC prompt will appear)</span></div>';
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(r?.error || "repair failed")}</span></div>`;
}
return { ok: false };
}
out.innerHTML = "";
renderCompletedNativeRepair(r);
return { ok: true };
} catch (err) {
out.innerHTML = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
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 = `
<div class="diag-row ok"><span class="icon">✓</span><span class="name">Launcher started</span><span class="detail">${escapeHtml(response.message || "install-host.ps1 launched")}</span></div>
<div class="diag-row info"><span class="icon">i</span><span class="name">Script</span><span class="detail">${escapeHtml(response.script_path || "")}</span></div>
<div class="diag-row info"><span class="icon">i</span><span class="name">Manifest target</span><span class="detail">${escapeHtml(response.manifest_path || "")}</span></div>
<div class="diag-row warn"><span class="icon">!</span><span class="name">Next steps</span><span class="detail">1) 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.</span></div>
`;
}
function renderBlockedByNativeIssue(out, title) {
out.innerHTML = `<div class="diag-row info"><span class="icon">i</span><span class="name">${escapeHtml(title)}</span><span class="detail">Blocked until this PC registers the native host for the current extension ID. Use the setup card above.</span></div>`;
}
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
? `<button type="button" data-open-folder="${escapeHtml(hostFolderUrl)}" data-folder-path="${escapeHtml(hostDir)}">Open Host Folder</button>`
: "";
out.innerHTML = `
<div class="diag-row warn"><span class="icon">!</span><span class="name">Setup required</span><span class="detail">Native host registration must be fixed before cache, runtime, and host checks can run.</span></div>
<div class="diag-row warn"><span class="icon">!</span><span class="name">Likely cause</span><span class="detail">${escapeHtml(cause)}</span></div>
<div class="diag-row info"><span class="icon">i</span><span class="name">Host message</span><span class="detail">${escapeHtml(error)}</span></div>
<div class="diag-row ok"><span class="icon">→</span><span class="name">Fix on this PC</span><span class="detail">${escapeHtml(fix)}</span></div>
<div class="diag-row info"><span class="icon">1</span><span class="name">Run register-host</span><span class="detail">
<details open><summary>${escapeHtml(registerCommand)}</summary><pre>${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.`)}</pre></details>
<span class="diag-action">${openFolderBtn}<button type="button" data-copy="${escapeHtml(hostDir)}" data-copy-label="Copy Folder Path">Copy Folder Path</button><button type="button" data-copy="${escapeHtml(registerCommand)}" data-copy-label="Copy Script Path">Copy Script Path</button><button type="button" data-copy="${escapeHtml(installCommand)}" data-copy-label="Copy PowerShell Alternative">Copy PowerShell Alternative</button></span>
</span></div>
<div class="diag-row info"><span class="icon">2</span><span class="name">Restart Brave</span><span class="detail">Close every Brave window/process, reopen Brave, then reload the extension.</span></div>
<div class="diag-row info"><span class="icon">3</span><span class="name">Verify</span><span class="detail"><span class="diag-action"><button type="button" data-verify-registration>Verify Registration</button></span></span></div>
`;
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);
}
}