Monorepo for Aesthetic.Computer
aesthetic.computer
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});