Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

papers/platter: restore the original /platter, add jeffrey + opinions

I overwrote /platter (the white research-records page at platter.html)
with my own dark-themed dir index. Reverting that — the original page is
the right home for the platter overview, and the new content (Jeffrey's
Instagram link, opinions list) belongs as sections inside it.

- Delete system/public/papers.aesthetic.computer/platter/index.html
- Caddyfile: replace /platter→/platter/ permanent redir with
/platter/→/platter 302 to invalidate the cached 301; keep
/platter/jeffrey→/platter/jeffrey/ permanent so the dashboard's
relative manifest fetch still works
- platter.html: add Dashboards section linking to /platter/jeffrey/
and Opinions section listing the four .md essays directly (raw
markdown URLs, per request — not the AC pixel-text rendered version)

Also: jeffrey dashboard now uses full-quality originals in BOTH the
grid AND the lightbox (was thumbs in grid, full in lightbox). Browsers
lazy-load and cache, so the per-image cost is paid once on first scroll.

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

+40 -247
+9 -12
lith/Caddyfile
··· 56 56 } 57 57 root * /opt/ac/system/public/papers.aesthetic.computer 58 58 59 - # Explicit canonical redirects for the platter dirs. Two reasons: 60 - # (1) without the /platter redirect, try_files's interaction with 61 - # file_server somehow serves the papers root /index.html for the 62 - # no-slash form rather than rewriting to /platter/index.html. 63 - # (2) for /platter/jeffrey, try_files DOES correctly serve the 64 - # right index.html — but the page's relative `./manifest.json` 65 - # fetch then resolves to /platter/manifest.json (browser treats 66 - # `jeffrey` as a file in /platter/, not a dir), which 404s into 67 - # the SPA fallback and breaks the dashboard. Forcing a trailing 68 - # slash via redir keeps relative URLs honest. 69 - # (See: 2026-04-29 platter URL move debugging.) 70 - redir /platter /platter/ permanent 59 + # /platter is the existing white research-records page (platter.html). 60 + # We briefly created /platter/ as a directory with our own index — that 61 + # was the wrong call; restored below by deleting the dir's index and 62 + # reversing the prior 301 with a 302 to invalidate cached redirects. 63 + # /platter/jeffrey/ remains a real directory (the dashboard); its 64 + # explicit canonical redirect stays so the page's relative 65 + # `./manifest.json` fetch resolves correctly. 66 + # (See: 2026-04-29 platter restoration.) 67 + redir /platter/ /platter 302 71 68 redir /platter/jeffrey /platter/jeffrey/ permanent 72 69 73 70 # SPA fallback for unknown paths: if none of {path}, {path}.html,
+27
system/public/papers.aesthetic.computer/platter.html
··· 351 351 </div> 352 352 </div> 353 353 354 + <!-- DASHBOARDS — visual indexes over the platter material --> 355 + <div class="section" data-section="dashboards"> 356 + <div class="section-header" data-color="cyan" onclick="toggleSection(this)"> 357 + <span class="toggle">&#9660;</span> 358 + <h2>Dashboards</h2> 359 + <span class="count">visual indexes</span> 360 + </div> 361 + <div class="section-items"> 362 + <a class="item" href="/platter/jeffrey/"><span class="file">live</span> Jeffrey's Instagram &mdash; 457 confirmed-self selfies from <em>@whistlegraph</em>, face-matched and GPT-4o described</a> 363 + </div> 364 + </div> 365 + 366 + <!-- OPINIONS — machine-generated essays connecting external ideas to AC --> 367 + <div class="section" data-section="opinions"> 368 + <div class="section-header" data-color="gold" onclick="toggleSection(this)"> 369 + <span class="toggle">&#9660;</span> 370 + <h2>Opinions</h2> 371 + <span class="count">4 essays &middot; rendered at <em>aesthetic.computer/opinion</em></span> 372 + </div> 373 + <div class="section-items"> 374 + <a class="item" href="https://aesthetic.computer/opinion/lotus-notes-and-the-blank-canvas.md"><span class="file">md</span> Lotus Notes and the Blank Canvas <span class="file">2026-03-19</span></a> 375 + <a class="item" href="https://aesthetic.computer/opinion/digital-art-as-medium.md"><span class="file">md</span> Digital Art as Medium; NFTs as Mechanism <span class="file">2026-02-22</span></a> 376 + <a class="item" href="https://aesthetic.computer/opinion/what-is-happening-to-writing.md"><span class="file">md</span> What Is Happening to Writing? <span class="file">2026-02-19</span></a> 377 + <a class="item" href="https://aesthetic.computer/opinion/you-are-not-just-your-brain.md"><span class="file">md</span> You Are Not (Just) Your Brain <span class="file">2026-02-17</span></a> 378 + </div> 379 + </div> 380 + 354 381 <!-- WHISTLEGRAPH --> 355 382 <div class="section" data-section="whistlegraph"> 356 383 <div class="section-header" data-color="purple" onclick="toggleSection(this)">
-216
system/public/papers.aesthetic.computer/platter/index.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>platter · papers.aesthetic.computer</title> 7 - <meta name="description" content="Per-platter dashboards for Aesthetic Computer research material — visual indexes that complement the textual platter READMEs in papers/."> 8 - <link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png"> 9 - <link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css"> 10 - <style> 11 - :root { 12 - --bg: #111; --bg2: #1a1a1a; --fg: #ddd; --fg2: #888; 13 - --accent: #ff6b9d; --accent2: #4ecdc4; --gold: #ffd93d; 14 - --border: #333; --hover: #222; 15 - } 16 - * { margin: 0; padding: 0; box-sizing: border-box; } 17 - html, body { background: var(--bg); color: var(--fg); } 18 - body { 19 - font-family: 'Berkeley Mono Variable', ui-monospace, monospace; 20 - font-size: 13px; line-height: 1.5; 21 - min-height: 100vh; 22 - } 23 - a { color: var(--accent2); text-decoration: none; } 24 - a:hover { color: var(--accent); } 25 - 26 - header { 27 - padding: 24px 16px 16px; 28 - border-bottom: 1px solid var(--border); 29 - } 30 - h1 { 31 - font-size: 20px; font-weight: normal; letter-spacing: 5px; 32 - margin-bottom: 6px; 33 - } 34 - h1 .accent { color: var(--accent); } 35 - .sub { font-size: 11px; color: var(--fg2); max-width: 640px; } 36 - 37 - main { 38 - max-width: 880px; 39 - margin: 0 auto; 40 - padding: 16px; 41 - display: grid; 42 - grid-template-columns: 1fr; 43 - gap: 16px; 44 - } 45 - @media (min-width: 700px) { 46 - main { grid-template-columns: 1fr 1fr; } 47 - } 48 - 49 - .platter-card { 50 - display: block; 51 - background: var(--bg2); border: 1px solid var(--border); 52 - overflow: hidden; 53 - transition: border-color 0.1s, transform 0.1s; 54 - color: inherit; 55 - } 56 - .platter-card:hover { border-color: var(--accent2); } 57 - .platter-card:active { transform: scale(0.99); } 58 - 59 - .platter-card .cover { 60 - width: 100%; aspect-ratio: 16/9; 61 - background: #000; 62 - display: flex; align-items: center; justify-content: center; 63 - overflow: hidden; 64 - } 65 - .platter-card .cover img { 66 - width: 100%; height: 100%; object-fit: cover; display: block; 67 - } 68 - .platter-card .cover.coming-soon { 69 - color: var(--fg2); 70 - font-size: 11px; letter-spacing: 2px; text-transform: uppercase; 71 - } 72 - 73 - .platter-card .info { 74 - padding: 12px 14px; 75 - display: flex; flex-direction: column; gap: 6px; 76 - } 77 - .platter-card .info h2 { 78 - font-size: 14px; font-weight: normal; letter-spacing: 2px; 79 - color: var(--fg); 80 - } 81 - .platter-card .info .meta { 82 - font-size: 11px; color: var(--fg2); 83 - } 84 - .platter-card .info .desc { 85 - font-size: 12px; color: var(--fg); 86 - line-height: 1.5; 87 - } 88 - .platter-card.disabled { 89 - opacity: 0.5; pointer-events: none; 90 - } 91 - 92 - .opinions { 93 - max-width: 880px; margin: 24px auto 0; padding: 0 16px; 94 - } 95 - .section-h { 96 - font-size: 14px; font-weight: normal; letter-spacing: 3px; 97 - color: var(--fg); padding-bottom: 8px; 98 - border-bottom: 1px solid var(--border); 99 - margin-bottom: 8px; 100 - } 101 - .opinions-sub { font-size: 11px; color: var(--fg2); margin-bottom: 12px; max-width: 640px; } 102 - .opinion-list { list-style: none; } 103 - .opinion-list li { padding: 0; } 104 - .opinion-list li.opinion-loading { color: var(--fg2); font-size: 11px; padding: 8px 0; } 105 - .opinion-list a { 106 - display: grid; grid-template-columns: 110px 1fr; 107 - gap: 12px; padding: 8px 10px; 108 - border-bottom: 1px solid var(--border); 109 - color: var(--fg); transition: background-color 0.1s; 110 - } 111 - .opinion-list a:hover { background: var(--hover); color: var(--accent); text-decoration: none; } 112 - .opinion-list .o-date { 113 - color: var(--fg2); font-size: 11px; 114 - font-variant-numeric: tabular-nums; 115 - padding-top: 2px; 116 - } 117 - .opinion-list .o-title { font-size: 13px; } 118 - .opinion-list .o-author { font-size: 10px; color: var(--fg2); display: block; margin-top: 2px; } 119 - 120 - footer { 121 - padding: 24px 16px; text-align: center; font-size: 11px; 122 - color: var(--fg2); border-top: 1px solid var(--border); 123 - margin-top: 32px; 124 - } 125 - footer a { color: var(--fg2); } 126 - footer a:hover { color: var(--accent2); } 127 - </style> 128 - </head> 129 - <body> 130 - 131 - <header> 132 - <h1>platter<span class="accent">·</span>index</h1> 133 - <div class="sub"> 134 - visual dashboards over Aesthetic Computer's research platters — the source-material collections behind the 135 - <a href="../">papers</a>. each platter is an evolving body of references, captures, and notes; these pages are the navigable surface. 136 - </div> 137 - </header> 138 - 139 - <main> 140 - 141 - <a class="platter-card" href="./jeffrey/"> 142 - <div class="cover"> 143 - <img src="https://assets.aesthetic.computer/jeffreys/whistlegraph/DFtJH9uzU3C.jpg" alt="" loading="lazy"> 144 - </div> 145 - <div class="info"> 146 - <h2>jeffrey's instagram</h2> 147 - <div class="meta">457 confirmed-self selfies · 2014–2026 · face-matched + GPT-4o described</div> 148 - <div class="desc"> 149 - the curated archive of jeffrey alan scudder's selfies from 150 - <a href="https://www.instagram.com/whistlegraph/" target="_blank">@whistlegraph</a>, 151 - cross-matched against the AV studio shoot for identity and tagged per-image with subject, expression, pose, and tags. 152 - </div> 153 - </div> 154 - </a> 155 - 156 - <div class="platter-card disabled"> 157 - <div class="cover coming-soon">whistlegraph · soon</div> 158 - <div class="info"> 159 - <h2>whistlegraph</h2> 160 - <div class="meta">drawing-while-singing scores · 2019–2026</div> 161 - <div class="desc"> 162 - index of the whistlegraph corpus — pieces, scores, public showings, press, fan submissions. textual platter exists at 163 - <a href="https://github.com/digitpain/aesthetic-computer/blob/main/papers/whistlegraph-platter/README.md">papers/whistlegraph-platter</a>. 164 - </div> 165 - </div> 166 - </div> 167 - 168 - </main> 169 - 170 - <section class="opinions"> 171 - <h2 class="section-h">opinions</h2> 172 - <div class="opinions-sub"> 173 - machine-generated essays connecting external ideas to AC's design philosophy. 174 - rendered natively at <a href="https://aesthetic.computer/opinion">aesthetic.computer/opinion</a> using pixel text. 175 - </div> 176 - <ul class="opinion-list" id="opinion-list"> 177 - <li class="opinion-loading">loading…</li> 178 - </ul> 179 - </section> 180 - 181 - <footer> 182 - <a href="../">papers.aesthetic.computer</a> 183 - &nbsp;·&nbsp; 184 - <a href="https://github.com/digitpain/aesthetic-computer/tree/main/papers">papers/ on github</a> 185 - </footer> 186 - 187 - <script> 188 - (async () => { 189 - const list = document.getElementById("opinion-list"); 190 - try { 191 - const res = await fetch("https://aesthetic.computer/opinion/index.json", { cache: "no-store" }); 192 - if (!res.ok) throw new Error(res.status + " " + res.statusText); 193 - const items = (await res.json()).sort((a, b) => (a.date < b.date ? 1 : -1)); 194 - if (!items.length) { list.innerHTML = `<li class="opinion-loading">no opinions yet</li>`; return; } 195 - list.innerHTML = items.map((o) => ` 196 - <li> 197 - <a href="https://aesthetic.computer/opinion/${o.slug}" target="_blank"> 198 - <span class="o-date">${o.date}</span> 199 - <span> 200 - <span class="o-title">${escapeHtml(o.title)}</span> 201 - <span class="o-author">${escapeHtml(o.author || "")}</span> 202 - </span> 203 - </a> 204 - </li>`).join(""); 205 - } catch (e) { 206 - list.innerHTML = `<li class="opinion-loading">failed to load: ${e.message}</li>`; 207 - } 208 - })(); 209 - 210 - function escapeHtml(s) { 211 - return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); 212 - } 213 - </script> 214 - 215 - </body> 216 - </html>
+4 -19
system/public/papers.aesthetic.computer/platter/jeffrey/index.html
··· 261 261 const html = list.map((r, i) => ` 262 262 <div class="card" data-i="${rows.indexOf(r)}"> 263 263 <div class="thumb-wrap"> 264 - <img src="${r.thumb}" loading="lazy" alt=""> 264 + <img src="${r.full || r.thumb}" loading="lazy" decoding="async" alt=""> 265 265 <div class="sim">${(r.similarity).toFixed(2)}</div> 266 266 </div> 267 267 <div class="meta"> ··· 278 278 279 279 function openLightbox(r) { 280 280 const igUrl = `https://www.instagram.com/p/${r.shortcode}/`; 281 - // Show the thumb instantly (it's likely cached from the grid), then swap 282 - // to the full-quality original once it loads. Avoids the lightbox flashing 283 - // empty for ~200ms on the network round-trip. 284 - const initial = r.thumb; 281 + // Same full-res image the grid uses — the browser already has it cached 282 + // from the card click, so the lightbox renders instantly. 285 283 const hires = r.full || r.thumb; 286 284 lbBody.innerHTML = ` 287 - <div class="lb-img"><img src="${initial}" data-hires="${hires}" alt="" style="filter:saturate(0.95);"></div> 285 + <div class="lb-img"><img src="${hires}" alt=""></div> 288 286 <div class="lb-info"> 289 287 <h2>${r.date}</h2> 290 288 <div class="lb-row"><span class="k">subject</span><span class="v">${escapeHtml(r.description || "—")}</span></div> ··· 301 299 </div>`; 302 300 lightbox.classList.add("open"); 303 301 document.body.style.overflow = "hidden"; 304 - 305 - // Progressive swap to full-quality once it loads. 306 - const lbImg = lbBody.querySelector(".lb-img img"); 307 - if (lbImg && lbImg.dataset.hires && lbImg.dataset.hires !== lbImg.src) { 308 - const probe = new Image(); 309 - probe.onload = () => { 310 - lbImg.src = lbImg.dataset.hires; 311 - lbImg.style.filter = ""; 312 - }; 313 - probe.src = lbImg.dataset.hires; 314 - } else { 315 - lbImg.style.filter = ""; 316 - } 317 302 } 318 303 319 304 function closeLightbox() {