Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: Caddy access logs, traffic dashboard, l5/processing/rdp routes

- Caddy JSON access logs to /var/log/caddy/access.log (auto-rotated)
- /lith/traffic endpoint: aggregates by path, host, status
- Silo dashboard "lith" tab gets a "traffic" sub-tab with bar charts
- l5.aesthetic.computer and processing.aesthetic.computer serve their
own index.html instead of falling through to main SPA
- rdp.jas.life serves static files
- www.justanothersystem.org properly 301s to justanothersystem.org

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

+142 -3
+52 -2
lith/Caddyfile
··· 4 4 # --- papers.aesthetic.computer --- 5 5 :443 { 6 6 tls /etc/caddy/origin-cert.pem /etc/caddy/origin-key.pem 7 + log { 8 + output file /var/log/caddy/access.log { 9 + roll_size 50MiB 10 + roll_keep 3 11 + roll_keep_for 72h 12 + } 13 + format json 14 + level INFO 15 + } 7 16 # --- Global performance headers --- 8 17 # Cache static assets (1h fresh, serve stale for 24h while revalidating) 9 18 @cacheable path *.mjs *.js *.css *.woff2 *.woff *.ttf *.png *.jpg *.jpeg *.svg *.gif *.webp *.ico *.mp3 *.wav *.mp4 *.json ··· 101 110 } 102 111 103 112 # --- justanothersystem.org --- 104 - @jas host justanothersystem.org www.justanothersystem.org 113 + @wwwjas2 host www.justanothersystem.org 114 + handle @wwwjas2 { 115 + redir https://justanothersystem.org{uri} 301 116 + } 117 + 118 + @jas host justanothersystem.org 105 119 handle @jas { 106 120 handle /api/* { 107 121 reverse_proxy localhost:8888 ··· 280 294 redir https://aesthetic.computer{uri} 301 281 295 } 282 296 297 + # --- l5.aesthetic.computer --- 298 + @l5 host l5.aesthetic.computer 299 + handle @l5 { 300 + handle /api/* { 301 + reverse_proxy localhost:8888 302 + } 303 + handle /docs* { 304 + reverse_proxy localhost:8888 305 + } 306 + root * /opt/ac/system/public/l5.aesthetic.computer 307 + try_files {path} {path}.html /index.html 308 + file_server 309 + } 310 + 311 + # --- processing.aesthetic.computer --- 312 + @processing host processing.aesthetic.computer 313 + handle @processing { 314 + handle /api/* { 315 + reverse_proxy localhost:8888 316 + } 317 + handle /docs* { 318 + reverse_proxy localhost:8888 319 + } 320 + root * /opt/ac/system/public/processing.aesthetic.computer 321 + try_files {path} {path}.html /index.html 322 + file_server 323 + } 324 + 325 + # --- rdp.jas.life --- 326 + @rdp host rdp.jas.life 327 + handle @rdp { 328 + root * /opt/ac/system/public/rdp.jas.life 329 + try_files {path} {path}.html /index.html 330 + file_server 331 + } 332 + 283 333 # --- notepat.com, kidlisp.com root, www variants --- 284 334 # These all serve the main AC SPA 285 - @mainspa host aesthetic.computer www.aesthetic.computer kidlisp.com www.kidlisp.com notepat.com www.notepat.com l5.aesthetic.computer p5.aesthetic.computer processing.aesthetic.computer sitemap.aesthetic.computer 335 + @mainspa host aesthetic.computer www.aesthetic.computer kidlisp.com www.kidlisp.com notepat.com www.notepat.com p5.aesthetic.computer sitemap.aesthetic.computer 286 336 handle @mainspa { 287 337 # Preload critical assets (browser starts fetching before parsing HTML) 288 338 header Link "</aesthetic.computer/boot.mjs>; rel=preload; as=script; crossorigin, </aesthetic.computer/style.css>; rel=preload; as=style"
+38
lith/server.mjs
··· 374 374 res.json({ requests: filtered.slice(0, limit), total: filtered.length }); 375 375 }); 376 376 377 + // --- Caddy access log summary (for silo dashboard) --- 378 + app.get("/lith/traffic", async (req, res) => { 379 + try { 380 + const logPath = "/var/log/caddy/access.log"; 381 + const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean); 382 + const recent = lines.slice(-500); // last 500 entries 383 + const byPath = {}, byHost = {}, byStatus = {}; 384 + let total = 0; 385 + 386 + for (const line of recent) { 387 + try { 388 + const d = JSON.parse(line); 389 + const r = d.request || {}; 390 + const uri = (r.uri || "/").split("?")[0]; 391 + const host = r.host || "unknown"; 392 + const status = String(d.status || 0); 393 + // Aggregate by first path segment 394 + const seg = "/" + (uri.split("/")[1] || ""); 395 + byPath[seg] = (byPath[seg] || 0) + 1; 396 + byHost[host] = (byHost[host] || 0) + 1; 397 + byStatus[status] = (byStatus[status] || 0) + 1; 398 + total++; 399 + } catch {} 400 + } 401 + 402 + const sortDesc = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]); 403 + res.json({ 404 + total, 405 + logLines: lines.length, 406 + byPath: sortDesc(byPath).slice(0, 30), 407 + byHost: sortDesc(byHost).slice(0, 20), 408 + byStatus: sortDesc(byStatus), 409 + }); 410 + } catch (err) { 411 + res.json({ total: 0, error: err.message }); 412 + } 413 + }); 414 + 377 415 // --- /media/* handler (ports Netlify edge function media.js) --- 378 416 app.all("/media/*rest", async (req, res) => { 379 417 const parts = req.path.split("/").filter(Boolean); // ["media", ...]
+52 -1
silo/dashboard.html
··· 579 579 <div style="display:flex;gap:4px;padding:4px 6px;border-bottom:1px solid var(--border)"> 580 580 <button class="sub-tab-btn active" onclick="lithSubTab(this,'lith-errors-panel')">errors</button> 581 581 <button class="sub-tab-btn" onclick="lithSubTab(this,'lith-requests-panel')">recent requests</button> 582 + <button class="sub-tab-btn" onclick="lithSubTab(this,'lith-traffic-panel')">traffic</button> 582 583 </div> 583 584 584 585 <div id="lith-errors-panel" class="sub-panel active" style="display:block;padding:6px;overflow-y:auto;max-height:calc(100vh - 360px)"> ··· 606 607 <tbody id="lith-requests-body"></tbody> 607 608 </table> 608 609 </div> 610 + 611 + <div id="lith-traffic-panel" class="sub-panel" style="display:none;padding:6px;overflow-y:auto;max-height:calc(100vh - 360px)"> 612 + <div style="display:flex;gap:12px;flex-wrap:wrap"> 613 + <div style="flex:1;min-width:250px"> 614 + <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by path (last 500 requests)</div> 615 + <div id="lith-traffic-paths" style="font-size:11px">loading...</div> 616 + </div> 617 + <div style="flex:1;min-width:200px"> 618 + <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by host</div> 619 + <div id="lith-traffic-hosts" style="font-size:11px">loading...</div> 620 + </div> 621 + <div style="min-width:120px"> 622 + <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by status</div> 623 + <div id="lith-traffic-status" style="font-size:11px">loading...</div> 624 + </div> 625 + </div> 626 + </div> 627 + 609 628 </div> 610 629 611 630 </div> ··· 1937 1956 btn.classList.add('active'); 1938 1957 document.getElementById('lith-errors-panel').style.display = 'none'; 1939 1958 document.getElementById('lith-requests-panel').style.display = 'none'; 1959 + document.getElementById('lith-traffic-panel').style.display = 'none'; 1940 1960 document.getElementById(panelId).style.display = 'block'; 1941 1961 } 1942 1962 ··· 1955 1975 1956 1976 async function loadLith() { 1957 1977 try { 1958 - const [statsRes, errorsRes, reqsRes] = await Promise.all([ 1978 + const [statsRes, errorsRes, reqsRes, trafficRes] = await Promise.all([ 1959 1979 fetch(LITH_URL + '/lith/stats'), 1960 1980 fetch(LITH_URL + '/lith/errors?limit=100'), 1961 1981 fetch(LITH_URL + '/lith/requests?limit=100'), 1982 + fetch(LITH_URL + '/lith/traffic').catch(() => ({ json: () => ({ total: 0 }) })), 1962 1983 ]); 1963 1984 const stats = await statsRes.json(); 1964 1985 const errors = await errorsRes.json(); 1965 1986 const reqs = await reqsRes.json(); 1987 + const traffic = await trafficRes.json(); 1966 1988 1967 1989 // Overview 1968 1990 document.getElementById('lith-uptime').textContent = fmtUptime(stats.uptime); ··· 2028 2050 '</tr>'; 2029 2051 }).join(''); 2030 2052 } 2053 + // Traffic panels 2054 + function barRow(label, count, max) { 2055 + var pct = Math.max(2, Math.round(count / max * 100)); 2056 + return '<div style="display:flex;align-items:center;gap:4px;padding:1px 0">' + 2057 + '<span style="width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(label) + '</span>' + 2058 + '<div style="flex:1;height:6px;background:var(--bg);border-radius:2px;overflow:hidden">' + 2059 + '<div style="width:' + pct + '%;height:100%;background:var(--accent);border-radius:2px"></div>' + 2060 + '</div>' + 2061 + '<span style="width:40px;text-align:right;color:var(--fg2)">' + count + '</span></div>'; 2062 + } 2063 + if (traffic.byPath && traffic.byPath.length) { 2064 + var maxP = traffic.byPath[0][1]; 2065 + document.getElementById('lith-traffic-paths').innerHTML = traffic.byPath.map(function(p) { return barRow(p[0], p[1], maxP); }).join(''); 2066 + } else { 2067 + document.getElementById('lith-traffic-paths').textContent = 'no data'; 2068 + } 2069 + if (traffic.byHost && traffic.byHost.length) { 2070 + var maxH = traffic.byHost[0][1]; 2071 + document.getElementById('lith-traffic-hosts').innerHTML = traffic.byHost.map(function(h) { return barRow(h[0], h[1], maxH); }).join(''); 2072 + } else { 2073 + document.getElementById('lith-traffic-hosts').textContent = 'no data'; 2074 + } 2075 + if (traffic.byStatus && traffic.byStatus.length) { 2076 + document.getElementById('lith-traffic-status').innerHTML = traffic.byStatus.map(function(s) { 2077 + var color = s[0].startsWith('5') ? 'var(--err)' : s[0].startsWith('4') ? 'var(--warn)' : 'var(--ok)'; 2078 + return '<div style="padding:1px 0"><span style="color:' + color + ';font-weight:bold">' + s[0] + '</span> <span style="color:var(--fg2)">' + s[1] + '</span></div>'; 2079 + }).join(''); 2080 + } 2081 + 2031 2082 } catch (err) { 2032 2083 document.getElementById('lith-uptime').textContent = 'offline'; 2033 2084 document.getElementById('lith-uptime').style.color = 'var(--err)';