Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: live build telemetry on papers site

Added SSE client that connects to oven.aesthetic.computer/papers-build to show real-time build progress. Shows scrolling log lines with timestamps (like OS boot telemetry), progress bar, stage label, and percentage. Auto-hides when no build is running, auto-reloads page after successful build to pick up new PDFs. Logs panel is collapsible by clicking the header.

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

+182
+182
system/public/papers.aesthetic.computer/index.html
··· 298 298 .pals { position: fixed; bottom: 1.2em; right: 1.2em; width: 48px; opacity: 0.4; } 299 299 .pals:hover { opacity: 1; } 300 300 301 + /* Build telemetry */ 302 + .build-status { 303 + margin-top: 1.5em; 304 + border: 1px solid var(--box-border); 305 + border-radius: 6px; 306 + overflow: hidden; 307 + font-size: 11px; 308 + display: none; 309 + } 310 + .build-status.active { display: block; } 311 + .build-header { 312 + display: flex; 313 + align-items: center; 314 + gap: 8px; 315 + padding: 8px 12px; 316 + background: var(--box-bg); 317 + border-bottom: 1px solid var(--box-border); 318 + cursor: pointer; 319 + user-select: none; 320 + } 321 + .build-header:hover { opacity: 0.8; } 322 + .build-dot { 323 + width: 8px; height: 8px; 324 + border-radius: 50%; 325 + background: var(--cyan); 326 + animation: build-pulse 1.5s ease-in-out infinite; 327 + } 328 + .build-dot.done { background: #4caf50; animation: none; } 329 + .build-dot.fail { background: #e53935; animation: none; } 330 + @keyframes build-pulse { 331 + 0%, 100% { opacity: 1; } 332 + 50% { opacity: 0.3; } 333 + } 334 + .build-label { color: var(--text); font-weight: 600; flex: 1; } 335 + .build-stage { color: var(--pink); font-weight: 500; } 336 + .build-pct { color: var(--dim); min-width: 3em; text-align: right; } 337 + .build-progress { 338 + height: 2px; 339 + background: var(--sep); 340 + } 341 + .build-progress-bar { 342 + height: 100%; 343 + background: var(--cyan); 344 + transition: width 0.5s ease; 345 + width: 0%; 346 + } 347 + .build-logs { 348 + max-height: 280px; 349 + overflow-y: auto; 350 + padding: 8px 12px; 351 + background: rgba(0,0,0,0.15); 352 + font-family: 'Berkeley Mono Variable', monospace; 353 + font-size: 10px; 354 + line-height: 1.5; 355 + color: var(--dim); 356 + } 357 + .build-logs.collapsed { display: none; } 358 + .build-log-line { 359 + white-space: pre-wrap; 360 + word-break: break-all; 361 + } 362 + .build-log-line .ts { 363 + color: var(--purple); 364 + opacity: 0.6; 365 + margin-right: 6px; 366 + } 367 + .build-log-line.stderr { color: #e57373; } 368 + .build-log-line.ok { color: #4caf50; } 369 + .build-log-line.deploy { color: var(--cyan); } 370 + 301 371 /* === MOBILE (<600px) === */ 302 372 @media (max-width: 599px) { 303 373 body { padding: 0.75em 1em; } ··· 522 592 </div> 523 593 524 594 <!-- papers-end --> 595 + 596 + <div class="build-status" id="buildStatus"> 597 + <div class="build-header" id="buildHeader"> 598 + <span class="build-dot" id="buildDot"></span> 599 + <span class="build-label" id="buildLabel">building papers...</span> 600 + <span class="build-stage" id="buildStage"></span> 601 + <span class="build-pct" id="buildPct"></span> 602 + </div> 603 + <div class="build-progress"><div class="build-progress-bar" id="buildBar"></div></div> 604 + <div class="build-logs" id="buildLogs"></div> 605 + </div> 525 606 526 607 <div class="footer"> 527 608 <span><a href="https://aesthetic.computer">aesthetic.computer</a> &middot; <a href="/platter">platter</a> &middot; <a href="https://github.com/digitpain/aesthetic.computer">source</a></span> ··· 915 996 for (const p of papers) container.appendChild(p); 916 997 } catch {} 917 998 })(); 999 + 1000 + // === BUILD TELEMETRY === 1001 + const OVEN = 'https://oven.aesthetic.computer'; 1002 + const buildEl = document.getElementById('buildStatus'); 1003 + const buildDot = document.getElementById('buildDot'); 1004 + const buildLabel = document.getElementById('buildLabel'); 1005 + const buildStage = document.getElementById('buildStage'); 1006 + const buildPct = document.getElementById('buildPct'); 1007 + const buildBar = document.getElementById('buildBar'); 1008 + const buildLogs = document.getElementById('buildLogs'); 1009 + let logsCollapsed = false; 1010 + 1011 + document.getElementById('buildHeader').addEventListener('click', () => { 1012 + logsCollapsed = !logsCollapsed; 1013 + buildLogs.classList.toggle('collapsed', logsCollapsed); 1014 + }); 1015 + 1016 + function addLogLine(line, stream) { 1017 + const div = document.createElement('div'); 1018 + div.className = 'build-log-line'; 1019 + if (stream === 'stderr') div.classList.add('stderr'); 1020 + if (/^\s*(OK|DEPLOY)\s/.test(line)) div.classList.add(line.trim().startsWith('OK') ? 'ok' : 'deploy'); 1021 + const ts = new Date().toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); 1022 + div.innerHTML = `<span class="ts">${ts}</span>${line.replace(/</g, '&lt;')}`; 1023 + buildLogs.appendChild(div); 1024 + // Keep last 200 lines 1025 + while (buildLogs.children.length > 200) buildLogs.removeChild(buildLogs.firstChild); 1026 + buildLogs.scrollTop = buildLogs.scrollHeight; 1027 + } 1028 + 1029 + function connectBuildStream(jobId) { 1030 + buildEl.classList.add('active'); 1031 + buildDot.className = 'build-dot'; 1032 + buildLabel.textContent = 'building papers...'; 1033 + buildLogs.innerHTML = ''; 1034 + 1035 + const src = new EventSource(`${OVEN}/papers-build/${jobId}/stream`); 1036 + 1037 + src.addEventListener('status', (e) => { 1038 + try { 1039 + const d = JSON.parse(e.data); 1040 + buildStage.textContent = d.stage || ''; 1041 + const pct = Math.round(d.percent || 0); 1042 + buildPct.textContent = pct + '%'; 1043 + buildBar.style.width = pct + '%'; 1044 + } catch {} 1045 + }); 1046 + 1047 + src.addEventListener('logs', (e) => { 1048 + try { 1049 + const d = JSON.parse(e.data); 1050 + for (const log of (d.logs || [])) { 1051 + addLogLine(log.line, log.stream); 1052 + } 1053 + } catch {} 1054 + }); 1055 + 1056 + src.addEventListener('complete', (e) => { 1057 + try { 1058 + const d = JSON.parse(e.data); 1059 + const ok = d.status === 'success'; 1060 + buildDot.className = 'build-dot ' + (ok ? 'done' : 'fail'); 1061 + buildLabel.textContent = ok ? 'build complete' : 'build failed'; 1062 + buildStage.textContent = ''; 1063 + buildPct.textContent = ok ? '100%' : 'err'; 1064 + buildBar.style.width = ok ? '100%' : buildBar.style.width; 1065 + if (ok) addLogLine('publish complete — reloading in 5s...', 'stdout'); 1066 + // Auto-reload after successful build to pick up new PDFs 1067 + if (ok) setTimeout(() => location.reload(), 5000); 1068 + } catch {} 1069 + src.close(); 1070 + }); 1071 + 1072 + src.onerror = () => { 1073 + // SSE disconnected — check again in 30s 1074 + src.close(); 1075 + setTimeout(pollOven, 30000); 1076 + }; 1077 + } 1078 + 1079 + async function pollOven() { 1080 + try { 1081 + const res = await fetch(`${OVEN}/papers-build`, { signal: AbortSignal.timeout(5000) }); 1082 + if (!res.ok) return; 1083 + const data = await res.json(); 1084 + // Find an active build 1085 + const active = data.active || data.current; 1086 + if (active && (active.status === 'running' || active.status === 'pending')) { 1087 + connectBuildStream(active.id); 1088 + } else { 1089 + // No active build — hide and poll again later 1090 + buildEl.classList.remove('active'); 1091 + setTimeout(pollOven, 60000); 1092 + } 1093 + } catch { 1094 + setTimeout(pollOven, 60000); 1095 + } 1096 + } 1097 + 1098 + // Start polling for active builds 1099 + pollOven(); 918 1100 919 1101 // Theme toggle 920 1102 function toggleTheme() {