let selectedProduct = null; // "stick" | "download" let selectedProductIds = new Set(); // "multiboot" let selectedDevice = null; let catalogItems = []; let libraryIds = new Set(); // Produkt-IDs die schon lokal (dauerhaft) vorhanden sind let mode = "stick"; // "stick" | "download" | "multiboot" const MULTIBOOT_MAX = 8; const step1Title = document.getElementById("step1Title"); const versionList = document.getElementById("versionList"); const versionDetail = document.getElementById("versionDetail"); const catalogStatus = document.getElementById("catalogStatus"); const deviceSection = document.getElementById("deviceSection"); const deviceList = document.getElementById("deviceList"); const confirmPath = document.getElementById("confirmPath"); const stickConfirm = document.getElementById("stickConfirm"); const downloadHint = document.getElementById("downloadHint"); const multibootHint = document.getElementById("multibootHint"); const step3Title = document.getElementById("step3Title"); 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"); const libraryStatus = document.getElementById("libraryStatus"); const modeStickBtn = document.getElementById("modeStick"); const modeDownloadBtn = document.getElementById("modeDownload"); const modeMultibootBtn = document.getElementById("modeMultiboot"); function fmtSize(bytes) { if (!bytes) return "?"; const gb = bytes / 1e9; return gb >= 1 ? gb.toFixed(1) + " GB" : (bytes / 1e6).toFixed(0) + " MB"; } function setMode(next) { mode = next; modeStickBtn.classList.toggle("active", mode === "stick"); modeDownloadBtn.classList.toggle("active", mode === "download"); modeMultibootBtn.classList.toggle("active", mode === "multiboot"); const needsDevice = mode === "stick" || mode === "multiboot"; deviceSection.style.display = needsDevice ? "" : "none"; stickConfirm.style.display = needsDevice ? "" : "none"; downloadHint.style.display = mode === "download" ? "" : "none"; multibootHint.style.display = mode === "multiboot" ? "" : "none"; step1Title.textContent = mode === "multiboot" ? "1. macOS-Versionen (2-8 auswaehlen)" : "1. macOS-Version"; step3Title.textContent = mode === "download" ? "3. Herunterladen" : "3. Bestaetigen & Schreiben"; startBtn.textContent = mode === "download" ? "Image herunterladen" : (mode === "multiboot" ? "Multiboot-Stick erstellen" : "Stick erstellen"); renderVersionOptions(); updateStartEnabled(); } modeStickBtn.addEventListener("click", () => setMode("stick")); modeDownloadBtn.addEventListener("click", () => setMode("download")); modeMultibootBtn.addEventListener("click", () => setMode("multiboot")); async function loadLibrary() { const res = await fetch("/api/library"); const data = await res.json(); libraryIds = new Set((data.items || []).map((it) => it.id)); const count = (data.items || []).length; const size = fmtSize(data.total_bytes || 0); libraryStatus.textContent = count ? `${count} Installer bereits dauerhaft lokal vorhanden -- ${size} in ./downloads/library/` : "Noch keine Installer dauerhaft lokal gespeichert."; renderVersionOptions(); } function updateMultibootSummary() { const items = catalogItems.filter((i) => selectedProductIds.has(i.id)); const totalBytes = items.reduce((sum, i) => sum + (i.size_bytes || 0), 0); versionDetail.textContent = items.length ? `${items.length} Versionen ausgewaehlt, Original-Images zusammen ca. ${fmtSize(totalBytes)} -- Stick muss inkl. Partitions-Puffer entsprechend groesser sein.` : `Waehle 2-${MULTIBOOT_MAX} macOS-Versionen fuer den Multiboot-Stick aus.`; } function renderVersionOptions() { versionList.innerHTML = ""; for (const item of catalogItems) { const row = document.createElement("label"); row.className = "version-row"; const input = document.createElement("input"); const modeTag = item.mode === "recovery" ? " [experimentell]" : ""; const offlineTag = libraryIds.has(item.id) ? " -- bereits offline vorhanden" : ""; const labelText = `${item.title} ${item.version} - ${fmtSize(item.size_bytes)}${modeTag}${offlineTag}`; if (mode === "multiboot") { input.type = "checkbox"; input.checked = selectedProductIds.has(item.id); input.addEventListener("change", () => { if (input.checked) { if (selectedProductIds.size >= MULTIBOOT_MAX) { input.checked = false; return; } selectedProductIds.add(item.id); } else { selectedProductIds.delete(item.id); } row.classList.toggle("selected", input.checked); updateMultibootSummary(); updateStartEnabled(); }); row.classList.toggle("selected", input.checked); } else { input.type = "radio"; input.name = "versionPick"; input.checked = !!selectedProduct && selectedProduct.id === item.id; input.addEventListener("change", () => { selectedProduct = item; const modeText = item.mode === "recovery" ? "Big Sur+ Pfad: experimentell, Extraktion kann fehlschlagen." : "Klassischer dd-Pfad: robust und gut getestet."; const offlineText = libraryIds.has(item.id) ? " | bereits offline vorhanden, kein erneuter Download noetig." : ""; versionDetail.textContent = `${item.title} ${item.version} | ${fmtSize(item.size_bytes)} | ${modeText}${offlineText}`; document.querySelectorAll(".version-row").forEach((r) => r.classList.remove("selected")); row.classList.add("selected"); updateStartEnabled(); }); row.classList.toggle("selected", input.checked); } const span = document.createElement("span"); span.textContent = labelText; row.appendChild(input); row.appendChild(span); versionList.appendChild(row); } if (mode === "multiboot") updateMultibootSummary(); } async function loadVersions() { catalogStatus.textContent = "Lade Apple-Katalog..."; 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() : "?"})`; renderVersionOptions(); } 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() { let ok; if (mode === "download") { ok = !!selectedProduct; } else if (mode === "multiboot") { ok = selectedProductIds.size >= 2 && !!selectedDevice && confirmPath.value.trim() === selectedDevice.path; } else { 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 payload = { job_type: mode }; if (mode === "multiboot") { payload.product_ids = Array.from(selectedProductIds); } else { payload.product_id = selectedProduct.id; } if (mode === "stick" || mode === "multiboot") { payload.device_path = selectedDevice.path; payload.confirm_device_path = confirmPath.value.trim(); } const res = await fetch("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json(); if (data.error) { logEl.textContent = "FEHLER: " + data.error; updateStartEnabled(); 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(); if (data.status === "done") { loadLibrary(); } updateStartEnabled(); } }; es.onerror = () => { es.close(); }; } setMode("stick"); loadLibrary().then(loadVersions); loadDevices();