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
+
@@ -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
-
-
-