Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: "Need a laptop?" ad on prompt + os pages

- prompt.mjs: button text changed from "laptop" to "Need a laptop?"
- os.mjs: added "Need a laptop?" section with a "blank" button that
jumps to the blank piece

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+185 -156
+184 -155
system/public/aesthetic.computer/disks/os.mjs
··· 1 1 // os, 2026.03.12 2 2 // FedAC OS — build list with commit messages; download your personalized copy. 3 3 4 - const OVEN = "https://oven-edge.aesthetic-computer.workers.dev"; 5 - const OVEN_WS = "wss://oven.aesthetic.computer/ws"; 6 - const OVEN_ORIGIN = "https://oven.aesthetic.computer"; 7 - const ISO_BASE = OVEN + "/os/latest.iso"; 8 - function OVEN_BASE() { return OVEN; } 9 - function RELEASES_URL() { return OVEN + "/os-releases"; } 10 - function OVEN_WS_URL() { return OVEN_WS; } 4 + const OVEN = "https://oven-edge.aesthetic-computer.workers.dev"; 5 + const OVEN_WS = "wss://oven.aesthetic.computer/ws"; 6 + const OVEN_ORIGIN = "https://oven.aesthetic.computer"; 7 + const ISO_BASE = OVEN + "/os/latest.iso"; 8 + function OVEN_BASE() { return OVEN; } 9 + function RELEASES_URL() { return OVEN + "/os-releases"; } 10 + function OVEN_WS_URL() { return OVEN_WS; } 11 11 function templateIsoUrl() { 12 12 const base = ISO_BASE; 13 13 if (variantIdx === 0) return base; ··· 57 57 let hasGit = null; 58 58 let setupBtn = null; // "set up tokens" button shown when missing 59 59 let showTokenHint = false; 60 + 61 + // 💻 "Need a laptop?" ad 62 + let laptopBtn = null; 60 63 61 64 // Build telemetry 62 65 let activeBuild = null; // { id, status, stage, percent, ref, error } ··· 336 339 updateBootBtn(ui); 337 340 updateVariantBtn(ui); 338 341 updateWifiBtn(ui); 342 + laptopBtn = new ui.TextButton("blank", { x: 6, y: 0 }); 339 343 } 340 344 341 345 function updateVariantBtn(ui) { ··· 363 367 wifiBtn = new ui.TextButton(wifiEnabled ? "ON" : "OFF", { x: 6, y: 0 }); 364 368 } 365 369 366 - function timeAgo(ts) { 370 + function timeAgo(ts) { 367 371 if (!ts) return ""; 368 372 const now = Date.now(); 369 373 const then = new Date(ts).getTime(); ··· 377 381 if (day < 30) return day + "d"; 378 382 const mo = Math.floor(day / 30); 379 383 if (mo < 12) return mo + "mo"; 380 - return Math.floor(mo / 12) + "y"; 381 - } 382 - 383 - function buildStatusInfo(build, isCurrent, isDep, darkMode) { 384 - let status = (build?.status || "").toLowerCase(); 385 - if (!status) status = isDep ? "deprecated" : "success"; 386 - 387 - if (status === "done") status = "success"; 388 - if (status === "queued") status = "queue"; 389 - 390 - const dark = { 391 - success: { label: "ok", color: [100, 225, 140] }, 392 - failed: { label: "fail", color: [255, 125, 125] }, 393 - cancelled: { label: "stop", color: [235, 185, 115] }, 394 - running: { label: "run", color: [120, 185, 255] }, 395 - queue: { label: "queue", color: [170, 185, 220] }, 396 - deprecated: { label: "old", color: [170, 130, 130] }, 397 - unknown: { label: "?", color: [150, 155, 170] }, 398 - }; 399 - const light = { 400 - success: { label: "ok", color: [20, 115, 45] }, 401 - failed: { label: "fail", color: [170, 35, 35] }, 402 - cancelled: { label: "stop", color: [140, 95, 15] }, 403 - running: { label: "run", color: [30, 95, 175] }, 404 - queue: { label: "queue", color: [90, 105, 145] }, 405 - deprecated: { label: "old", color: [130, 95, 95] }, 406 - unknown: { label: "?", color: [120, 125, 135] }, 407 - }; 408 - const table = darkMode ? dark : light; 409 - return table[status] || table.unknown; 410 - } 384 + return Math.floor(mo / 12) + "y"; 385 + } 386 + 387 + function buildStatusInfo(build, isCurrent, isDep, darkMode) { 388 + let status = (build?.status || "").toLowerCase(); 389 + if (!status) status = isDep ? "deprecated" : "success"; 390 + 391 + if (status === "done") status = "success"; 392 + if (status === "queued") status = "queue"; 393 + 394 + const dark = { 395 + success: { label: "ok", color: [100, 225, 140] }, 396 + failed: { label: "fail", color: [255, 125, 125] }, 397 + cancelled: { label: "stop", color: [235, 185, 115] }, 398 + running: { label: "run", color: [120, 185, 255] }, 399 + queue: { label: "queue", color: [170, 185, 220] }, 400 + deprecated: { label: "old", color: [170, 130, 130] }, 401 + unknown: { label: "?", color: [150, 155, 170] }, 402 + }; 403 + const light = { 404 + success: { label: "ok", color: [20, 115, 45] }, 405 + failed: { label: "fail", color: [170, 35, 35] }, 406 + cancelled: { label: "stop", color: [140, 95, 15] }, 407 + running: { label: "run", color: [30, 95, 175] }, 408 + queue: { label: "queue", color: [90, 105, 145] }, 409 + deprecated: { label: "old", color: [130, 95, 95] }, 410 + unknown: { label: "?", color: [120, 125, 135] }, 411 + }; 412 + const table = darkMode ? dark : light; 413 + return table[status] || table.unknown; 414 + } 411 415 412 416 function paint($) { 413 417 const { screen, ink, line: drawLine, dark, mask, unmask } = $; ··· 681 685 682 686 y += isMobile ? 14 : 10; 683 687 688 + // --- LAPTOP AD --- 689 + sectionHeader("Need a laptop?", dark ? [18, 14, 24] : [230, 222, 238], dark ? [30, 24, 40] : [215, 208, 225], 120); 690 + ink(...C.instText).write("Run AC OS on any x86 laptop.", { x: pad, y, wrap: wrapW }, undefined, undefined, false, "MatrixChunky8"); 691 + y += matrixH + 4; 692 + if (laptopBtn) { 693 + laptopBtn.reposition({ x: pad, y }); 694 + laptopBtn.paint( 695 + $, 696 + [C.bootBtnBg, C.bootBtnBorder, ...C.current, 200], 697 + [C.bootBtnHoverBg, C.bootBtnHoverBorder, [255, 255, 255], 255], 698 + undefined, 699 + [C.bootBtnBg, C.bootBtnBorder, ...C.current, 230], 700 + ); 701 + y += laptopBtn.height + btnGap; 702 + } 703 + 704 + y += isMobile ? 14 : 10; 705 + 684 706 // --- BUILDS section --- 685 707 sectionHeader("Builds", dark ? [16, 22, 36] : [218, 222, 238], C.secBuildBg, 2000); 686 708 } else if (downloading) { ··· 723 745 const name = rawName.length > maxNameChars ? rawName.slice(0, maxNameChars - 1) + "~" : rawName; 724 746 const hash = (b.git_hash || "?").slice(0, 7); 725 747 const ago = timeAgo(b.build_ts); 726 - const msg = b.commit_msg || ""; 727 - const who = b.handle || ""; 728 - const sizeMB = b.size ? (b.size / 1048576).toFixed(0) + "MB" : ""; 729 - const statusInfo = buildStatusInfo(b, isCurrent, isDep, dark); 748 + const msg = b.commit_msg || ""; 749 + const who = b.handle || ""; 750 + const sizeMB = b.size ? (b.size / 1048576).toFixed(0) + "MB" : ""; 751 + const statusInfo = buildStatusInfo(b, isCurrent, isDep, dark); 730 752 731 753 // Alternating strip background 732 754 const strip = i % 2 === 0 ? stripA : stripB; ··· 763 785 x += (sizeMB.length + 1) * charW; 764 786 } 765 787 766 - if (who && !isNarrow) { 767 - ink(...(isCurrent ? C.handle : isDep ? C.depHandle : C.handleOld)); 768 - $.write("@" + who, { x, y: ry }); 769 - } 770 - 771 - // Status tag on right edge 772 - if (statusInfo) { 773 - const tag = "[" + statusInfo.label + "]"; 774 - const tagX = w - pad - tag.length * charW; 775 - ink(...statusInfo.color); 776 - $.write(tag, { x: tagX, y: ry }); 777 - } 788 + if (who && !isNarrow) { 789 + ink(...(isCurrent ? C.handle : isDep ? C.depHandle : C.handleOld)); 790 + $.write("@" + who, { x, y: ry }); 791 + } 792 + 793 + // Status tag on right edge 794 + if (statusInfo) { 795 + const tag = "[" + statusInfo.label + "]"; 796 + const tagX = w - pad - tag.length * charW; 797 + ink(...statusInfo.color); 798 + $.write(tag, { x: tagX, y: ry }); 799 + } 778 800 779 801 // Strikethrough for deprecated 780 802 if (isDep) { ··· 828 850 } 829 851 } 830 852 831 - function act({ event: e, needsPaint, download }) { 853 + function act({ event: e, needsPaint, download, jump }) { 832 854 dlFn = download; 833 855 834 856 if (e.is("dark-mode") || e.is("light-mode")) { ··· 887 909 }); 888 910 } 889 911 912 + // Laptop ad button: jump to blank 913 + if (laptopBtn) { 914 + laptopBtn.btn.act(e, { 915 + push: () => jump("blank"), 916 + }); 917 + } 918 + 890 919 // Setup tokens button: show hint text 891 920 if (setupBtn && token) { 892 921 setupBtn.btn.act(e, { ··· 910 939 }); 911 940 } 912 941 913 - async function startDownload(needsPaint) { 942 + async function startDownload(needsPaint) { 914 943 downloading = true; 915 944 downloadProgress = 0; 916 945 downloadMB = 0; 917 946 downloadTotalMB = 0; 918 947 const piece = BOOT_PIECES[bootPieceIdx]; 919 - downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; 920 - console.log("[os] Starting download:", osLabel(), "piece:", piece); 921 - needsPaint(); 922 - 923 - try { 924 - const query = 925 - "?piece=" + encodeURIComponent(piece) + 926 - "&wifi=" + (wifiEnabled ? "1" : "0") + 927 - "&layout=efi&strict=1&cb=" + Date.now() + 928 - (variantIdx === 1 ? "&variant=cl" : ""); 929 - // Strict EFI downloads must come from oven origin. 930 - // The edge worker can return stale/truncated artifacts without layout headers. 931 - const isoCandidates = [OVEN_ORIGIN + "/os-image" + query]; 932 - const MIN_EXPECTED_EFI_BYTES = 300 * 1024 * 1024; 933 - const MIN_EXPECTED_ISO_BYTES = 200 * 1024 * 1024; 934 - 935 - let res = null; 936 - let usedUrl = ""; 937 - let lastErr = ""; 938 - for (const url of isoCandidates) { 939 - console.log("[os] Fetching:", url); 940 - let attempt; 941 - try { 942 - attempt = await fetch(url, { 943 - headers: { Authorization: "Bearer " + token }, 944 - }); 945 - } catch (err) { 946 - lastErr = err?.message || String(err); 947 - continue; 948 - } 949 - 950 - if (!attempt.ok) { 951 - const err = await attempt.json().catch(() => ({})); 952 - lastErr = err.error || ("Download failed: " + attempt.status); 953 - continue; 954 - } 955 - 956 - const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); 957 - if (!servedLayout) { 958 - console.warn("[os] Missing x-ac-os-layout header from", url); 959 - try { attempt.body?.cancel(); } catch (_) {} 960 - lastErr = "Server did not report image layout (x-ac-os-layout missing)"; 961 - continue; 962 - } 963 - if (servedLayout && servedLayout !== "efi") { 964 - console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); 965 - try { attempt.body?.cancel(); } catch (_) {} 966 - lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; 967 - continue; 968 - } 969 - 970 - const len = parseInt(attempt.headers.get("content-length") || "0"); 971 - const minExpectedBytes = 972 - servedLayout === "efi" 973 - ? MIN_EXPECTED_EFI_BYTES 974 - : servedLayout === "iso" 975 - ? MIN_EXPECTED_ISO_BYTES 976 - : MIN_EXPECTED_EFI_BYTES; 977 - if (len > 0 && len < minExpectedBytes) { 978 - console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url, "layout:", servedLayout || "?"); 979 - try { attempt.body?.cancel(); } catch (_) {} 980 - lastErr = "Received suspiciously small image (" + len + " bytes)"; 981 - continue; 982 - } 983 - 984 - res = attempt; 985 - usedUrl = url; 986 - break; 987 - } 988 - 989 - if (!res) { 990 - throw new Error(lastErr || "Download failed"); 991 - } 992 - console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); 993 - 994 - const contentLength = parseInt(res.headers.get("content-length") || "0"); 948 + downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; 949 + console.log("[os] Starting download:", osLabel(), "piece:", piece); 950 + needsPaint(); 951 + 952 + try { 953 + const query = 954 + "?piece=" + encodeURIComponent(piece) + 955 + "&wifi=" + (wifiEnabled ? "1" : "0") + 956 + "&layout=efi&strict=1&cb=" + Date.now() + 957 + (variantIdx === 1 ? "&variant=cl" : ""); 958 + // Strict EFI downloads must come from oven origin. 959 + // The edge worker can return stale/truncated artifacts without layout headers. 960 + const isoCandidates = [OVEN_ORIGIN + "/os-image" + query]; 961 + const MIN_EXPECTED_EFI_BYTES = 300 * 1024 * 1024; 962 + const MIN_EXPECTED_ISO_BYTES = 200 * 1024 * 1024; 963 + 964 + let res = null; 965 + let usedUrl = ""; 966 + let lastErr = ""; 967 + for (const url of isoCandidates) { 968 + console.log("[os] Fetching:", url); 969 + let attempt; 970 + try { 971 + attempt = await fetch(url, { 972 + headers: { Authorization: "Bearer " + token }, 973 + }); 974 + } catch (err) { 975 + lastErr = err?.message || String(err); 976 + continue; 977 + } 978 + 979 + if (!attempt.ok) { 980 + const err = await attempt.json().catch(() => ({})); 981 + lastErr = err.error || ("Download failed: " + attempt.status); 982 + continue; 983 + } 984 + 985 + const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); 986 + if (!servedLayout) { 987 + console.warn("[os] Missing x-ac-os-layout header from", url); 988 + try { attempt.body?.cancel(); } catch (_) {} 989 + lastErr = "Server did not report image layout (x-ac-os-layout missing)"; 990 + continue; 991 + } 992 + if (servedLayout && servedLayout !== "efi") { 993 + console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); 994 + try { attempt.body?.cancel(); } catch (_) {} 995 + lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; 996 + continue; 997 + } 998 + 999 + const len = parseInt(attempt.headers.get("content-length") || "0"); 1000 + const minExpectedBytes = 1001 + servedLayout === "efi" 1002 + ? MIN_EXPECTED_EFI_BYTES 1003 + : servedLayout === "iso" 1004 + ? MIN_EXPECTED_ISO_BYTES 1005 + : MIN_EXPECTED_EFI_BYTES; 1006 + if (len > 0 && len < minExpectedBytes) { 1007 + console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url, "layout:", servedLayout || "?"); 1008 + try { attempt.body?.cancel(); } catch (_) {} 1009 + lastErr = "Received suspiciously small image (" + len + " bytes)"; 1010 + continue; 1011 + } 1012 + 1013 + res = attempt; 1014 + usedUrl = url; 1015 + break; 1016 + } 1017 + 1018 + if (!res) { 1019 + throw new Error(lastErr || "Download failed"); 1020 + } 1021 + console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); 1022 + 1023 + const contentLength = parseInt(res.headers.get("content-length") || "0"); 995 1024 downloadTotalMB = contentLength / 1048576; 996 1025 downloadStatus = "downloading..."; 997 1026 needsPaint(); ··· 1013 1042 downloadStatus = "preparing file..."; 1014 1043 needsPaint(); 1015 1044 1016 - const total = chunks.reduce((s, c) => s + c.length, 0); 1017 - const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); 1018 - const minExpectedBytes = 1019 - servedLayout === "efi" 1020 - ? MIN_EXPECTED_EFI_BYTES 1021 - : servedLayout === "iso" 1022 - ? MIN_EXPECTED_ISO_BYTES 1023 - : MIN_EXPECTED_EFI_BYTES; 1024 - if (total < minExpectedBytes) { 1025 - throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); 1026 - } 1027 - if (contentLength > 0 && total !== contentLength) { 1028 - throw new Error( 1029 - "Download truncated (" + 1030 - (total / 1048576).toFixed(1) + 1031 - "MB of " + 1032 - (contentLength / 1048576).toFixed(1) + 1033 - "MB)" 1034 - ); 1035 - } 1036 - const combined = new Uint8Array(total); 1045 + const total = chunks.reduce((s, c) => s + c.length, 0); 1046 + const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); 1047 + const minExpectedBytes = 1048 + servedLayout === "efi" 1049 + ? MIN_EXPECTED_EFI_BYTES 1050 + : servedLayout === "iso" 1051 + ? MIN_EXPECTED_ISO_BYTES 1052 + : MIN_EXPECTED_EFI_BYTES; 1053 + if (total < minExpectedBytes) { 1054 + throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); 1055 + } 1056 + if (contentLength > 0 && total !== contentLength) { 1057 + throw new Error( 1058 + "Download truncated (" + 1059 + (total / 1048576).toFixed(1) + 1060 + "MB of " + 1061 + (contentLength / 1048576).toFixed(1) + 1062 + "MB)" 1063 + ); 1064 + } 1065 + const combined = new Uint8Array(total); 1037 1066 let offset = 0; 1038 1067 for (const chunk of chunks) { 1039 1068 combined.set(chunk, offset); ··· 1046 1075 const d = new Date(); 1047 1076 const p = (n) => String(n).padStart(2, "0"); 1048 1077 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 1049 - const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; 1078 + const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; 1050 1079 1051 1080 console.log("[os] Download complete:", filename, (total / 1048576).toFixed(1) + "MB"); 1052 1081 dlFn(filename, combined, { type: "application/octet-stream" });
+1 -1
system/public/aesthetic.computer/disks/prompt.mjs
··· 5191 5191 const btnPaddingTop = 3; 5192 5192 const btnPaddingRight = 3; 5193 5193 5194 - const blankBtnText = "laptop"; 5194 + const blankBtnText = "Need a laptop?"; 5195 5195 const blankBtnY = btnPaddingTop; 5196 5196 const blankBtnX = screen.width - blankBtnText.length * 6 - 12 - btnPaddingRight; 5197 5197