Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: response cache, Full SSL, deploy webhook, ac-deploy command

- Response cache for hot endpoints (version 4s→11ms, mood 100ms→10ms)
- Caddy now serves HTTPS with origin cert, all 8 zones set to Full SSL
- /lith/deploy webhook endpoint for auto-deploy on git push
- ac-deploy fish function: push + pull + restart in one command
- Journal log rotation: 500MB max, 7 day retention
- Cloudflare edge cache rules added to all 8 zones
- store-kidlisp?code=blank 404 is expected (JS piece, not KidLisp)

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

+96 -1
+27
.devcontainer/config.fish
··· 1957 1957 node --watch server.mjs 1958 1958 end 1959 1959 1960 + function ac-deploy --description 'Deploy lith: push + pull + restart on droplet' 1961 + cd ~/aesthetic-computer 1962 + set LITH_IP "209.38.133.33" 1963 + 1964 + echo "🪨 Deploying to lith ($LITH_IP)..." 1965 + 1966 + # Push local changes if any 1967 + set -l status_output (git status --porcelain) 1968 + if test -n "$status_output" 1969 + echo "⚠️ You have uncommitted changes. Commit first." 1970 + return 1 1971 + end 1972 + 1973 + echo "📤 Pushing to origin..." 1974 + git push origin main; or begin; echo "❌ Push failed"; return 1; end 1975 + 1976 + echo "🔄 Pulling on droplet..." 1977 + ssh -o StrictHostKeyChecking=no root@$LITH_IP "cd /opt/ac && git pull origin main && git rev-parse --short HEAD > system/public/.commit-ref && cat system/public/.commit-ref" 1978 + 1979 + echo "🔁 Restarting lith..." 1980 + ssh root@$LITH_IP "systemctl restart lith" 1981 + 1982 + sleep 3 1983 + set -l version (curl -s "https://aesthetic.computer/api/version" | python3 -c "import json,sys; print(json.load(sys.stdin)['deployed'])" 2>/dev/null) 1984 + echo "✅ Deployed: $version" 1985 + end 1986 + 1960 1987 function ac-media 1961 1988 cd ~/aesthetic-computer/system 1962 1989 echo "📦 Starting Caddy media server on :8111..."
+6 -1
lith/Caddyfile
··· 2 2 # Cloudflare handles TLS. Caddy serves HTTP on :80. 3 3 4 4 # --- papers.aesthetic.computer --- 5 - :80 { 5 + :443 { 6 + tls /etc/caddy/origin-cert.pem /etc/caddy/origin-key.pem 6 7 # --- Global performance headers --- 7 8 # Cache static assets (1h fresh, serve stale for 24h while revalidating) 8 9 @cacheable path *.mjs *.js *.css *.woff2 *.woff *.ttf *.png *.jpg *.jpeg *.svg *.gif *.webp *.ico *.mp3 *.wav *.mp4 *.json ··· 343 344 reverse_proxy localhost:8888 344 345 } 345 346 } 347 + 348 + :80 { 349 + redir https://{host}{uri} 301 350 + }
+63
lith/server.mjs
··· 59 59 const app = express(); 60 60 const BOOT_TIME = Date.now(); 61 61 62 + // --- Response cache for hot GET endpoints --- 63 + const responseCache = new Map(); // key → { body, headers, statusCode, expires } 64 + const CACHE_TTLS = { 65 + "handle-colors": 60_000, // 1 min (colors rarely change) 66 + "version": 30_000, // 30s (git state) 67 + "handles": 60_000, // 1 min 68 + "mood": 30_000, // 30s 69 + "tv": 30_000, // 30s 70 + "keeps-config": 300_000, // 5 min (contract addresses) 71 + "kidlisp-count": 60_000, // 1 min 72 + "playlist": 60_000, // 1 min 73 + "clock": 0, // never cache (it's a clock) 74 + }; 75 + 76 + // Clean expired entries every 30s 77 + setInterval(() => { 78 + const now = Date.now(); 79 + for (const [k, v] of responseCache) { 80 + if (v.expires < now) responseCache.delete(k); 81 + } 82 + }, 30_000); 83 + 62 84 // --- Function stats & error log --- 63 85 const fnStats = {}; // { fnName: { calls, errors, totalMs, lastCall, lastError } } 64 86 const errorLog = []; // [{ time, fn, status, error, path, method }] ··· 178 200 return res.status(404).send("Function not found: " + name); 179 201 } 180 202 203 + // Check response cache (GET only, with matching query string) 204 + const ttl = CACHE_TTLS[name]; 205 + if (ttl && req.method === "GET") { 206 + const cacheKey = `${name}:${req.originalUrl}`; 207 + const cached = responseCache.get(cacheKey); 208 + if (cached && cached.expires > Date.now()) { 209 + recordCall(name, 0, cached.statusCode, req.path, req.method, null); 210 + if (cached.headers) res.set(cached.headers); 211 + res.set("X-Lith-Cache", "HIT"); 212 + return res.status(cached.statusCode).send(cached.body); 213 + } 214 + } 215 + 181 216 const t0 = Date.now(); 182 217 try { 183 218 const event = toEvent(req); ··· 209 244 return pump().catch((err) => { 210 245 console.error(`fn/${name} stream error:`, err); 211 246 res.end(); 247 + }); 248 + } 249 + 250 + // Store in cache if cacheable 251 + if (ttl && req.method === "GET" && statusCode < 400) { 252 + const cacheKey = `${name}:${req.originalUrl}`; 253 + responseCache.set(cacheKey, { 254 + body: result.isBase64Encoded ? Buffer.from(result.body, "base64") : result.body, 255 + headers: result.headers, 256 + statusCode, 257 + expires: Date.now() + ttl, 212 258 }); 213 259 } 214 260 ··· 275 321 req.params.fn = resolveFunction(req); 276 322 return handleFunction(req, res); 277 323 } 324 + 325 + // --- Deploy webhook (POST /lith/deploy?secret=...) --- 326 + import { execSync } from "child_process"; 327 + const DEPLOY_SECRET = process.env.DEPLOY_SECRET || ""; 328 + 329 + app.post("/lith/deploy", (req, res) => { 330 + const secret = req.query.secret || req.headers["x-deploy-secret"]; 331 + if (!DEPLOY_SECRET || secret !== DEPLOY_SECRET) { 332 + return res.status(401).send("Unauthorized"); 333 + } 334 + try { 335 + const out = execSync("/opt/ac/lith/webhook.sh", { timeout: 30000, encoding: "utf-8" }); 336 + res.send(out); 337 + } catch (err) { 338 + res.status(500).send(err.message); 339 + } 340 + }); 278 341 279 342 // --- Routes --- 280 343