Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix(native): surface kernel errors and expose build success/failure statuses

+278 -14
+36 -4
fedac/native/docker-build.sh
··· 23 23 BUILD_TS="${AC_BUILD_TS:-$(date -u '+%Y-%m-%dT%H:%M')}" 24 24 BUILD_NAME="${AC_BUILD_NAME:-docker-build}" 25 25 HANDLE="${AC_HANDLE:-jeffrey}" 26 + KERNEL_JOBS="${AC_KERNEL_JOBS:-$(nproc)}" 27 + 28 + show_kernel_error_context() { 29 + local log_file="$1" 30 + local line 31 + local start 32 + local end 33 + 34 + line=$(grep -n -m 1 -E '(^|[^[:alpha:]])error:|Error [0-9]+|No rule to make target|undefined reference' "$log_file" | cut -d: -f1 || true) 35 + if [ -n "$line" ]; then 36 + start=$((line > 80 ? line - 80 : 1)) 37 + end=$((line + 160)) 38 + err " Kernel error context (lines ${start}-${end}):" 39 + sed -n "${start},${end}p" "$log_file" >&2 40 + fi 41 + err " Kernel build tail (last 220 lines):" 42 + tail -220 "$log_file" >&2 || true 43 + } 26 44 27 45 log "Building $BUILD_NAME ($GIT_HASH)" 28 46 ··· 321 339 322 340 # Configure — force-disable bloated GPU drivers that olddefconfig enables 323 341 cd "$LINUX_DIR" 324 - make olddefconfig 2>&1 | tail -3 342 + KCFG_LOG="$BUILD/kernel-olddefconfig.log" 343 + make olddefconfig >"$KCFG_LOG" 2>&1 || { err "Kernel olddefconfig failed"; tail -80 "$KCFG_LOG" >&2; exit 1; } 344 + tail -3 "$KCFG_LOG" || true 325 345 326 346 # Strip GPU drivers that Fedora defaults enable (they cause KALLSYMS overflow) 327 347 scripts/config --disable DRM_AMDGPU ··· 332 352 scripts/config --disable DRM_CIRRUS_QEMU 333 353 scripts/config --disable DRM_VIRTIO_GPU 334 354 scripts/config --enable DRM_SIMPLEDRM 335 - make olddefconfig 2>&1 | tail -1 355 + make olddefconfig >>"$KCFG_LOG" 2>&1 || { err "Kernel olddefconfig (post-config) failed"; tail -120 "$KCFG_LOG" >&2; exit 1; } 356 + tail -1 "$KCFG_LOG" || true 336 357 337 358 # Clean stale objects to avoid config mismatch errors 338 359 make clean 2>/dev/null || true 339 360 340 361 # Build 341 - log " Compiling ($(nproc) cores)..." 342 - make -j$(nproc) KALLSYMS_EXTRA_PASS=1 bzImage 2>&1 | tail -3 362 + log " Compiling (${KERNEL_JOBS} cores)..." 363 + KERNEL_LOG="$BUILD/kernel-build.log" 364 + if ! make -j"${KERNEL_JOBS}" KALLSYMS_EXTRA_PASS=1 bzImage >"$KERNEL_LOG" 2>&1; then 365 + err "Kernel compile failed while building bzImage." 366 + show_kernel_error_context "$KERNEL_LOG" 367 + exit 1 368 + fi 369 + tail -3 "$KERNEL_LOG" || true 343 370 344 371 # Copy output 372 + if [ ! -f arch/x86/boot/bzImage ]; then 373 + err "Kernel build completed but arch/x86/boot/bzImage is missing." 374 + show_kernel_error_context "$KERNEL_LOG" 375 + exit 1 376 + fi 345 377 cp arch/x86/boot/bzImage "$BUILD/vmlinuz" 346 378 cp arch/x86/boot/bzImage "$OUT/vmlinuz" 2>/dev/null || true 347 379
+75
oven/native-builder.mjs
··· 8 8 import path from "path"; 9 9 import { randomUUID } from "crypto"; 10 10 import { spawn } from "child_process"; 11 + import { MongoClient } from "mongodb"; 11 12 12 13 function runSync(cmd, args, cwd) { 13 14 return new Promise((resolve) => { ··· 21 22 22 23 const MAX_RECENT_JOBS = 10; 23 24 const MAX_LOG_LINES = 2000; 25 + const NATIVE_BUILD_COLLECTION = 26 + process.env.NATIVE_BUILD_COLLECTION || "oven-native-builds"; 24 27 25 28 // fedac/native/ lives in the native-git repo on oven (polled by native-git-poller). 26 29 const NATIVE_DIR = ··· 37 40 38 41 let progressCallback = null; 39 42 let lastProgressBroadcast = 0; 43 + let nativeBuildMongoClient = null; 44 + let nativeBuildMongoDb = null; 40 45 41 46 export function onNativeBuildProgress(cb) { 42 47 progressCallback = cb; ··· 46 51 return new Date().toISOString(); 47 52 } 48 53 54 + function toDateOrNull(v) { 55 + if (!v) return null; 56 + const d = new Date(v); 57 + if (Number.isNaN(d.getTime())) return null; 58 + return d; 59 + } 60 + 61 + async function getNativeBuildMongo() { 62 + if (nativeBuildMongoDb) return nativeBuildMongoDb; 63 + const uri = process.env.MONGODB_CONNECTION_STRING; 64 + const dbName = process.env.MONGODB_NAME; 65 + if (!uri || !dbName) return null; 66 + try { 67 + nativeBuildMongoClient = await MongoClient.connect(uri); 68 + nativeBuildMongoDb = nativeBuildMongoClient.db(dbName); 69 + await nativeBuildMongoDb 70 + .collection(NATIVE_BUILD_COLLECTION) 71 + .createIndex({ when: -1 }); 72 + await nativeBuildMongoDb 73 + .collection(NATIVE_BUILD_COLLECTION) 74 + .createIndex({ buildName: 1, when: -1 }); 75 + return nativeBuildMongoDb; 76 + } catch (err) { 77 + console.error("[native-builder] MongoDB connect failed:", err.message); 78 + return null; 79 + } 80 + } 81 + 82 + async function persistNativeBuildRecord(job) { 83 + try { 84 + const db = await getNativeBuildMongo(); 85 + if (!db) return; 86 + const startedAt = toDateOrNull(job.startedAt); 87 + const finishedAt = toDateOrNull(job.finishedAt); 88 + const durationMs = 89 + startedAt && finishedAt ? Math.max(0, finishedAt - startedAt) : null; 90 + const record = { 91 + jobId: job.id, 92 + buildName: job.buildName || null, 93 + ref: job.ref || null, 94 + gitHash: job.ref && job.ref !== "unknown" ? String(job.ref).slice(0, 40) : null, 95 + status: job.status || "unknown", 96 + stage: job.stage || null, 97 + percent: Number.isFinite(job.percent) ? job.percent : null, 98 + error: job.error || null, 99 + exitCode: Number.isFinite(job.exitCode) ? job.exitCode : null, 100 + commitMsg: job.commitMsg || null, 101 + flags: Array.isArray(job.flags) ? job.flags : [], 102 + changedPaths: job.changedPaths || "", 103 + createdAt: toDateOrNull(job.createdAt), 104 + startedAt, 105 + updatedAt: toDateOrNull(job.updatedAt), 106 + finishedAt, 107 + durationMs, 108 + logCount: Array.isArray(job.logs) ? job.logs.length : 0, 109 + logTail: Array.isArray(job.logs) 110 + ? job.logs.slice(-120).map((l) => l.line) 111 + : [], 112 + source: "oven-native-builder", 113 + when: new Date(), 114 + }; 115 + await db.collection(NATIVE_BUILD_COLLECTION).insertOne(record); 116 + } catch (err) { 117 + console.error("[native-builder] Failed to persist build record:", err.message); 118 + } 119 + } 120 + 49 121 function stripAnsi(s) { 50 122 return String(s || "").replace(/\u001b\[[0-9;]*m/g, ""); 51 123 } ··· 200 272 proc.on("error", reject); 201 273 proc.on("close", (code) => { 202 274 job.process = null; 275 + job.exitCode = code; 203 276 if (code !== 0) reject(new Error(`${label} failed (exit ${code})`)); 204 277 else resolve(); 205 278 }); ··· 331 404 job.status = "success"; 332 405 job.stage = "done"; 333 406 job.percent = 100; 407 + job.error = null; 334 408 job.finishedAt = nowISO(); 335 409 if (progressCallback) progressCallback(makeSnapshot(job)); 336 410 } catch (err) { ··· 340 414 job.error = err.message || String(err); 341 415 if (progressCallback) progressCallback(makeSnapshot(job)); 342 416 } finally { 417 + await persistNativeBuildRecord(job); 343 418 if (activeJobId === job.id) activeJobId = null; 344 419 } 345 420 }
+119
oven/server.mjs
··· 23 23 import { startPoller as startPapersGitPoller, getPollerStatus as getPapersPollerStatus } from './papers-git-poller.mjs'; 24 24 import { join, dirname } from 'path'; 25 25 import { fileURLToPath } from 'url'; 26 + import { MongoClient } from 'mongodb'; 26 27 27 28 const app = express(); 28 29 const PORT = process.env.PORT || 3002; ··· 48 49 const MAX_ACTIVITY_LOG = 100; 49 50 let wss = null; // Will be set after server starts 50 51 52 + const NATIVE_BUILD_COLLECTION = 53 + process.env.NATIVE_BUILD_COLLECTION || 'oven-native-builds'; 54 + const NATIVE_BUILD_STATUS_CACHE_MS = 30000; 55 + let nativeBuildStatusMongoClient = null; 56 + let nativeBuildStatusMongoDb = null; 57 + let nativeBuildStatusCacheAt = 0; 58 + let nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] }; 59 + 51 60 function addServerLog(type, icon, msg) { 52 61 const entry = { time: new Date().toISOString(), type, icon, msg }; 53 62 activityLogBuffer.unshift(entry); ··· 60 69 wss.clients.forEach(client => { 61 70 if (client.readyState === 1) client.send(logMsg); 62 71 }); 72 + } 73 + } 74 + 75 + function toIsoOrNull(value) { 76 + if (!value) return null; 77 + const d = new Date(value); 78 + if (Number.isNaN(d.getTime())) return null; 79 + return d.toISOString(); 80 + } 81 + 82 + async function getNativeBuildStatusMongoDb() { 83 + if (nativeBuildStatusMongoDb) return nativeBuildStatusMongoDb; 84 + const uri = process.env.MONGODB_CONNECTION_STRING; 85 + const dbName = process.env.MONGODB_NAME; 86 + if (!uri || !dbName) return null; 87 + try { 88 + nativeBuildStatusMongoClient = await MongoClient.connect(uri); 89 + nativeBuildStatusMongoDb = nativeBuildStatusMongoClient.db(dbName); 90 + return nativeBuildStatusMongoDb; 91 + } catch (err) { 92 + console.error('[os-releases] MongoDB connect failed:', err.message); 93 + return null; 94 + } 95 + } 96 + 97 + async function getNativeBuildStatusData() { 98 + const now = Date.now(); 99 + if (now - nativeBuildStatusCacheAt < NATIVE_BUILD_STATUS_CACHE_MS) { 100 + return nativeBuildStatusCache; 101 + } 102 + 103 + const db = await getNativeBuildStatusMongoDb(); 104 + if (!db) { 105 + nativeBuildStatusCacheAt = now; 106 + nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] }; 107 + return nativeBuildStatusCache; 108 + } 109 + 110 + try { 111 + const docs = await db 112 + .collection(NATIVE_BUILD_COLLECTION) 113 + .find({}) 114 + .sort({ when: -1 }) 115 + .limit(250) 116 + .toArray(); 117 + 118 + const byName = new Map(); 119 + const failedAttempts = []; 120 + for (const doc of docs) { 121 + const name = String(doc.buildName || doc.name || '').trim(); 122 + if (!name) continue; 123 + const status = String(doc.status || 'unknown').toLowerCase(); 124 + const ref = String(doc.ref || doc.gitHash || '').trim(); 125 + const item = { 126 + name, 127 + status, 128 + ref: ref || null, 129 + error: doc.error || null, 130 + commitMsg: doc.commitMsg || null, 131 + buildTs: 132 + toIsoOrNull(doc.finishedAt) || 133 + toIsoOrNull(doc.startedAt) || 134 + toIsoOrNull(doc.createdAt) || 135 + toIsoOrNull(doc.when) || 136 + null, 137 + }; 138 + if (!byName.has(name)) byName.set(name, item); 139 + if ((status === 'failed' || status === 'cancelled') && failedAttempts.length < 8) { 140 + failedAttempts.push(item); 141 + } 142 + } 143 + 144 + nativeBuildStatusCacheAt = now; 145 + nativeBuildStatusCache = { byName, failedAttempts }; 146 + return nativeBuildStatusCache; 147 + } catch (err) { 148 + console.error('[os-releases] Failed to load native build statuses:', err.message); 149 + nativeBuildStatusCacheAt = now; 150 + nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] }; 151 + return nativeBuildStatusCache; 63 152 } 64 153 } 65 154 ··· 2790 2879 const r = await fetch(`${RELEASES_BASE}/releases.json`); 2791 2880 if (!r.ok) return res.status(r.status).json({ error: 'Failed to fetch releases' }); 2792 2881 const data = await r.json(); 2882 + const releases = Array.isArray(data?.releases) ? data.releases : []; 2883 + 2884 + const { byName, failedAttempts } = await getNativeBuildStatusData(); 2885 + const releaseNames = new Set(); 2886 + for (const rel of releases) { 2887 + const name = String(rel?.name || '').trim(); 2888 + if (!name) continue; 2889 + releaseNames.add(name); 2890 + const statusMeta = byName.get(name); 2891 + if (statusMeta?.status) rel.status = statusMeta.status; 2892 + else if (!rel.status) rel.status = 'success'; 2893 + if (statusMeta?.error && !rel.error) rel.error = statusMeta.error; 2894 + if (statusMeta?.buildTs && !rel.last_attempt_ts) rel.last_attempt_ts = statusMeta.buildTs; 2895 + } 2896 + 2897 + const failedRows = failedAttempts 2898 + .filter((it) => !releaseNames.has(it.name)) 2899 + .map((it) => ({ 2900 + name: it.name, 2901 + git_hash: it.ref ? it.ref.slice(0, 40) : null, 2902 + build_ts: it.buildTs || new Date().toISOString(), 2903 + commit_msg: it.commitMsg || it.error || `build ${it.status}`, 2904 + status: it.status, 2905 + error: it.error || null, 2906 + deprecated: true, 2907 + attempt_only: true, 2908 + size: 0, 2909 + })); 2910 + 2911 + data.releases = releases.concat(failedRows); 2793 2912 res.json(data); 2794 2913 } catch (err) { 2795 2914 res.status(502).json({ error: err.message });
+48 -10
system/public/aesthetic.computer/disks/os.mjs
··· 363 363 wifiBtn = new ui.TextButton(wifiEnabled ? "ON" : "OFF", { x: 6, y: 0 }); 364 364 } 365 365 366 - function timeAgo(ts) { 366 + function timeAgo(ts) { 367 367 if (!ts) return ""; 368 368 const now = Date.now(); 369 369 const then = new Date(ts).getTime(); ··· 377 377 if (day < 30) return day + "d"; 378 378 const mo = Math.floor(day / 30); 379 379 if (mo < 12) return mo + "mo"; 380 - return Math.floor(mo / 12) + "y"; 381 - } 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 + } 382 411 383 412 function paint($) { 384 413 const { screen, ink, line: drawLine, dark, mask, unmask } = $; ··· 694 723 const name = rawName.length > maxNameChars ? rawName.slice(0, maxNameChars - 1) + "~" : rawName; 695 724 const hash = (b.git_hash || "?").slice(0, 7); 696 725 const ago = timeAgo(b.build_ts); 697 - const msg = b.commit_msg || ""; 698 - const who = b.handle || ""; 699 - const sizeMB = b.size ? (b.size / 1048576).toFixed(0) + "MB" : ""; 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); 700 730 701 731 // Alternating strip background 702 732 const strip = i % 2 === 0 ? stripA : stripB; ··· 733 763 x += (sizeMB.length + 1) * charW; 734 764 } 735 765 736 - if (who && !isNarrow) { 737 - ink(...(isCurrent ? C.handle : isDep ? C.depHandle : C.handleOld)); 738 - $.write("@" + who, { x, y: ry }); 739 - } 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 + } 740 778 741 779 // Strikethrough for deprecated 742 780 if (isDep) {