Monorepo for Aesthetic.Computer
aesthetic.computer
1// os.mjs — AC Native OS management piece
2// Shows current version, checks for updates, downloads + flashes + reboots.
3// Jumped to from prompt.mjs via "os" command or from notepat OS button.
4
5const OS_BASE_URL = "https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os/";
6const OS_VERSION_URL = OS_BASE_URL + "native-notepat-latest.version";
7const OS_VMLINUZ_URL = OS_BASE_URL + "native-notepat-latest.vmlinuz";
8const OS_INITRAMFS_URL = OS_BASE_URL + "native-notepat-latest.initramfs.cpio.gz";
9let remoteSize = 0; // parsed from version file (line 2)
10
11// Kernel no longer embeds initramfs (Phase 2 — loaded externally via EFI stub
12// `initrd=\initramfs.cpio.gz` from the ESP root). OTA must download BOTH the
13// kernel and the initramfs and flash them atomically, otherwise the device
14// boots a new kernel against a stale initramfs and code-path drift follows.
15let initramfsDownloaded = false;
16
17// States: idle | checking | up-to-date | available
18// | downloading (kernel) | downloading-initramfs | flashing
19// | confirm-reboot | shutting-down | error
20// | devices | clone-confirm | cloning
21let state = "idle";
22let currentVersion = "";
23let remoteVersion = "";
24let progress = 0;
25let errorMsg = "";
26let fetchPending = false;
27let checkFrame = 0;
28let flashTargetIdx = 0;
29let frame = 0;
30let shutdownFrame = 0; // frame counter for shutdown animation
31let flashedMB = 0; // verified MB for display during confirm/shutdown
32
33// Telemetry lines that scroll by during flash
34const telemetry = [];
35let telemetryScroll = 0;
36
37// Device manager state
38let deviceIdx = 0;
39let lastTargetCount = -1; // track hot-plug changes
40let cloneTarget = null;
41
42function addTelemetry(msg) {
43 telemetry.push({ text: msg, frame, y: 0 });
44 if (telemetry.length > 50) telemetry.shift();
45}
46
47function boot({ system }) {
48 currentVersion = system?.version || "unknown";
49 // Default flash target: prefer non-boot device (e.g., NVMe when booting from USB)
50 const targets = system?.flashTargets || [];
51 const bootDev = system?.bootDevice;
52 const nonBootIdx = targets.findIndex(t => t.device !== bootDev);
53 if (nonBootIdx >= 0) flashTargetIdx = nonBootIdx;
54 // Auto-check on boot if online
55 if (system?.fetchPending === false) {
56 state = "checking";
57 fetchPending = true;
58 checkFrame = 0;
59 system?.fetch?.(OS_VERSION_URL);
60 }
61}
62
63function act({ event: e, sound, system }) {
64 if (!e.is("keyboard:down")) return;
65
66 // Escape goes back to prompt (not during flash or shutdown)
67 if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) {
68 if (state !== "flashing" && state !== "shutting-down" && state !== "downloading") {
69 system?.jump?.("prompt");
70 return;
71 }
72 }
73
74 // Reboot confirmation: y = reboot, n = back to prompt
75 if (state === "confirm-reboot") {
76 if (e.is("keyboard:down:y") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) {
77 // Block reboot if USB live media still present on a cross-device flash —
78 // firmware will boot the stale USB kernel instead of the freshly-flashed one
79 const targets = system?.flashTargets || [];
80 const tgt = targets[flashTargetIdx];
81 const flashedToBoot = !tgt || tgt.device === system?.bootDevice;
82 if (!flashedToBoot) {
83 const usbStillAttached = targets.some(t => t.removable && t.device === system?.bootDevice);
84 if (usbStillAttached) {
85 sound?.synth({ type: "square", tone: 180, duration: 0.14, volume: 0.12, attack: 0.003, decay: 0.12 });
86 return;
87 }
88 }
89 state = "shutting-down";
90 shutdownFrame = 0;
91 sound?.synth({ type: "triangle", tone: 784, duration: 0.1, volume: 0.15, attack: 0.003, decay: 0.08 });
92 return;
93 }
94 if (e.is("keyboard:down:n") || e.is("keyboard:down:escape")) {
95 system?.jump?.("prompt");
96 return;
97 }
98 return;
99 }
100
101 // Touch-like key shortcuts for available state
102 if (state === "available") {
103 if (e.is("keyboard:down:y") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) {
104 const targets = system?.flashTargets || [];
105 const tgt = targets[flashTargetIdx];
106 const device = tgt?.device || undefined;
107 globalThis.__osFlashDevice = device;
108 state = "downloading";
109 progress = 0;
110 telemetry.length = 0;
111 addTelemetry("fetching " + OS_VMLINUZ_URL.split("/").pop());
112 system?.fetchBinary?.(OS_VMLINUZ_URL, "/tmp/vmlinuz.new", (remoteSize || 93_000_000));
113 return;
114 }
115 if (e.is("keyboard:down:n")) {
116 system?.jump?.("prompt");
117 return;
118 }
119 if (e.is("keyboard:down:tab")) {
120 const targets = system?.flashTargets || [];
121 flashTargetIdx = (flashTargetIdx + 1) % Math.max(1, targets.length);
122 sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.12, attack: 0.002, decay: 0.03 });
123 return;
124 }
125 }
126
127 // 'd' to enter device manager from idle/up-to-date/available/error
128 if (state === "idle" || state === "up-to-date" || state === "available" || state === "error") {
129 if (e.is("keyboard:down:d")) {
130 state = "devices";
131 deviceIdx = 0;
132 sound?.synth({ type: "sine", tone: 523, duration: 0.05, volume: 0.1, attack: 0.002, decay: 0.04 });
133 return;
134 }
135 // 'f' enters the firmware panel — gated on system.firmware.available so
136 // it's a no-op on machines without MrChromebox/coreboot + SPI access.
137 if (e.is("keyboard:down:f") && system?.firmware?.available) {
138 state = "firmware";
139 sound?.synth({ type: "sine", tone: 523, duration: 0.05, volume: 0.1, attack: 0.002, decay: 0.04 });
140 return;
141 }
142 }
143
144 // Firmware panel controls
145 if (state === "firmware") {
146 if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) {
147 state = "idle";
148 return;
149 }
150 // Block further input while the install thread is mid-flight.
151 if (system?.firmware?.pending) return;
152 if (e.is("keyboard:down:y") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) {
153 system?.firmware?.install?.("install");
154 sound?.synth({ type: "triangle", tone: 784, duration: 0.1, volume: 0.12, attack: 0.003, decay: 0.08 });
155 return;
156 }
157 if (e.is("keyboard:down:t")) {
158 system?.firmware?.install?.("dry-run");
159 sound?.synth({ type: "sine", tone: 659, duration: 0.06, volume: 0.1, attack: 0.002, decay: 0.05 });
160 return;
161 }
162 if (e.is("keyboard:down:r")) {
163 system?.firmware?.install?.("restore");
164 sound?.synth({ type: "triangle", tone: 440, duration: 0.1, volume: 0.12, attack: 0.003, decay: 0.08 });
165 return;
166 }
167 return;
168 }
169
170 // Device manager controls
171 if (state === "devices") {
172 const targets = system?.flashTargets || [];
173 if (e.is("keyboard:down:tab") || e.is("keyboard:down:arrowdown")) {
174 deviceIdx = (deviceIdx + 1) % Math.max(1, targets.length);
175 sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.1, attack: 0.002, decay: 0.03 });
176 return;
177 }
178 if (e.is("keyboard:down:arrowup")) {
179 deviceIdx = (deviceIdx - 1 + targets.length) % Math.max(1, targets.length);
180 sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.1, attack: 0.002, decay: 0.03 });
181 return;
182 }
183 // 'c' to clone current OS to selected device
184 if (e.is("keyboard:down:c")) {
185 const tgt = targets[deviceIdx];
186 if (tgt && tgt.device !== system?.bootDevice) {
187 cloneTarget = tgt;
188 state = "clone-confirm";
189 sound?.synth({ type: "triangle", tone: 660, duration: 0.08, volume: 0.12, attack: 0.003, decay: 0.06 });
190 } else {
191 sound?.synth({ type: "square", tone: 220, duration: 0.1, volume: 0.08, attack: 0.005, decay: 0.08 });
192 }
193 return;
194 }
195 // 'u' to update selected device from CDN
196 if (e.is("keyboard:down:u")) {
197 const tgt = targets[deviceIdx];
198 if (tgt) {
199 flashTargetIdx = deviceIdx;
200 state = "checking";
201 fetchPending = true;
202 checkFrame = frame;
203 system?.fetch?.(OS_VERSION_URL);
204 }
205 return;
206 }
207 if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) {
208 state = "idle";
209 return;
210 }
211 return;
212 }
213
214 // Clone confirmation
215 if (state === "clone-confirm") {
216 if (e.is("keyboard:down:y") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) {
217 state = "cloning";
218 telemetry.length = 0;
219 addTelemetry("cloning to " + cloneTarget.device);
220 // Clone = flash the currently running kernel to the target device
221 // The running kernel is at /mnt/EFI/BOOT/BOOTX64.EFI or KERNEL.EFI
222 const bootKernel = "/mnt/EFI/BOOT/BOOTX64.EFI";
223 globalThis.__osFlashDevice = cloneTarget.device;
224 system?.flashUpdate?.(bootKernel, cloneTarget.device);
225 sound?.synth({ type: "triangle", tone: 784, duration: 0.1, volume: 0.12, attack: 0.003, decay: 0.08 });
226 return;
227 }
228 if (e.is("keyboard:down:n") || e.is("keyboard:down:escape")) {
229 state = "devices";
230 return;
231 }
232 return;
233 }
234
235 // Tap to retry on error or re-check when up-to-date/idle
236 if (state === "error" || state === "up-to-date" || state === "idle") {
237 if (e.is("keyboard:down:enter") || e.is("keyboard:down:return") || e.is("keyboard:down:space")) {
238 if (!system?.fetchPending) {
239 state = "checking";
240 fetchPending = true;
241 checkFrame = frame;
242 system?.fetch?.(OS_VERSION_URL);
243 }
244 }
245 }
246}
247
248function paint({ wipe, ink, box, line, write, screen, system, wifi }) {
249 frame++;
250 const T = __theme.update();
251 const w = screen.width, h = screen.height;
252 const pad = 10;
253 const font = "font_1";
254
255 // === Shutdown animation (full-screen takeover) ===
256 if (state === "shutting-down") {
257 shutdownFrame++;
258 const t = shutdownFrame / 120; // 2 second animation
259
260 // Background: dark blue fading to black
261 const bg = Math.max(0, Math.floor(20 * (1 - t)));
262 wipe(0, bg, Math.floor(bg * 1.5));
263
264 // Scrolling telemetry lines flying upward
265 for (let i = 0; i < telemetry.length; i++) {
266 const entry = telemetry[i];
267 const baseY = h - (shutdownFrame - i * 3) * 2;
268 if (baseY < -10 || baseY > h + 10) continue;
269 const alpha = Math.max(0, Math.min(255, Math.floor(200 * (1 - t))));
270 const green = 80 + (i * 37 % 120);
271 ink(60, green, 100, alpha);
272 write(entry.text, { x: pad + (i % 3) * 2, y: baseY, size: 1, font });
273 }
274
275 // Central message
276 if (t < 0.8) {
277 const pulse = Math.floor(180 + 75 * Math.sin(shutdownFrame * 0.2));
278 ink(pulse, 255, pulse, Math.floor(255 * (1 - t / 0.8)));
279 write("launching new os", { x: pad, y: h / 2 - 20, size: 2, font: "matrix" });
280
281 // Build name
282 const buildName = remoteVersion.split(" ")[0] || "update";
283 ink(255, 200, 60, Math.floor(200 * (1 - t / 0.8)));
284 write(buildName, { x: pad, y: h / 2 + 4, size: 1, font });
285 }
286
287 // Verified size
288 if (t < 0.6) {
289 ink(80, 200, 120, Math.floor(180 * (1 - t / 0.6)));
290 write(`verified ${flashedMB}MB`, { x: pad, y: h / 2 + 20, size: 1, font });
291 }
292
293 // Progress bar shrinking to nothing
294 const barW = Math.max(0, Math.floor((w - pad * 2) * (1 - t)));
295 if (barW > 0) {
296 ink(60, 180, 100, Math.floor(200 * (1 - t)));
297 box(pad, h - 8, barW, 4, true);
298 }
299
300 // Trigger reboot after animation
301 if (shutdownFrame >= 150) { // 2.5 seconds
302 system?.reboot?.();
303 }
304 return;
305 }
306
307 // === Normal UI ===
308 wipe(T.bg[0], T.bg[1], T.bg[2]);
309
310 // Responsive: use half-width columns on wide screens
311 const wide = w > 260;
312 const colW = wide ? Math.floor((w - pad * 3) / 2) : w - pad * 2;
313 const col2X = wide ? pad + colW + pad : pad;
314
315 // Title
316 ink(T.fg, T.fg + 10, T.fg);
317 write("ac/native", { x: pad, y: 10, size: 2, font: "matrix" });
318
319 // Connection status
320 if (!wifi?.connected) {
321 ink(T.err[0], T.err[1], T.err[2]);
322 write("offline", { x: pad, y: 34, size: 1, font });
323 ink(T.fgMute, T.fgMute - 10, T.fgMute - 10);
324 write("connect wifi first", { x: pad, y: 46, size: 1, font });
325 } else {
326 ink(T.fgMute, T.fgMute + 10, T.fgMute);
327 write("current", { x: pad, y: 34, size: 1, font });
328 ink(T.fgDim, T.fgDim, T.fgDim);
329 const maxChars = Math.floor(colW / 6);
330 write(currentVersion.slice(0, maxChars), { x: pad, y: 46, size: 1, font });
331 }
332
333 // Machine hint
334 {
335 const sX = wide ? col2X : pad;
336 const sY = wide ? 34 : 58;
337 ink(T.fgMute, T.fgMute + 5, T.fgMute + 10);
338 write("machine: hw + sw info", { x: sX, y: sY, size: 1, font });
339 }
340
341 const stateY = 66;
342
343 if (state === "checking") {
344 ink(T.warn[0], T.warn[1], T.warn[2]);
345 const dots = ".".repeat((Math.floor(frame / 20) % 3) + 1);
346 write("checking" + dots, { x: pad, y: stateY, size: 1, font });
347
348 } else if (state === "up-to-date") {
349 ink(T.ok[0], T.ok[1], T.ok[2]);
350 write("up to date!", { x: pad, y: stateY, size: 1, font });
351 ink(T.fgMute, T.fgMute + 10, T.fgMute);
352 write(remoteVersion, { x: pad, y: stateY + 14, size: 1, font });
353 ink(T.fgMute, T.fgMute, T.fgMute + 10);
354 write("enter: recheck esc: back", { x: pad, y: stateY + 30, size: 1, font });
355
356 } else if (state === "available") {
357 ink(T.fgMute, T.fgMute + 10, T.fgMute);
358 write("available", { x: pad, y: stateY, size: 1, font });
359 ink(T.warn[0], T.warn[1], T.warn[2]);
360 write(remoteVersion, { x: pad, y: stateY + 14, size: 1, font });
361
362 // Flash target selector
363 const targets = system?.flashTargets || [];
364 if (targets.length > 0) {
365 if (flashTargetIdx >= targets.length) flashTargetIdx = 0;
366 const tgt = targets[flashTargetIdx];
367 const tgtLabel = (tgt?.label || "?") + " (" + (tgt?.device || "?") + ")";
368 const isBoot = tgt?.device === system?.bootDevice;
369 ink(80, 100, 120);
370 write("target:", { x: pad, y: stateY + 30, size: 1, font });
371 ink(isBoot ? 80 : 200, isBoot ? 200 : 200, isBoot ? 255 : 80);
372 write(tgtLabel, { x: pad + 48, y: stateY + 30, size: 1, font });
373 if (isBoot) {
374 ink(60, 140, 200);
375 write("(current boot)", { x: pad, y: stateY + 42, size: 1, font });
376 }
377 if (targets.length > 1) {
378 ink(80, 80, 100);
379 write("tab: next target", { x: pad, y: stateY + 56, size: 1, font });
380 }
381 }
382
383 // Install confirmation prompt
384 const hintY = stateY + (targets.length > 0 ? 72 : 34);
385 const pulse = Math.floor(180 + 75 * Math.sin(frame * 0.08));
386 ink(pulse, 255, pulse);
387 write("install? y/n", { x: pad, y: hintY, size: 1, font });
388 ink(80, 80, 100);
389 write("esc: back", { x: pad, y: hintY + 14, size: 1, font });
390
391 } else if (state === "downloading") {
392 ink(120, 140, 120);
393 const dots = ".".repeat((Math.floor(frame / 15) % 3) + 1);
394 write("downloading" + dots, { x: pad, y: stateY, size: 1, font });
395
396 // File info
397 const expectedMB = ((remoteSize || 93_000_000) / 1048576).toFixed(0);
398 const dlMB = ((progress || 0) * (remoteSize || 93_000_000) / 1048576).toFixed(1);
399 ink(80, 100, 80);
400 write(`${dlMB} / ${expectedMB} MB`, { x: pad, y: stateY + 14, size: 1, font });
401
402 // Progress bar
403 const barW = w - pad * 2, barH = 8, barY = stateY + 30;
404 ink(30, 40, 50);
405 box(pad, barY, barW, barH, true);
406 ink(60, 180, 100);
407 box(pad, barY, Math.round(barW * (progress || 0)), barH, true);
408 ink(160);
409 write(Math.round((progress || 0) * 100) + "%", { x: pad, y: barY + 12, size: 1, font });
410
411 // Target info
412 ink(60, 70, 80);
413 write("-> " + (globalThis.__osFlashDevice || system?.bootDevice || "?"), { x: pad, y: barY + 26, size: 1, font });
414
415 } else if (state === "flashing") {
416 const phase = system?.flashPhase ?? 0;
417 const phaseIcons = ["...", ">>>", "~~~", "???", "!!!"];
418 const phaseNames = ["preparing", "writing EFI", "syncing to disk", "verifying", "complete"];
419 const phaseText = phaseNames[phase] || "preparing";
420 const icon = phaseIcons[phase] || "...";
421
422 // Phase indicator with animation
423 const dots = ".".repeat((Math.floor(frame / 10) % 3) + 1);
424 ink(...(phase === 3 ? [100, 200, 255] : phase === 4 ? [80, 255, 120] : [255, 160, 60]));
425 write(`${icon} ${phaseText}${phase < 4 ? dots : ""}`, { x: pad, y: stateY, size: 1, font });
426
427 // Target device
428 ink(100);
429 write("-> " + (globalThis.__osFlashDevice || system?.bootDevice || "?"), { x: pad, y: stateY + 14, size: 1, font });
430
431 // Phase progress visualization
432 const phases = ["prepare", "write", "sync", "verify"];
433 let px = pad;
434 for (let i = 0; i < phases.length; i++) {
435 const active = i === phase || (phase === 4 && i === 3);
436 const done = i < phase || phase === 4;
437 ink(done ? 60 : 30, done ? 140 : 40, done ? 80 : 50);
438 const pw = Math.floor((w - pad * 2 - 12) / 4);
439 box(px, stateY + 30, pw, 6, true);
440 if (active && !done) {
441 // Animated fill
442 const fill = Math.floor(pw * ((frame % 60) / 60));
443 ink(255, 200, 60);
444 box(px, stateY + 30, fill, 6, true);
445 }
446 px += pw + 4;
447 }
448
449 // Warning
450 ink(140);
451 write("do not power off", { x: pad, y: stateY + 44, size: 1, font });
452
453 // Scrolling telemetry at bottom
454 const telY = stateY + 60;
455 const maxLines = Math.floor((h - telY - 14) / 10);
456 const startIdx = Math.max(0, telemetry.length - maxLines);
457 for (let i = startIdx; i < telemetry.length; i++) {
458 const lineY = telY + (i - startIdx) * 10;
459 const age = frame - telemetry[i].frame;
460 const brightness = Math.max(40, Math.min(120, 120 - age));
461 ink(brightness, Math.floor(brightness * 1.2), brightness);
462 write(telemetry[i].text, { x: pad, y: lineY, size: 1, font });
463 }
464
465 } else if (state === "confirm-reboot") {
466 // Flash complete — ask user to reboot
467 const mb = flashedMB;
468 ink(80, 255, 120);
469 write("update installed!", { x: pad, y: stateY, size: 2, font: "matrix" });
470
471 ink(120, 200, 140);
472 write(`verified ${mb}MB written`, { x: pad, y: stateY + 24, size: 1, font });
473
474 ink(200, 180, 100);
475 write(remoteVersion, { x: pad, y: stateY + 38, size: 1, font });
476
477 // Flash diagnostics
478 const dst = system?.flashDst || "?";
479 const sameDev = system?.flashSameDevice ? "same-dev" : "cross-dev";
480 ink(80, 80, 100);
481 write(`${dst} (${sameDev})`, { x: pad, y: stateY + 50, size: 1, font });
482
483 // Reboot prompt — pulsing
484 const pulse = Math.floor(200 + 55 * Math.sin(frame * 0.1));
485 ink(pulse, pulse, 255);
486 write("reboot now?", { x: pad, y: stateY + 58, size: 2, font: "matrix" });
487
488 // Warn if flashed to non-boot device (e.g. USB→NVMe: remove USB first)
489 const targets = system?.flashTargets || [];
490 const tgt = targets[flashTargetIdx];
491 const flashedToBoot = !tgt || tgt.device === system?.bootDevice;
492 // USB still attached? flashTargets re-enumerates each frame so removal is live
493 const usbStillAttached = !flashedToBoot &&
494 targets.some(t => t.removable && t.device === system?.bootDevice);
495 const rebootBlocked = usbStillAttached;
496 if (!flashedToBoot) {
497 if (usbStillAttached) {
498 // Blink warning until USB is removed
499 const blink = Math.floor(frame / 12) % 2 === 0;
500 if (blink) {
501 ink(255, 180, 60);
502 write("⚠ unplug USB before rebooting", { x: pad, y: stateY + 80, size: 1, font });
503 } else {
504 ink(255, 60, 60);
505 write(" reboot blocked ", { x: pad, y: stateY + 80, size: 1, font });
506 }
507 } else {
508 // Live media removed — clear the warning, show ready state
509 ink(100, 220, 120);
510 write("✓ USB removed, safe to reboot", { x: pad, y: stateY + 80, size: 1, font });
511 }
512 }
513
514 const hintY = flashedToBoot ? stateY + 80 : stateY + 94;
515 if (rebootBlocked) {
516 ink(120, 80, 80);
517 write("y: disabled (remove USB)", { x: pad, y: hintY, size: 1, font });
518 } else {
519 ink(60, 200, 80);
520 write("y: reboot to new os", { x: pad, y: hintY, size: 1, font });
521 }
522 ink(140, 100, 80);
523 write("n: back to prompt", { x: pad, y: hintY + 14, size: 1, font });
524
525 // Scrolling telemetry in background
526 const telY = hintY + 32;
527 const maxLines = Math.floor((h - telY - 14) / 10);
528 const startIdx = Math.max(0, telemetry.length - maxLines);
529 for (let i = startIdx; i < telemetry.length; i++) {
530 const lineY = telY + (i - startIdx) * 10;
531 ink(40, 50, 45);
532 write(telemetry[i].text, { x: pad, y: lineY, size: 1, font });
533 }
534
535 } else if (state === "error") {
536 ink(T.err[0], T.err[1], T.err[2]);
537 write(("error: " + errorMsg).slice(0, Math.floor((w - pad * 2) / 6)), { x: pad, y: stateY, size: 1, font });
538 ink(T.fgMute);
539 write("enter: retry esc: back", { x: pad, y: stateY + 14, size: 1, font });
540
541 } else if (state === "devices") {
542 // === Device manager ===
543 const targets = system?.flashTargets || [];
544 const bootDev = system?.bootDevice;
545
546 // Hot-plug detection: play sound on change
547 if (targets.length !== lastTargetCount && lastTargetCount >= 0) {
548 // Would play sound here but we don't have sound ref in paint
549 }
550 lastTargetCount = targets.length;
551
552 ink(T.fg, T.fg + 10, T.fg);
553 write("devices", { x: pad, y: stateY, size: 2, font: "matrix" });
554
555 if (targets.length === 0) {
556 ink(T.fgMute);
557 write("no devices found", { x: pad, y: stateY + 24, size: 1, font });
558 } else {
559 if (deviceIdx >= targets.length) deviceIdx = 0;
560 const rowH = 20;
561 for (let i = 0; i < targets.length; i++) {
562 const tgt = targets[i];
563 const ry = stateY + 24 + i * rowH;
564 const isBoot = tgt.device === bootDev;
565 const selected = i === deviceIdx;
566
567 // Selection indicator
568 if (selected) {
569 ink(40, 60, 80);
570 box(pad - 2, ry - 2, w - pad * 2 + 4, rowH - 2, true);
571 }
572
573 // Device label + path
574 ink(selected ? 255 : T.fgMute, selected ? 255 : T.fgMute, selected ? 255 : T.fgMute);
575 write((selected ? "> " : " ") + (tgt.label || "?"), { x: pad, y: ry, size: 1, font });
576 ink(80, 80, 100);
577 write(tgt.device, { x: pad + 100, y: ry, size: 1, font });
578
579 // Boot indicator
580 if (isBoot) {
581 ink(60, 200, 120);
582 write("boot", { x: w - pad - 30, y: ry, size: 1, font });
583 } else if (tgt.removable) {
584 ink(100, 140, 200);
585 write("usb", { x: w - pad - 24, y: ry, size: 1, font });
586 }
587 }
588
589 // Actions for selected device
590 const actY = stateY + 24 + targets.length * rowH + 8;
591 const sel = targets[deviceIdx];
592 const isBootDev = sel?.device === bootDev;
593
594 ink(80, 80, 100);
595 write("tab/arrows: select", { x: pad, y: actY, size: 1, font });
596
597 if (!isBootDev) {
598 ink(100, 200, 140);
599 write("c: clone current os", { x: pad, y: actY + 14, size: 1, font });
600 }
601 ink(100, 160, 220);
602 write("u: update from cloud", { x: pad, y: actY + 28, size: 1, font });
603 }
604
605 } else if (state === "clone-confirm") {
606 ink(T.warn[0], T.warn[1], T.warn[2]);
607 write("clone os?", { x: pad, y: stateY, size: 2, font: "matrix" });
608
609 ink(T.fgMute + 20, T.fgMute + 20, T.fgMute);
610 write("from: " + (system?.bootDevice || "?"), { x: pad, y: stateY + 24, size: 1, font });
611 write(" to: " + (cloneTarget?.label || "?") + " (" + (cloneTarget?.device || "?") + ")", { x: pad, y: stateY + 38, size: 1, font });
612
613 ink(T.fg);
614 write(currentVersion, { x: pad, y: stateY + 56, size: 1, font });
615
616 ink(255, 180, 60);
617 write("this will overwrite the target!", { x: pad, y: stateY + 74, size: 1, font });
618
619 const pulse = Math.floor(200 + 55 * Math.sin(frame * 0.1));
620 ink(pulse, 255, pulse);
621 write("y: clone n: cancel", { x: pad, y: stateY + 92, size: 1, font });
622
623 } else if (state === "cloning") {
624 // Reuse flashing UI
625 const phase = system?.flashPhase ?? 0;
626 const phaseNames = ["preparing", "writing EFI", "syncing", "verifying", "complete"];
627 const dots = ".".repeat((Math.floor(frame / 10) % 3) + 1);
628 ink(255, 160, 60);
629 write("cloning" + (phase < 4 ? dots : "!"), { x: pad, y: stateY, size: 1, font });
630 ink(120);
631 write(phaseNames[phase] || "...", { x: pad, y: stateY + 14, size: 1, font });
632 ink(80);
633 write("-> " + (cloneTarget?.device || "?"), { x: pad, y: stateY + 28, size: 1, font });
634 ink(140);
635 write("do not power off", { x: pad, y: stateY + 44, size: 1, font });
636
637 } else if (state === "firmware") {
638 // ── Firmware update panel ──
639 // Only entered from idle when system.firmware.available is true, so by
640 // definition we can surface "update available" without a coreboot check
641 // here. We still sanity-gate the `y` action on .available in case of
642 // hot state (rare: SPI driver unbind, MTD removed).
643 const fw = system?.firmware || {};
644 ink(T.fg, T.fg + 10, T.fg);
645 write("firmware", { x: pad, y: stateY, size: 2, font: "matrix" });
646 ink(T.fgMute);
647 write("board: " + (fw.board || "?"), { x: pad, y: stateY + 24, size: 1, font });
648 write("vendor: " + (fw.biosVendor || "?").slice(0, 32), { x: pad, y: stateY + 38, size: 1, font });
649 write("current: " + (fw.biosVersion || "?").slice(0, 32), { x: pad, y: stateY + 52, size: 1, font });
650
651 if (fw.pending) {
652 ink(T.warn[0], T.warn[1], T.warn[2]);
653 const dots = ".".repeat((Math.floor(frame / 12) % 3) + 1);
654 write("flashing firmware" + dots, { x: pad, y: stateY + 74, size: 1, font });
655 ink(255, 80, 80);
656 write("DO NOT POWER OFF", { x: pad, y: stateY + 88, size: 1, font });
657 } else if (fw.done) {
658 if (fw.ok) {
659 ink(T.ok[0], T.ok[1], T.ok[2]);
660 write("✓ firmware updated", { x: pad, y: stateY + 74, size: 1, font });
661 if (fw.backupPath) {
662 ink(T.fgMute);
663 write("backup: " + fw.backupPath.slice(0, 44), { x: pad, y: stateY + 88, size: 1, font });
664 }
665 ink(T.warn[0], T.warn[1], T.warn[2]);
666 write("reboot to activate (esc → os → y)", { x: pad, y: stateY + 102, size: 1, font });
667 } else {
668 ink(T.err[0], T.err[1], T.err[2]);
669 write("✗ flash failed — see log", { x: pad, y: stateY + 74, size: 1, font });
670 }
671 } else {
672 const pulse = Math.floor(180 + 75 * Math.sin(frame * 0.08));
673 ink(pulse, 255, pulse);
674 write("y: install t: dry-run r: restore", { x: pad, y: stateY + 74, size: 1, font });
675 ink(T.fgMute);
676 write("swaps bootsplash on MrChromebox ROM", { x: pad, y: stateY + 88, size: 1, font });
677 }
678
679 // Live log tail — last ~6 lines from the install script
680 const log = fw.log || [];
681 const maxLines = Math.min(6, log.length);
682 const logY = stateY + 120;
683 for (let i = 0; i < maxLines; i++) {
684 const entry = log[log.length - maxLines + i];
685 ink(60, 120, 80);
686 write(String(entry).slice(0, Math.floor((w - pad * 2) / 6)), { x: pad, y: logY + i * 10, size: 1, font });
687 }
688
689 } else {
690 // idle
691 ink(T.fgMute);
692 write("enter: check for updates", { x: pad, y: stateY, size: 1, font });
693 ink(T.fgMute - 20, T.fgMute, T.fgMute + 10);
694 write("d: devices", { x: pad, y: stateY + 14, size: 1, font });
695 // Only surface the firmware shortcut when the kernel + DMI confirm this
696 // board can actually be flashed — no false promises on stock OEM BIOS.
697 if (system?.firmware?.available) {
698 ink(T.fgMute - 20, T.fgMute + 10, T.fgMute);
699 write("f: firmware", { x: pad, y: stateY + 28, size: 1, font });
700 }
701 }
702
703 // Bottom hint (not during shutdown)
704 if (state !== "shutting-down") {
705 ink(T.fgMute, T.fgMute + 10, T.fgMute);
706 write(state === "devices" ? "esc: back to os" : "esc: back", { x: pad, y: h - 12, size: 1, font });
707 }
708
709 // === State machine: poll fetch/flash results ===
710
711 // Version check result (from .version file: "name hash-ts\nsize")
712 if (fetchPending && system?.fetchResult !== undefined && system?.fetchResult !== null) {
713 const raw = (typeof system.fetchResult === "string" ? system.fetchResult : "").trim();
714 fetchPending = false;
715 if (!raw || raw.length < 5) {
716 state = "error";
717 errorMsg = "bad version response";
718 } else {
719 const lines = raw.split("\n");
720 remoteVersion = lines[0].trim();
721 if (lines[1]) remoteSize = parseInt(lines[1].trim()) || 0;
722 state = (remoteVersion === currentVersion) ? "up-to-date" : "available";
723 }
724 }
725 if (fetchPending && system?.fetchError) {
726 fetchPending = false;
727 state = "error";
728 errorMsg = "fetch failed";
729 }
730 // Timeout
731 if (fetchPending && frame - checkFrame > 600) {
732 fetchPending = false;
733 state = "error";
734 errorMsg = "timeout";
735 }
736
737 // Download progress (kernel)
738 if (state === "downloading") {
739 const prevProgress = progress;
740 progress = system?.fetchBinaryProgress ?? progress;
741 if (progress > 0 && Math.floor(progress * 10) > Math.floor(prevProgress * 10)) {
742 const pct = Math.round(progress * 100);
743 addTelemetry(`kernel ${pct}% (${(progress * (remoteSize || 13_000_000) / 1048576).toFixed(1)}MB)`);
744 }
745 if (system?.fetchBinaryDone) {
746 if (system?.fetchBinaryOk) {
747 addTelemetry("kernel downloaded, fetching initramfs...");
748 state = "downloading-initramfs";
749 progress = 0;
750 initramfsDownloaded = false;
751 // Initramfs size isn't in the .version file today; use a generous
752 // default so the progress bar doesn't stall at 100% prematurely.
753 system?.fetchBinary?.(OS_INITRAMFS_URL, "/tmp/initramfs.cpio.gz.new", 336_000_000);
754 } else {
755 state = "error";
756 errorMsg = "kernel download failed";
757 }
758 }
759 }
760
761 // Download progress (initramfs) — kicks off flash once both files are local
762 if (state === "downloading-initramfs") {
763 const prevProgress = progress;
764 progress = system?.fetchBinaryProgress ?? progress;
765 if (progress > 0 && Math.floor(progress * 10) > Math.floor(prevProgress * 10)) {
766 const pct = Math.round(progress * 100);
767 addTelemetry(`initramfs ${pct}%`);
768 }
769 if (system?.fetchBinaryDone) {
770 if (system?.fetchBinaryOk) {
771 initramfsDownloaded = true;
772 addTelemetry("initramfs downloaded, starting flash...");
773 state = "flashing";
774 const dev = globalThis.__osFlashDevice;
775 addTelemetry("target: " + (dev || system?.bootDevice || "auto"));
776 // Four-arg flashUpdate: (kernelSrc, device, initramfsSrc)
777 // Device may be omitted — null lets C auto-detect the boot device.
778 system?.flashUpdate?.("/tmp/vmlinuz.new", dev || null, "/tmp/initramfs.cpio.gz.new");
779 } else {
780 state = "error";
781 errorMsg = "initramfs download failed";
782 }
783 }
784 }
785
786 // Clone progress
787 if (state === "cloning") {
788 if (system?.flashDone) {
789 if (system?.flashOk) {
790 flashedMB = ((system?.flashVerifiedBytes ?? 0) / 1048576).toFixed(1);
791 state = "confirm-reboot";
792 } else {
793 state = "error";
794 errorMsg = "clone verify failed";
795 }
796 }
797 }
798
799 // Flash progress — track phase transitions for telemetry
800 if (state === "flashing") {
801 const phase = system?.flashPhase ?? 0;
802 const prevPhase = globalThis.__osLastFlashPhase ?? -1;
803 if (phase !== prevPhase) {
804 globalThis.__osLastFlashPhase = phase;
805 const names = ["preparing flash", "writing EFI image", "syncing to disk", "verifying bytes", "flash complete"];
806 addTelemetry(names[phase] || `phase ${phase}`);
807 if (phase === 1) addTelemetry("dst: EFI/BOOT/BOOTX64.EFI");
808 }
809 if (system?.flashDone) {
810 // Capture flash log from C layer
811 const flog = system?.flashLog || [];
812 for (const line of flog) addTelemetry("[c] " + line);
813 if (system?.flashDst) addTelemetry("wrote: " + system.flashDst);
814 addTelemetry("same_dev=" + (system?.flashSameDevice ? "yes" : "no"));
815
816 if (system?.flashOk) {
817 flashedMB = ((system?.flashVerifiedBytes ?? 0) / 1048576).toFixed(1);
818 addTelemetry(`verified OK: ${flashedMB}MB`);
819 state = "confirm-reboot";
820 } else {
821 state = "error";
822 errorMsg = "flash verify failed";
823 addTelemetry("VERIFY FAILED");
824 for (const line of flog) addTelemetry("[c] " + line);
825 }
826 }
827 }
828}
829
830function sim() {}
831
832export { boot, paint, act, sim };