let selectedProduct = null; let selectedDevice = null; let catalogItems = []; const versionSelect = document.getElementById("versionSelect"); const versionDetail = document.getElementById("versionDetail"); const catalogStatus = document.getElementById("catalogStatus"); const deviceList = document.getElementById("deviceList"); const confirmPath = document.getElementById("confirmPath"); const startBtn = document.getElementById("startBtn"); const progressCard = document.getElementById("progressCard"); const progressFill = document.getElementById("progressFill"); const progressPhase = document.getElementById("progressPhase"); const logEl = document.getElementById("log"); function fmtSize(bytes) { if (!bytes) return "?"; const gb = bytes / 1e9; return gb >= 1 ? gb.toFixed(1) + " GB" : (bytes / 1e6).toFixed(0) + " MB"; } async function loadVersions() { catalogStatus.textContent = "Lade Apple-Katalog..."; versionSelect.innerHTML = ""; const res = await fetch("/api/versions"); const data = await res.json(); catalogItems = data.items || []; if (data.loading) { catalogStatus.textContent = "Katalog wird noch geladen, gleich nochmal versuchen..."; setTimeout(loadVersions, 3000); return; } if (data.error) { catalogStatus.textContent = "Fehler beim Laden: " + data.error; return; } catalogStatus.textContent = `${catalogItems.length} macOS-Installer gefunden (Katalog: ${data.catalog_url ? data.catalog_url.split("/").pop() : "?"})`; for (const item of catalogItems) { const opt = document.createElement("option"); opt.value = item.id; const modeTag = item.mode === "recovery" ? " [experimentell]" : ""; opt.textContent = `${item.title} ${item.version} - ${fmtSize(item.size_bytes)}${modeTag}`; versionSelect.appendChild(opt); } } versionSelect.addEventListener("change", () => { selectedProduct = catalogItems.find((i) => i.id === versionSelect.value) || null; if (selectedProduct) { const modeText = selectedProduct.mode === "recovery" ? "Big Sur+ Pfad: experimentell, Extraktion kann fehlschlagen." : "Klassischer dd-Pfad: robust und gut getestet."; versionDetail.textContent = `${selectedProduct.title} ${selectedProduct.version} | ${fmtSize(selectedProduct.size_bytes)} | ${modeText}`; } updateStartEnabled(); }); async function loadDevices() { deviceList.innerHTML = "
Scanne USB-Geraete...
"; const res = await fetch("/api/devices"); const data = await res.json(); deviceList.innerHTML = ""; if (!data.items || data.items.length === 0) { deviceList.innerHTML = "
Keine USB-Sticks gefunden. Stick einstecken, dann 'neu scannen'.
"; return; } for (const dev of data.items) { const el = document.createElement("div"); const blocked = dev.is_root_disk || dev.mounted; el.className = "device" + (blocked ? " disabled" : ""); el.innerHTML = `
${dev.path} ${dev.is_root_disk ? "(SYSTEM-PLATTE - gesperrt)" : ""}
${dev.vendor || ""} ${dev.model || ""} - ${fmtSize(dev.size_bytes)} - ${dev.transport || "?"}${dev.mounted ? " - GEMOUNTET" : ""}
`; if (!blocked) { el.addEventListener("click", () => { document.querySelectorAll(".device").forEach((d) => d.classList.remove("selected")); el.classList.add("selected"); selectedDevice = dev; updateStartEnabled(); }); } deviceList.appendChild(el); } } function updateStartEnabled() { const ok = selectedProduct && selectedDevice && confirmPath.value.trim() === selectedDevice.path; startBtn.disabled = !ok; } confirmPath.addEventListener("input", updateStartEnabled); document.getElementById("refreshVersions").addEventListener("click", async () => { await fetch("/api/versions/refresh", { method: "POST" }); setTimeout(loadVersions, 1500); }); document.getElementById("refreshDevices").addEventListener("click", loadDevices); startBtn.addEventListener("click", async () => { startBtn.disabled = true; progressCard.style.display = "block"; logEl.textContent = ""; const res = await fetch("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_id: selectedProduct.id, device_path: selectedDevice.path, confirm_device_path: confirmPath.value.trim(), }), }); const data = await res.json(); if (data.error) { logEl.textContent = "FEHLER: " + data.error; return; } streamJob(data.job_id); }); function streamJob(jobId) { const es = new EventSource(`/api/jobs/${jobId}/stream`); es.onmessage = (ev) => { const data = JSON.parse(ev.data); progressFill.style.width = (data.progress || 0) + "%"; progressPhase.textContent = `${data.phase} (${data.progress || 0}%) - ${data.status}`; if (data.msg) { logEl.textContent += data.msg + "\n"; logEl.scrollTop = logEl.scrollHeight; } if (data.status === "done" || data.status === "failed" || data.status === "aborted") { es.close(); } }; es.onerror = () => { es.close(); }; } loadVersions(); loadDevices();