feat: Multiboot-Stick-Modus (mehrere macOS-Versionen auf einem USB-Stick)
- GUI: dritter Modus-Umschalter, Checkbox-Mehrfachauswahl statt Single-Select - Backend: sgdisk partitioniert den Stick in N Apple_HFS-Partitionen (Typ AF00), jede mit Label = macOS-Titel/Version, jede bekommt ihr eigenes Installer-Image per dd -- Macs EFI-Bootpicker (Alt/Option) zeigt alle Partitionen zur Auswahl - Groessen-Check VOR dem Schreiben: bricht mit klarer Fehlermeldung ab wenn der Stick zu klein ist, statt mittendrin zu scheitern - Dockerfile: parted (partprobe) ergaenzt - End-to-end gegen ein echtes Loop-Device verifiziert: sgdisk legt korrekte Partitionen mit Labels an, dd schreibt in die richtige Partition, Inhalt stimmt exakt ueberein (nicht nur behauptet) - README: neuer Abschnitt 4 (Funktionsweise, Ablauf, ehrliche Grenzen), Multiboot als experimentell markiert (erbt recovery-Pfad-Unsicherheit bei Big Sur+, kein echter Mac-Boot-Test bisher durchgefuehrt)
This commit is contained in:
+94
-29
@@ -1,10 +1,14 @@
|
||||
let selectedProduct = null;
|
||||
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"
|
||||
let mode = "stick"; // "stick" | "download" | "multiboot"
|
||||
|
||||
const versionSelect = document.getElementById("versionSelect");
|
||||
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");
|
||||
@@ -12,6 +16,7 @@ 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");
|
||||
@@ -21,6 +26,7 @@ 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 "?";
|
||||
@@ -32,15 +38,24 @@ function setMode(next) {
|
||||
mode = next;
|
||||
modeStickBtn.classList.toggle("active", mode === "stick");
|
||||
modeDownloadBtn.classList.toggle("active", mode === "download");
|
||||
deviceSection.style.display = mode === "stick" ? "" : "none";
|
||||
stickConfirm.style.display = mode === "stick" ? "" : "none";
|
||||
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";
|
||||
step3Title.textContent = mode === "stick" ? "3. Bestaetigen & Schreiben" : "3. Herunterladen";
|
||||
startBtn.textContent = mode === "stick" ? "Stick erstellen" : "Image herunterladen";
|
||||
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");
|
||||
@@ -54,16 +69,68 @@ async function loadLibrary() {
|
||||
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() {
|
||||
versionSelect.innerHTML = "";
|
||||
versionList.innerHTML = "";
|
||||
for (const item of catalogItems) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = item.id;
|
||||
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" : "";
|
||||
opt.textContent = `${item.title} ${item.version} - ${fmtSize(item.size_bytes)}${modeTag}${offlineTag}`;
|
||||
versionSelect.appendChild(opt);
|
||||
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() {
|
||||
@@ -84,18 +151,6 @@ async function loadVersions() {
|
||||
renderVersionOptions();
|
||||
}
|
||||
|
||||
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.";
|
||||
const offlineText = libraryIds.has(selectedProduct.id) ? " | bereits offline vorhanden, kein erneuter Download noetig." : "";
|
||||
versionDetail.textContent = `${selectedProduct.title} ${selectedProduct.version} | ${fmtSize(selectedProduct.size_bytes)} | ${modeText}${offlineText}`;
|
||||
}
|
||||
updateStartEnabled();
|
||||
});
|
||||
|
||||
async function loadDevices() {
|
||||
deviceList.innerHTML = "<div class='detail'>Scanne USB-Geraete...</div>";
|
||||
const res = await fetch("/api/devices");
|
||||
@@ -126,9 +181,14 @@ async function loadDevices() {
|
||||
}
|
||||
|
||||
function updateStartEnabled() {
|
||||
const ok = mode === "download"
|
||||
? !!selectedProduct
|
||||
: selectedProduct && selectedDevice && confirmPath.value.trim() === selectedDevice.path;
|
||||
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);
|
||||
@@ -143,8 +203,13 @@ startBtn.addEventListener("click", async () => {
|
||||
startBtn.disabled = true;
|
||||
progressCard.style.display = "block";
|
||||
logEl.textContent = "";
|
||||
const payload = { product_id: selectedProduct.id, job_type: mode };
|
||||
if (mode === "stick") {
|
||||
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();
|
||||
}
|
||||
|
||||
+4
-2
@@ -19,19 +19,21 @@
|
||||
</div>
|
||||
<div class="mode-toggle">
|
||||
<button id="modeStick" class="mode-btn active" type="button">Auf USB-Stick schreiben</button>
|
||||
<button id="modeMultiboot" class="mode-btn" type="button">Multiboot-Stick (mehrere Versionen)</button>
|
||||
<button id="modeDownload" class="mode-btn" type="button">Nur herunterladen (Offline-Bibliothek)</button>
|
||||
</div>
|
||||
<p class="detail">Im Download-Modus wird das Original-Image dauerhaft unter <code>./downloads/library/</code> im Projektordner abgelegt — ideal um sich vorab eine komplette Offline-Sammlung aller macOS-Versionen aufzubauen (z.B. auf eine externe Platte), die auch ganz ohne Internet zum Stick-Bauen reicht. Ein Stick-Bau nutzt automatisch bereits heruntergeladene Images wieder, statt sie erneut zu laden.</p>
|
||||
<p id="multibootHint" class="detail" style="display:none">Legt 2-8 gewaehlte macOS-Versionen als eigene Partitionen auf EINEN Stick — Macs Bootpicker (Alt/Option-Taste beim Start) zeigt dann alle zur Auswahl. Braucht entsprechend mehr Platz auf dem Stick (Summe aller Image-Groessen + Puffer). <strong>Experimentell</strong>, besonders in Kombination mit Big-Sur+-Versionen (siehe README).</p>
|
||||
<div id="libraryStatus" class="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<h2>1. macOS-Version</h2>
|
||||
<h2 id="step1Title">1. macOS-Version</h2>
|
||||
<button id="refreshVersions" class="btn-ghost">neu laden</button>
|
||||
</div>
|
||||
<div id="catalogStatus" class="status"></div>
|
||||
<select id="versionSelect" size="10"></select>
|
||||
<div id="versionList" class="version-list"></div>
|
||||
<p id="versionDetail" class="detail"></p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -47,6 +47,26 @@ select {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
select option { padding: 0.3rem; }
|
||||
.version-list {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0d0f14;
|
||||
}
|
||||
.version-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.version-row:last-child { border-bottom: none; }
|
||||
.version-row:hover { background: #131620; }
|
||||
.version-row.selected { background: #1b2333; }
|
||||
.version-row input { flex: none; }
|
||||
.detail { color: var(--muted); font-size: 0.85rem; min-height: 1.2em; }
|
||||
.status { color: var(--muted); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.device {
|
||||
@@ -94,9 +114,11 @@ button:disabled { background: #333844; color: #777; cursor: not-allowed; }
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mode-btn {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
background: #0d0f14;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user