Initial commit: macOS USB Stick Creator - Docker Compose Web-GUI fuer bootfaehige macOS-Installer
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
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 = "<div class='detail'>Scanne USB-Geraete...</div>";
|
||||
const res = await fetch("/api/devices");
|
||||
const data = await res.json();
|
||||
deviceList.innerHTML = "";
|
||||
if (!data.items || data.items.length === 0) {
|
||||
deviceList.innerHTML = "<div class='detail'>Keine USB-Sticks gefunden. Stick einstecken, dann 'neu scannen'.</div>";
|
||||
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 = `<div>
|
||||
<div class="path">${dev.path} ${dev.is_root_disk ? "(SYSTEM-PLATTE - gesperrt)" : ""}</div>
|
||||
<div class="meta">${dev.vendor || ""} ${dev.model || ""} - ${fmtSize(dev.size_bytes)} - ${dev.transport || "?"}${dev.mounted ? " - GEMOUNTET" : ""}</div>
|
||||
</div>`;
|
||||
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();
|
||||
Reference in New Issue
Block a user