From e190faac04d11337ab35170da2e478c4e4f054ef Mon Sep 17 00:00:00 2001 From: Jeffrey Alan Scudder Date: Mon, 23 Mar 2026 16:06:23 -0700 Subject: [PATCH] fix(native): stage built-in firmware for kernel embed in docker build --- at/index.html | 267 +++++++++++++++--- fedac/native/docker-build.sh | 94 +++++- .../bills.aesthetic.computer/index.html | 33 ++- 3 files changed, 343 insertions(+), 51 deletions(-) diff --git a/at/index.html b/at/index.html index e59466f3f..f4fcfb48c 100644 --- a/at/index.html +++ b/at/index.html @@ -185,6 +185,34 @@ Mission statement + Top Users + All Media feed .search-box input:focus { border-color: rgb(205, 92, 155); } + .user-filters { + display: flex; + gap: 0.45em; + flex-wrap: wrap; + margin: 0.2em 0 1em 0; + } + + .user-filter { + border: 1px solid rgba(205, 92, 155, 0.25); + background: rgba(205, 92, 155, 0.08); + color: rgba(0, 0, 0, 0.7); + font-family: monospace; + font-size: 0.75em; + padding: 0.35em 0.55em; + border-radius: 999px; + cursor: pointer; + transition: all 0.15s; + } + + .user-filter.active { + background: rgba(205, 92, 155, 0.2); + border-color: rgba(205, 92, 155, 0.45); + color: rgb(205, 92, 155); + font-weight: bold; + } + + .user-filter:hover { background: rgba(205, 92, 155, 0.14); } + /* User list */ .user-list { display: flex; flex-direction: column; } @@ -266,6 +294,30 @@ Mission statement + Top Users + All Media feed .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(205, 92, 155, 0.3); border-radius: 50%; border-top-color: rgb(205, 92, 155); animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } + .feed-filters { + display: flex; + gap: 0.5em; + flex-wrap: wrap; + margin: 0.5em 0 1em 0; + } + + .feed-filter { + display: flex; + align-items: center; + gap: 0.3em; + font-size: 0.8em; + cursor: pointer; + padding: 0.3em 0.6em; + background: rgba(205, 92, 155, 0.1); + border-radius: 3px; + user-select: none; + transition: opacity 0.15s; + } + + .feed-filter.off { opacity: 0.35; } + + .feed-filter input { display: none; } + .load-more { display: block; margin: 1em auto; @@ -349,6 +401,7 @@ Mission statement + Top Users + All Media feed +

Loading users...

@@ -356,6 +409,7 @@ Mission statement + Top Users + All Media feed
+

Loading media...

@@ -385,8 +439,78 @@ Mission statement + Top Users + All Media feed { id: 'computer.aesthetic.paper', icon: '๐Ÿ“„', label: 'paper' }, ]; + const COLLECTION_BY_LABEL = Object.fromEntries(COLLECTIONS.map(c => [c.label, c.id])); + let allUsers = []; let feedLoaded = false; + let activeUserCollection = 'all'; + let userSearchQuery = ''; + let didToHandle = new Map(); + + function normalizeHandle(value) { + return String(value || '') + .trim() + .replace(/^@/, '') + .replace(/\.at\.aesthetic\.computer$/i, '') + .toLowerCase(); + } + + function formatDid(did) { + if (!did) return 'unknown'; + return did.length > 24 ? `${did.slice(0, 20)}...` : did; + } + + function userMatchesQuery(user, query) { + if (!query) return true; + const fields = [ + user.handle, + user.code, + normalizeHandle(user.handle), + normalizeHandle(user.code), + ]; + return fields.some(v => String(v || '').toLowerCase().includes(query)); + } + + function getFilteredUsers() { + let users = allUsers; + if (activeUserCollection !== 'all') { + const colId = COLLECTION_BY_LABEL[activeUserCollection]; + users = users.filter(user => (user.collections || []).includes(colId)); + } + const q = normalizeHandle(userSearchQuery); + if (q) users = users.filter(user => userMatchesQuery(user, q)); + return users; + } + + function renderFilteredUsers() { + renderUsers(getFilteredUsers()); + } + + function buildUserFilters() { + const el = document.getElementById('user-filters'); + if (!el) return; + + const filters = [{ label: 'all', icon: 'โญ' }, ...COLLECTIONS]; + el.innerHTML = filters + .map(f => ``) + .join(''); + + el.querySelectorAll('.user-filter').forEach(btn => { + btn.addEventListener('click', () => { + activeUserCollection = btn.dataset.label; + buildUserFilters(); + renderFilteredUsers(); + }); + }); + } + + function repoLabelForItem(item) { + if (item._handle) return item._handle; + const repo = item.uri ? item.uri.split('/')[2] : item._did; + if (!repo) return 'unknown'; + if (repo.startsWith('did:')) return formatDid(repo); + return `@${normalizeHandle(repo)}`; + } // --- Tabs --- document.querySelectorAll('.tab').forEach(tab => { @@ -412,7 +536,8 @@ Mission statement + Top Users + All Media feed document.getElementById('total-records').textContent = data.stats.totalRecords.toLocaleString(); document.getElementById('active-users').textContent = data.stats.activeUsers.toLocaleString(); } - renderUsers(allUsers); + buildUserFilters(); + renderFilteredUsers(); } catch (error) { document.getElementById('users-container').innerHTML = '
Failed to load users.
'; } @@ -429,7 +554,7 @@ Mission statement + Top Users + All Media feed const row = document.createElement('a'); row.className = 'user-row'; const identifier = user.handle || user.code; - const shortHandle = identifier.replace('.at.aesthetic.computer', '').replace('@', ''); + const shortHandle = normalizeHandle(identifier); row.href = `https://${shortHandle}.at.aesthetic.computer`; row.target = '_blank'; @@ -442,7 +567,7 @@ Mission statement + Top Users + All Media feed 'computer.aesthetic.tape': '๐Ÿ“ผ', }; for (const [col, emoji] of Object.entries(badgeMap)) { - if (user.collections.includes(col)) { + if ((user.collections || []).includes(col)) { badges.push(`${emoji} ${user.recordCounts[col] || 0}`); } } @@ -473,65 +598,135 @@ Mission statement + Top Users + All Media feed } document.getElementById('search').addEventListener('input', (e) => { - const q = e.target.value.toLowerCase(); - renderUsers(q ? allUsers.filter(u => (u.handle || u.code).toLowerCase().includes(q)) : allUsers); + userSearchQuery = e.target.value || ''; + renderFilteredUsers(); }); // --- All Media Feed --- + let allFeedItems = []; + let activeFilters = new Set(COLLECTIONS.map(c => c.label)); + + function timeAgo(dateStr) { + const now = Date.now(); + const then = new Date(dateStr).getTime(); + const sec = Math.floor((now - then) / 1000); + if (sec < 60) return 'just now'; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}m ago`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h ago`; + const days = Math.floor(hr / 24); + if (days < 30) return `${days}d ago`; + if (days < 365) { + const months = Math.floor(days / 30); + return `${months}mo ago`; + } + const years = Math.floor(days / 365); + return `${years}y ago`; + } + + function buildFilters() { + const el = document.getElementById('feed-filters'); + el.innerHTML = COLLECTIONS.map(col => + `` + ).join(''); + el.querySelectorAll('.feed-filter').forEach(label => { + label.addEventListener('click', (e) => { + e.preventDefault(); + const col = label.dataset.col; + if (activeFilters.has(col)) { activeFilters.delete(col); label.classList.add('off'); } + else { activeFilters.add(col); label.classList.remove('off'); } + renderFeed(allFeedItems, document.getElementById('feed-container')); + }); + }); + } + async function loadFeed() { feedLoaded = true; const container = document.getElementById('feed-container'); + buildFilters(); + didToHandle = new Map(); try { - // Fetch repos, then latest records from each collection across top repos - const reposRes = await fetch(`${PDS_URL}/xrpc/com.atproto.sync.listRepos?limit=50`); - const reposData = await reposRes.json(); - const repos = (reposData.repos || []).map(r => r.did); - - const allItems = []; - - // For each collection, fetch recent records from all repos (limit per repo for speed) - for (const col of COLLECTIONS) { - for (const did of repos.slice(0, 20)) { - try { - const res = await fetch(`${PDS_URL}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(col.id)}&limit=5`); - if (!res.ok) continue; - const data = await res.json(); - for (const rec of (data.records || [])) { - allItems.push({ ...rec.value, uri: rec.uri, _col: col, _did: did }); - } - } catch { /* skip */ } + // Use top users' DIDs from already-loaded data (avoids extra listRepos call) + // Fall back to listRepos if users haven't loaded yet + let dids = []; + if (allUsers.length > 0) { + // Get unique user IDs, resolve to DIDs via the users data + const seen = new Set(); + for (const u of allUsers.slice(0, 15)) { + const handle = normalizeHandle(u.handle || u.code || ''); + if (handle && !seen.has(handle)) { + seen.add(handle); + } } + // Resolve handles to DIDs + const resolves = await Promise.allSettled( + [...seen].map(h => + fetch(`${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(h + '.at.aesthetic.computer')}`) + .then(r => r.ok ? r.json() : null) + .then(d => { + if (d?.did) didToHandle.set(d.did, `@${h}`); + return d?.did; + }) + ) + ); + dids = resolves.map(r => r.value).filter(Boolean); } - // Sort by when (reverse chrono) - allItems.sort((a, b) => { - const da = new Date(a.when || a.createdAt || 0); - const db = new Date(b.when || b.createdAt || 0); - return db - da; - }); + if (dids.length === 0) { + const reposRes = await fetch(`${PDS_URL}/xrpc/com.atproto.sync.listRepos?limit=15`); + const reposData = await reposRes.json(); + const repos = reposData.repos || []; + dids = repos.map(r => r.did); + for (const repo of repos) { + const handle = normalizeHandle(repo.handle || ''); + if (repo.did && handle) didToHandle.set(repo.did, `@${handle}`); + } + } + + // Fire ALL requests in parallel: dids ร— collections + const fetches = []; + for (const did of dids.slice(0, 12)) { + for (const col of COLLECTIONS) { + fetches.push( + fetch(`${PDS_URL}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(col.id)}&limit=5&reverse=true`) + .then(r => r.ok ? r.json() : { records: [] }) + .then(data => (data.records || []).map(rec => ({ ...rec.value, uri: rec.uri, _col: col, _did: did, _handle: didToHandle.get(did) || '' }))) + .catch(() => []) + ); + } + } + + const results = await Promise.all(fetches); + allFeedItems = results.flat(); + + // Sort reverse chrono + allFeedItems.sort((a, b) => new Date(b.when || b.createdAt || 0) - new Date(a.when || a.createdAt || 0)); - renderFeed(allItems.slice(0, 100), container); + renderFeed(allFeedItems, container); } catch (error) { container.innerHTML = '
Failed to load media feed.
'; } } function renderFeed(items, container) { - if (!items.length) { container.innerHTML = '
No media found
'; return; } + const filtered = items.filter(item => activeFilters.has(item._col.label)); + if (!filtered.length) { container.innerHTML = '
No media found
'; return; } const feed = document.createElement('div'); feed.className = 'feed'; - for (const item of items) { + for (const item of filtered.slice(0, 150)) { const el = document.createElement('div'); el.className = 'feed-item'; const col = item._col; const when = item.when || item.createdAt; - const dateStr = when ? new Date(when).toLocaleDateString() : ''; - const handle = item.uri ? item.uri.split('/')[2] : item._did; - const shortDid = handle.length > 20 ? handle.slice(0, 16) + '...' : handle; + const ago = when ? timeAgo(when) : ''; + const repoLabel = repoLabelForItem(item); let title = ''; let thumb = ''; @@ -579,7 +774,7 @@ Mission statement + Top Users + All Media feed ${thumb}
${titleHtml}
-
${col.label} ยท ${dateStr} ยท ${shortDid}
+
${col.label} ยท ${ago} ยท ${repoLabel}
`; feed.appendChild(el); } diff --git a/fedac/native/docker-build.sh b/fedac/native/docker-build.sh index 51dd331ba..37cfc58d4 100644 --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -42,6 +42,40 @@ show_kernel_error_context() { tail -220 "$log_file" >&2 || true } +run_make_with_heartbeat() { + local log_file="$1" + shift + local heartbeat_secs="${AC_KERNEL_HEARTBEAT_SECS:-20}" + local last_count="-1" + local last_line="" + local line_count + local current_line + + : > "$log_file" + "$@" >"$log_file" 2>&1 & + local make_pid=$! + + while kill -0 "$make_pid" 2>/dev/null; do + sleep "$heartbeat_secs" + [ -f "$log_file" ] || continue + line_count=$(wc -l <"$log_file" 2>/dev/null || echo 0) + current_line=$(tail -1 "$log_file" 2>/dev/null || true) + current_line="${current_line:0:180}" + if [ "$line_count" != "$last_count" ] || [ "$current_line" != "$last_line" ]; then + log " [kernel] running... lines=$line_count last=$current_line" + last_count="$line_count" + last_line="$current_line" + else + log " [kernel] running... lines=$line_count (no new lines yet)" + fi + done + + if wait "$make_pid"; then + return 0 + fi + return $? +} + log "Building $BUILD_NAME ($GIT_HASH)" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -334,6 +368,62 @@ fi # Copy config cp "$NATIVE/kernel/config-minimal" "$LINUX_DIR/.config" +# Stage built-in firmware blobs referenced by CONFIG_EXTRA_FIRMWARE +# into a container-local directory and rewrite CONFIG_EXTRA_FIRMWARE_DIR. +FIRMWARE_ABS="$BUILD/firmware" +mkdir -p "$FIRMWARE_ABS" + +HOST_FWDIR="" +for d in /usr/lib/firmware /lib/firmware; do + if [ -d "$d" ]; then + HOST_FWDIR="$d" + break + fi +done + +copy_builtin_fw_blob() { + local rel="$1" + local src_base="$2" + local dst="$FIRMWARE_ABS/$rel" + mkdir -p "$(dirname "$dst")" + if [ -f "$src_base/$rel" ]; then + cp -L "$src_base/$rel" "$dst" + return 0 + fi + if [ -f "$src_base/$rel.zst" ]; then + zstd -d "$src_base/$rel.zst" -o "$dst" 2>/dev/null && return 0 + fi + if [ -f "$src_base/$rel.xz" ]; then + xz -dc "$src_base/$rel.xz" >"$dst" 2>/dev/null && return 0 + fi + return 1 +} + +FW_LIST=$(sed -n 's/^CONFIG_EXTRA_FIRMWARE="\([^"]*\)"/\1/p' "$LINUX_DIR/.config" | head -1) +if [ -n "$FW_LIST" ]; then + if [ -z "$HOST_FWDIR" ]; then + err "Kernel config requests built-in firmware but no /usr/lib/firmware or /lib/firmware directory was found." + exit 1 + fi + + missing_fw="" + for fw in $FW_LIST; do + if ! copy_builtin_fw_blob "$fw" "$HOST_FWDIR"; then + missing_fw="$missing_fw $fw" + fi + done + + if [ -n "$missing_fw" ]; then + err "Missing built-in firmware blobs:$missing_fw" + err "Looked under: $HOST_FWDIR (including .zst/.xz variants)" + exit 1 + fi + + sed -i "s|^CONFIG_EXTRA_FIRMWARE_DIR=.*|CONFIG_EXTRA_FIRMWARE_DIR=\"$FIRMWARE_ABS\"|" "$LINUX_DIR/.config" + log " Built-in firmware dir: $FIRMWARE_ABS" + log " Built-in firmware files: $FW_LIST" +fi + # Copy initramfs into kernel tree cp "$BUILD/initramfs.cpio.lz4" "$LINUX_DIR/initramfs.cpio.lz4" @@ -361,14 +451,14 @@ make clean 2>/dev/null || true # Build log " Compiling (${KERNEL_JOBS} cores)..." KERNEL_LOG="$BUILD/kernel-build.log" -if ! make -j"${KERNEL_JOBS}" KALLSYMS_EXTRA_PASS=1 bzImage >"$KERNEL_LOG" 2>&1; then +if ! run_make_with_heartbeat "$KERNEL_LOG" make -j"${KERNEL_JOBS}" KALLSYMS_EXTRA_PASS=1 bzImage; then err "Kernel compile failed while building bzImage (parallel pass)." show_kernel_error_context "$KERNEL_LOG" if [ "${KERNEL_JOBS}" -gt 1 ]; then err "Retrying kernel build in serial mode (-j1, V=1) for deterministic diagnostics..." make clean 2>/dev/null || true KERNEL_LOG_RETRY="$BUILD/kernel-build-retry.log" - if ! make -j1 V=1 KALLSYMS_EXTRA_PASS=1 bzImage >"$KERNEL_LOG_RETRY" 2>&1; then + if ! run_make_with_heartbeat "$KERNEL_LOG_RETRY" make -j1 V=1 KALLSYMS_EXTRA_PASS=1 bzImage; then err "Kernel compile failed again in serial retry." show_kernel_error_context "$KERNEL_LOG_RETRY" exit 1 diff --git a/system/public/bills.aesthetic.computer/index.html b/system/public/bills.aesthetic.computer/index.html index 8ec29707c..180e6f764 100644 --- a/system/public/bills.aesthetic.computer/index.html +++ b/system/public/bills.aesthetic.computer/index.html @@ -73,9 +73,9 @@ .header-row { display: flex; align-items: baseline; - justify-content: space-between; + justify-content: flex-start; gap: 1em; - margin-bottom: 1.5em; + margin-bottom: 0.8em; } .header-left { @@ -194,6 +194,14 @@ text-decoration: none; } + .needs-support { + display: flex; + align-items: center; + gap: 0.6em; + flex-wrap: wrap; + margin-bottom: 1em; + } + /* Alerts */ .alerts { margin-bottom: 1.5em; } @@ -575,6 +583,7 @@ .header-left { gap: 0.4em; } .subtitle { font-size: 0.7em; } .bill-tab { font-size: 0.82em; padding: 0.7em 0.5em; } + .needs-support { align-items: flex-start; } .stats { grid-template-columns: 1fr 1fr; } .stat-value { font-size: 1.4em; } .net-flow { grid-template-columns: 1fr; gap: 0.3em; padding: 0.8em; } @@ -603,14 +612,11 @@

bills

aesthetic.computer โ€” cash flow
- - -
- give.aesthetic.computer +
+ +
@@ -660,12 +666,13 @@
-
- - -
-
+

priority actions

-- 2.51.2