Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 161 lines 4.7 kB view raw
1#!/usr/bin/env node 2// KidLisp Log Viewer (admin-only) 3// Pulls client-side telemetry from /api/kidlisp-log and /api/boot-log 4// 5// Auth: set AC_ADMIN_TOKEN env var or pass --token <bearer-token> 6// 7// Usage: 8// ./logs.mjs # recent kidlisp logs (default 20) 9// ./logs.mjs --limit 50 # last 50 entries 10// ./logs.mjs --type gpu-disabled # filter by log type 11// ./logs.mjs --since 2026-03-20 # logs since date 12// ./logs.mjs --boots # boot telemetry instead 13// ./logs.mjs --boots --errors # only boot errors 14// ./logs.mjs --stats # type breakdown counts 15// ./logs.mjs --token <token> # pass auth token inline 16 17const BASE = process.env.AC_API_URL || "https://aesthetic.computer"; 18 19const args = process.argv.slice(2); 20function flag(name) { 21 return args.includes(`--${name}`); 22} 23function opt(name, fallback) { 24 const i = args.indexOf(`--${name}`); 25 return i !== -1 && args[i + 1] ? args[i + 1] : fallback; 26} 27 28const token = opt("token", null) || process.env.AC_ADMIN_TOKEN; 29if (!token) { 30 console.error( 31 "Admin token required. Set AC_ADMIN_TOKEN or pass --token <bearer-token>", 32 ); 33 process.exit(1); 34} 35 36const authHeaders = { Authorization: `Bearer ${token}` }; 37 38async function main() { 39 if (flag("boots")) { 40 await fetchBoots(); 41 } else { 42 await fetchKidlispLogs(); 43 } 44} 45 46async function fetchKidlispLogs() { 47 const limit = opt("limit", "20"); 48 const type = opt("type", null); 49 const since = opt("since", null); 50 const ua = opt("ua", null); 51 52 const params = new URLSearchParams({ limit }); 53 if (type) params.set("type", type); 54 if (since) params.set("since", since); 55 if (ua) params.set("ua", ua); 56 57 const url = `${BASE}/api/kidlisp-log?${params}`; 58 const res = await fetch(url, { headers: authHeaders }); 59 if (!res.ok) { 60 console.error(`Error ${res.status}: ${await res.text()}`); 61 process.exit(1); 62 } 63 64 const { logs, stats } = await res.json(); 65 66 if (flag("stats")) { 67 console.log("\n Type breakdown:"); 68 for (const s of stats) { 69 console.log(` ${s._id}: ${s.count}`); 70 } 71 console.log(); 72 return; 73 } 74 75 if (logs.length === 0) { 76 console.log("No logs found."); 77 return; 78 } 79 80 for (const log of logs) { 81 const date = new Date(log.createdAt).toLocaleString(); 82 const device = log.device?.userAgent?.slice(0, 60) || "unknown"; 83 const country = log.server?.country || "??"; 84 const detail = log.detail 85 ? typeof log.detail === "object" 86 ? JSON.stringify(log.detail) 87 : log.detail 88 : ""; 89 console.log( 90 `[${date}] ${log.type || "?"} ${country} ${log.effect || ""} ${detail}`, 91 ); 92 console.log(` device: ${device}`); 93 if (log.gpuStatus) { 94 const failed = Object.entries(log.gpuStatus) 95 .filter(([, v]) => v > 0) 96 .map(([k, v]) => `${k}:${v}`); 97 if (failed.length) console.log(` gpu failures: ${failed.join(", ")}`); 98 } 99 } 100 console.log(`\n Showing ${logs.length} entries.`); 101} 102 103async function fetchBoots() { 104 const limit = opt("limit", "30"); 105 const params = new URLSearchParams({ limit }); 106 107 const url = `${BASE}/api/boot-log?${params}`; 108 const res = await fetch(url, { headers: authHeaders }); 109 if (!res.ok) { 110 console.error(`Error ${res.status}: ${await res.text()}`); 111 process.exit(1); 112 } 113 114 const { boots } = await res.json(); 115 116 const errorsOnly = flag("errors"); 117 const filtered = errorsOnly 118 ? boots.filter((b) => b.status === "error") 119 : boots; 120 121 if (filtered.length === 0) { 122 console.log(errorsOnly ? "No boot errors found." : "No boot logs found."); 123 return; 124 } 125 126 for (const boot of filtered) { 127 const date = new Date(boot.createdAt).toLocaleString(); 128 const status = boot.status || "?"; 129 const host = boot.meta?.host || "?"; 130 const path = boot.meta?.path || "/"; 131 const country = boot.server?.country || "??"; 132 133 const icon = 134 status === "error" ? "X" : status === "success" ? "+" : "-"; 135 136 console.log(`[${icon}] ${date} ${status} ${country} ${host}${path}`); 137 138 if (boot.error) { 139 const errMsg = 140 typeof boot.error === "string" 141 ? boot.error 142 : boot.error.message || JSON.stringify(boot.error).slice(0, 120); 143 console.log(` error: ${errMsg}`); 144 } 145 146 if (boot.events?.length) { 147 const errEvents = boot.events.filter( 148 (e) => e.level === "error" || e.label?.includes("error"), 149 ); 150 for (const ev of errEvents) { 151 console.log(` event: ${ev.label || ev.message || JSON.stringify(ev)}`); 152 } 153 } 154 } 155 console.log(`\n Showing ${filtered.length} of ${boots.length} entries.`); 156} 157 158main().catch((err) => { 159 console.error(err); 160 process.exit(1); 161});