diff --git a/system/public/aesthetic.computer/disks/os.mjs b/system/public/aesthetic.computer/disks/os.mjs index 25be05a013..b578fa17f7 100644 --- a/system/public/aesthetic.computer/disks/os.mjs +++ b/system/public/aesthetic.computer/disks/os.mjs @@ -1,13 +1,13 @@ // os, 2026.03.12 // FedAC OS — build list with commit messages; download your personalized copy. -const OVEN = "https://oven-edge.aesthetic-computer.workers.dev"; -const OVEN_WS = "wss://oven.aesthetic.computer/ws"; -const OVEN_ORIGIN = "https://oven.aesthetic.computer"; -const ISO_BASE = OVEN + "/os/latest.iso"; -function OVEN_BASE() { return OVEN; } -function RELEASES_URL() { return OVEN + "/os-releases"; } -function OVEN_WS_URL() { return OVEN_WS; } +const OVEN = "https://oven-edge.aesthetic-computer.workers.dev"; +const OVEN_WS = "wss://oven.aesthetic.computer/ws"; +const OVEN_ORIGIN = "https://oven.aesthetic.computer"; +const ISO_BASE = OVEN + "/os/latest.iso"; +function OVEN_BASE() { return OVEN; } +function RELEASES_URL() { return OVEN + "/os-releases"; } +function OVEN_WS_URL() { return OVEN_WS; } function templateIsoUrl() { const base = ISO_BASE; if (variantIdx === 0) return base; @@ -58,6 +58,9 @@ let hasGit = null; let setupBtn = null; // "set up tokens" button shown when missing let showTokenHint = false; +// 💻 "Need a laptop?" ad +let laptopBtn = null; + // Build telemetry let activeBuild = null; // { id, status, stage, percent, ref, error } let buildLogLines = []; // last few log lines @@ -336,6 +339,7 @@ function makeButtons(ui) { updateBootBtn(ui); updateVariantBtn(ui); updateWifiBtn(ui); + laptopBtn = new ui.TextButton("blank", { x: 6, y: 0 }); } function updateVariantBtn(ui) { @@ -363,7 +367,7 @@ function updateWifiBtn(ui) { wifiBtn = new ui.TextButton(wifiEnabled ? "ON" : "OFF", { x: 6, y: 0 }); } -function timeAgo(ts) { +function timeAgo(ts) { if (!ts) return ""; const now = Date.now(); const then = new Date(ts).getTime(); @@ -377,37 +381,37 @@ function timeAgo(ts) { if (day < 30) return day + "d"; const mo = Math.floor(day / 30); if (mo < 12) return mo + "mo"; - return Math.floor(mo / 12) + "y"; -} - -function buildStatusInfo(build, isCurrent, isDep, darkMode) { - let status = (build?.status || "").toLowerCase(); - if (!status) status = isDep ? "deprecated" : "success"; - - if (status === "done") status = "success"; - if (status === "queued") status = "queue"; - - const dark = { - success: { label: "ok", color: [100, 225, 140] }, - failed: { label: "fail", color: [255, 125, 125] }, - cancelled: { label: "stop", color: [235, 185, 115] }, - running: { label: "run", color: [120, 185, 255] }, - queue: { label: "queue", color: [170, 185, 220] }, - deprecated: { label: "old", color: [170, 130, 130] }, - unknown: { label: "?", color: [150, 155, 170] }, - }; - const light = { - success: { label: "ok", color: [20, 115, 45] }, - failed: { label: "fail", color: [170, 35, 35] }, - cancelled: { label: "stop", color: [140, 95, 15] }, - running: { label: "run", color: [30, 95, 175] }, - queue: { label: "queue", color: [90, 105, 145] }, - deprecated: { label: "old", color: [130, 95, 95] }, - unknown: { label: "?", color: [120, 125, 135] }, - }; - const table = darkMode ? dark : light; - return table[status] || table.unknown; -} + return Math.floor(mo / 12) + "y"; +} + +function buildStatusInfo(build, isCurrent, isDep, darkMode) { + let status = (build?.status || "").toLowerCase(); + if (!status) status = isDep ? "deprecated" : "success"; + + if (status === "done") status = "success"; + if (status === "queued") status = "queue"; + + const dark = { + success: { label: "ok", color: [100, 225, 140] }, + failed: { label: "fail", color: [255, 125, 125] }, + cancelled: { label: "stop", color: [235, 185, 115] }, + running: { label: "run", color: [120, 185, 255] }, + queue: { label: "queue", color: [170, 185, 220] }, + deprecated: { label: "old", color: [170, 130, 130] }, + unknown: { label: "?", color: [150, 155, 170] }, + }; + const light = { + success: { label: "ok", color: [20, 115, 45] }, + failed: { label: "fail", color: [170, 35, 35] }, + cancelled: { label: "stop", color: [140, 95, 15] }, + running: { label: "run", color: [30, 95, 175] }, + queue: { label: "queue", color: [90, 105, 145] }, + deprecated: { label: "old", color: [130, 95, 95] }, + unknown: { label: "?", color: [120, 125, 135] }, + }; + const table = darkMode ? dark : light; + return table[status] || table.unknown; +} function paint($) { const { screen, ink, line: drawLine, dark, mask, unmask } = $; @@ -681,6 +685,24 @@ function paint($) { y += isMobile ? 14 : 10; + // --- LAPTOP AD --- + sectionHeader("Need a laptop?", dark ? [18, 14, 24] : [230, 222, 238], dark ? [30, 24, 40] : [215, 208, 225], 120); + ink(...C.instText).write("Run AC OS on any x86 laptop.", { x: pad, y, wrap: wrapW }, undefined, undefined, false, "MatrixChunky8"); + y += matrixH + 4; + if (laptopBtn) { + laptopBtn.reposition({ x: pad, y }); + laptopBtn.paint( + $, + [C.bootBtnBg, C.bootBtnBorder, ...C.current, 200], + [C.bootBtnHoverBg, C.bootBtnHoverBorder, [255, 255, 255], 255], + undefined, + [C.bootBtnBg, C.bootBtnBorder, ...C.current, 230], + ); + y += laptopBtn.height + btnGap; + } + + y += isMobile ? 14 : 10; + // --- BUILDS section --- sectionHeader("Builds", dark ? [16, 22, 36] : [218, 222, 238], C.secBuildBg, 2000); } else if (downloading) { @@ -723,10 +745,10 @@ function paint($) { const name = rawName.length > maxNameChars ? rawName.slice(0, maxNameChars - 1) + "~" : rawName; const hash = (b.git_hash || "?").slice(0, 7); const ago = timeAgo(b.build_ts); - const msg = b.commit_msg || ""; - const who = b.handle || ""; - const sizeMB = b.size ? (b.size / 1048576).toFixed(0) + "MB" : ""; - const statusInfo = buildStatusInfo(b, isCurrent, isDep, dark); + const msg = b.commit_msg || ""; + const who = b.handle || ""; + const sizeMB = b.size ? (b.size / 1048576).toFixed(0) + "MB" : ""; + const statusInfo = buildStatusInfo(b, isCurrent, isDep, dark); // Alternating strip background const strip = i % 2 === 0 ? stripA : stripB; @@ -763,18 +785,18 @@ function paint($) { x += (sizeMB.length + 1) * charW; } - if (who && !isNarrow) { - ink(...(isCurrent ? C.handle : isDep ? C.depHandle : C.handleOld)); - $.write("@" + who, { x, y: ry }); - } - - // Status tag on right edge - if (statusInfo) { - const tag = "[" + statusInfo.label + "]"; - const tagX = w - pad - tag.length * charW; - ink(...statusInfo.color); - $.write(tag, { x: tagX, y: ry }); - } + if (who && !isNarrow) { + ink(...(isCurrent ? C.handle : isDep ? C.depHandle : C.handleOld)); + $.write("@" + who, { x, y: ry }); + } + + // Status tag on right edge + if (statusInfo) { + const tag = "[" + statusInfo.label + "]"; + const tagX = w - pad - tag.length * charW; + ink(...statusInfo.color); + $.write(tag, { x: tagX, y: ry }); + } // Strikethrough for deprecated if (isDep) { @@ -828,7 +850,7 @@ function paint($) { } } -function act({ event: e, needsPaint, download }) { +function act({ event: e, needsPaint, download, jump }) { dlFn = download; if (e.is("dark-mode") || e.is("light-mode")) { @@ -887,6 +909,13 @@ function act({ event: e, needsPaint, download }) { }); } + // Laptop ad button: jump to blank + if (laptopBtn) { + laptopBtn.btn.act(e, { + push: () => jump("blank"), + }); + } + // Setup tokens button: show hint text if (setupBtn && token) { setupBtn.btn.act(e, { @@ -910,88 +939,88 @@ function act({ event: e, needsPaint, download }) { }); } -async function startDownload(needsPaint) { +async function startDownload(needsPaint) { downloading = true; downloadProgress = 0; downloadMB = 0; downloadTotalMB = 0; const piece = BOOT_PIECES[bootPieceIdx]; - downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; - console.log("[os] Starting download:", osLabel(), "piece:", piece); - needsPaint(); - - try { - const query = - "?piece=" + encodeURIComponent(piece) + - "&wifi=" + (wifiEnabled ? "1" : "0") + - "&layout=efi&strict=1&cb=" + Date.now() + - (variantIdx === 1 ? "&variant=cl" : ""); - // Strict EFI downloads must come from oven origin. - // The edge worker can return stale/truncated artifacts without layout headers. - const isoCandidates = [OVEN_ORIGIN + "/os-image" + query]; - const MIN_EXPECTED_EFI_BYTES = 300 * 1024 * 1024; - const MIN_EXPECTED_ISO_BYTES = 200 * 1024 * 1024; - - let res = null; - let usedUrl = ""; - let lastErr = ""; - for (const url of isoCandidates) { - console.log("[os] Fetching:", url); - let attempt; - try { - attempt = await fetch(url, { - headers: { Authorization: "Bearer " + token }, - }); - } catch (err) { - lastErr = err?.message || String(err); - continue; - } - - if (!attempt.ok) { - const err = await attempt.json().catch(() => ({})); - lastErr = err.error || ("Download failed: " + attempt.status); - continue; - } - - const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); - if (!servedLayout) { - console.warn("[os] Missing x-ac-os-layout header from", url); - try { attempt.body?.cancel(); } catch (_) {} - lastErr = "Server did not report image layout (x-ac-os-layout missing)"; - continue; - } - if (servedLayout && servedLayout !== "efi") { - console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); - try { attempt.body?.cancel(); } catch (_) {} - lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; - continue; - } - - const len = parseInt(attempt.headers.get("content-length") || "0"); - const minExpectedBytes = - servedLayout === "efi" - ? MIN_EXPECTED_EFI_BYTES - : servedLayout === "iso" - ? MIN_EXPECTED_ISO_BYTES - : MIN_EXPECTED_EFI_BYTES; - if (len > 0 && len < minExpectedBytes) { - console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url, "layout:", servedLayout || "?"); - try { attempt.body?.cancel(); } catch (_) {} - lastErr = "Received suspiciously small image (" + len + " bytes)"; - continue; - } - - res = attempt; - usedUrl = url; - break; - } - - if (!res) { - throw new Error(lastErr || "Download failed"); - } - console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); - - const contentLength = parseInt(res.headers.get("content-length") || "0"); + downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; + console.log("[os] Starting download:", osLabel(), "piece:", piece); + needsPaint(); + + try { + const query = + "?piece=" + encodeURIComponent(piece) + + "&wifi=" + (wifiEnabled ? "1" : "0") + + "&layout=efi&strict=1&cb=" + Date.now() + + (variantIdx === 1 ? "&variant=cl" : ""); + // Strict EFI downloads must come from oven origin. + // The edge worker can return stale/truncated artifacts without layout headers. + const isoCandidates = [OVEN_ORIGIN + "/os-image" + query]; + const MIN_EXPECTED_EFI_BYTES = 300 * 1024 * 1024; + const MIN_EXPECTED_ISO_BYTES = 200 * 1024 * 1024; + + let res = null; + let usedUrl = ""; + let lastErr = ""; + for (const url of isoCandidates) { + console.log("[os] Fetching:", url); + let attempt; + try { + attempt = await fetch(url, { + headers: { Authorization: "Bearer " + token }, + }); + } catch (err) { + lastErr = err?.message || String(err); + continue; + } + + if (!attempt.ok) { + const err = await attempt.json().catch(() => ({})); + lastErr = err.error || ("Download failed: " + attempt.status); + continue; + } + + const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); + if (!servedLayout) { + console.warn("[os] Missing x-ac-os-layout header from", url); + try { attempt.body?.cancel(); } catch (_) {} + lastErr = "Server did not report image layout (x-ac-os-layout missing)"; + continue; + } + if (servedLayout && servedLayout !== "efi") { + console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); + try { attempt.body?.cancel(); } catch (_) {} + lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; + continue; + } + + const len = parseInt(attempt.headers.get("content-length") || "0"); + const minExpectedBytes = + servedLayout === "efi" + ? MIN_EXPECTED_EFI_BYTES + : servedLayout === "iso" + ? MIN_EXPECTED_ISO_BYTES + : MIN_EXPECTED_EFI_BYTES; + if (len > 0 && len < minExpectedBytes) { + console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url, "layout:", servedLayout || "?"); + try { attempt.body?.cancel(); } catch (_) {} + lastErr = "Received suspiciously small image (" + len + " bytes)"; + continue; + } + + res = attempt; + usedUrl = url; + break; + } + + if (!res) { + throw new Error(lastErr || "Download failed"); + } + console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); + + const contentLength = parseInt(res.headers.get("content-length") || "0"); downloadTotalMB = contentLength / 1048576; downloadStatus = "downloading..."; needsPaint(); @@ -1013,27 +1042,27 @@ async function startDownload(needsPaint) { downloadStatus = "preparing file..."; needsPaint(); - const total = chunks.reduce((s, c) => s + c.length, 0); - const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); - const minExpectedBytes = - servedLayout === "efi" - ? MIN_EXPECTED_EFI_BYTES - : servedLayout === "iso" - ? MIN_EXPECTED_ISO_BYTES - : MIN_EXPECTED_EFI_BYTES; - if (total < minExpectedBytes) { - throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); - } - if (contentLength > 0 && total !== contentLength) { - throw new Error( - "Download truncated (" + - (total / 1048576).toFixed(1) + - "MB of " + - (contentLength / 1048576).toFixed(1) + - "MB)" - ); - } - const combined = new Uint8Array(total); + const total = chunks.reduce((s, c) => s + c.length, 0); + const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); + const minExpectedBytes = + servedLayout === "efi" + ? MIN_EXPECTED_EFI_BYTES + : servedLayout === "iso" + ? MIN_EXPECTED_ISO_BYTES + : MIN_EXPECTED_EFI_BYTES; + if (total < minExpectedBytes) { + throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); + } + if (contentLength > 0 && total !== contentLength) { + throw new Error( + "Download truncated (" + + (total / 1048576).toFixed(1) + + "MB of " + + (contentLength / 1048576).toFixed(1) + + "MB)" + ); + } + const combined = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { combined.set(chunk, offset); @@ -1046,7 +1075,7 @@ async function startDownload(needsPaint) { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; - const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; + const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; console.log("[os] Download complete:", filename, (total / 1048576).toFixed(1) + "MB"); dlFn(filename, combined, { type: "application/octet-stream" }); diff --git a/system/public/aesthetic.computer/disks/prompt.mjs b/system/public/aesthetic.computer/disks/prompt.mjs index 9b5d3e087f..31f6266c2d 100644 --- a/system/public/aesthetic.computer/disks/prompt.mjs +++ b/system/public/aesthetic.computer/disks/prompt.mjs @@ -5191,7 +5191,7 @@ function paint($) { const btnPaddingTop = 3; const btnPaddingRight = 3; - const blankBtnText = "laptop"; + const blankBtnText = "Need a laptop?"; const blankBtnY = btnPaddingTop; const blankBtnX = screen.width - blankBtnText.length * 6 - 12 - btnPaddingRight;