@apitrail/studio 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 apitrail contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @apitrail/studio
2
+
3
+ > Standalone dev dashboard for [apitrail](https://apitrail.io). Run `pnpm dlx @apitrail/studio --db $DATABASE_URL` and get a real-time UI on `localhost:4545`.
4
+
5
+ Think of this as **Prisma Studio for your API logs**. No embedding, no auth gymnastics, no production weight — just a beautiful standalone tool for monitoring, debugging, and exploring what your Next.js app is doing.
6
+
7
+ ## Why standalone?
8
+
9
+ The embeddable `@apitrail/dashboard` is Server-Component-only (no client interactivity, limited styling flexibility). `@apitrail/studio` is the opposite:
10
+
11
+ - **Full SPA** — React 19, client-side state, URL filters, polling refresh, eventually live tail over SSE
12
+ - **Its own server** — Hono, tiny JSON API, no Next.js weight
13
+ - **Not in your app bundle** — ship `apitrail` core to prod, run studio only in dev / ops
14
+ - **Works with any framework** — as long as the data lives in `apitrail_spans`
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ # One-off via dlx (no global install)
20
+ pnpm dlx @apitrail/studio --db $DATABASE_URL
21
+
22
+ # Install globally and run
23
+ pnpm add -g @apitrail/studio
24
+ apitrail-studio --db postgres://…
25
+ ```
26
+
27
+ Output:
28
+
29
+ ```
30
+ ● apitrail studio v0.1.0-alpha.0
31
+ → http://127.0.0.1:4545
32
+ Press Ctrl+C to stop.
33
+ ```
34
+
35
+ ## Options
36
+
37
+ | Flag | Default | Description |
38
+ |---|---|---|
39
+ | `--db <url>` | `$APITRAIL_DATABASE_URL`, `$DATABASE_URL`, `$POSTGRES_URL` | Connection string |
40
+ | `--port <n>` | `4545` | HTTP port |
41
+ | `--host <addr>` | `127.0.0.1` | Bind address. Use `0.0.0.0` for LAN |
42
+ | `--table <name>` | `apitrail_spans` | Span table to read from |
43
+ | `--no-ssl` | — | Disable SSL on the pg connection |
44
+ | `--no-open` | — | Don't auto-open the browser |
45
+ | `--dev` | — | Enable CORS, skip UI serving (only JSON API — used when developing studio itself) |
46
+
47
+ ## Features (v0.1)
48
+
49
+ - **Overview** — requests/24h, errors, slow, p50, p95 (polling every 10 s)
50
+ - **Requests explorer** — method / status-class / path filters, sticky top, selectable rows
51
+ - **Trace detail** — side drawer with meta, waterfall of child spans, request/response headers + bodies (JSON pretty-printed)
52
+ - **Dark, fast, keyboard-friendly** — no framework overhead on the wire
53
+
54
+ ## Roadmap (v0.2+)
55
+
56
+ - Live tail via SSE
57
+ - Full-text search in bodies
58
+ - Error grouping by fingerprint
59
+ - Endpoint analytics (per-path latency percentiles)
60
+ - Saved filter presets in URL
61
+ - Auth for remote deployments (`--auth-basic user:pass` or an `--auth-token`)
62
+
63
+ ## Security
64
+
65
+ Studio has **no built-in auth**. Default binding is `127.0.0.1` so only your machine can reach it. If you bind to `0.0.0.0` or run it remotely, put it behind a reverse proxy with your own auth — it reads raw request bodies and headers from apitrail, which may include sensitive data even after the masking layer.
66
+
67
+ ## License
68
+
69
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createRequire } from "module";
5
+ import { parseArgs } from "util";
6
+
7
+ // src/server/index.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { dirname, join, resolve } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { serve } from "@hono/node-server";
12
+ import { serveStatic } from "@hono/node-server/serve-static";
13
+ import { Hono } from "hono";
14
+ import { cors } from "hono/cors";
15
+ import pg from "pg";
16
+
17
+ // src/server/queries.ts
18
+ function qi(name) {
19
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
20
+ throw new Error(`Invalid identifier: ${JSON.stringify(name)}`);
21
+ }
22
+ return `"${name}"`;
23
+ }
24
+ async function getOverview(pool, table) {
25
+ const t = qi(table);
26
+ const { rows } = await pool.query(`
27
+ SELECT
28
+ count(*) FILTER (WHERE kind = 'SERVER')::text AS total_24h,
29
+ count(*) FILTER (WHERE kind = 'SERVER' AND status_code >= 400)::text AS errors_24h,
30
+ count(*) FILTER (WHERE kind = 'SERVER' AND duration_ms > 500)::text AS slow_24h,
31
+ percentile_disc(0.5) WITHIN GROUP (ORDER BY duration_ms) FILTER (WHERE kind = 'SERVER') AS p50,
32
+ percentile_disc(0.95) WITHIN GROUP (ORDER BY duration_ms) FILTER (WHERE kind = 'SERVER') AS p95
33
+ FROM ${t}
34
+ WHERE created_at > now() - interval '24 hours'
35
+ `);
36
+ return rows[0] ?? { total_24h: "0", errors_24h: "0", slow_24h: "0", p50: null, p95: null };
37
+ }
38
+ async function getSpans(pool, table, filter) {
39
+ const t = qi(table);
40
+ const conds = [`kind = 'SERVER'`];
41
+ const params = [];
42
+ if (filter.method) {
43
+ params.push(filter.method.toUpperCase());
44
+ conds.push(`method = $${params.length}`);
45
+ }
46
+ if (filter.minStatus !== void 0) {
47
+ params.push(filter.minStatus);
48
+ conds.push(`status_code >= $${params.length}`);
49
+ }
50
+ if (filter.maxStatus !== void 0) {
51
+ params.push(filter.maxStatus);
52
+ conds.push(`status_code <= $${params.length}`);
53
+ }
54
+ if (filter.pathLike) {
55
+ params.push(`%${filter.pathLike}%`);
56
+ conds.push(`path ILIKE $${params.length}`);
57
+ }
58
+ const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
59
+ params.push(limit);
60
+ const { rows } = await pool.query(
61
+ `SELECT span_id, trace_id, method, path, route, status_code, duration_ms,
62
+ start_time, created_at, error_message, user_agent, client_ip, runtime
63
+ FROM ${t}
64
+ WHERE ${conds.join(" AND ")}
65
+ ORDER BY created_at DESC
66
+ LIMIT $${params.length}`,
67
+ params
68
+ );
69
+ return rows;
70
+ }
71
+ async function getTrace(pool, table, traceId) {
72
+ const t = qi(table);
73
+ const [rootRes, childRes] = await Promise.all([
74
+ pool.query(
75
+ `SELECT *
76
+ FROM ${t}
77
+ WHERE trace_id = $1 AND kind = 'SERVER'
78
+ LIMIT 1`,
79
+ [traceId]
80
+ ),
81
+ pool.query(
82
+ `SELECT span_id, parent_span_id, name, kind, status, start_time, duration_ms, error_message
83
+ FROM ${t}
84
+ WHERE trace_id = $1 AND kind != 'SERVER'
85
+ ORDER BY start_time ASC`,
86
+ [traceId]
87
+ )
88
+ ]);
89
+ return { root: rootRes.rows[0] ?? null, children: childRes.rows };
90
+ }
91
+
92
+ // src/server/index.ts
93
+ var OVERVIEW_CACHE_MS = 1500;
94
+ async function startServer(opts) {
95
+ const { Pool } = pg;
96
+ const pool = new Pool({
97
+ connectionString: opts.connectionString,
98
+ ssl: opts.ssl ? { rejectUnauthorized: false } : false,
99
+ max: 5
100
+ });
101
+ try {
102
+ await pool.query("SELECT 1");
103
+ } catch (err) {
104
+ await pool.end().catch(() => {
105
+ });
106
+ const e = err;
107
+ throw new Error(`Could not connect to Postgres: ${e.message}`);
108
+ }
109
+ const tableName = opts.tableName ?? "apitrail_spans";
110
+ const app = new Hono();
111
+ if (opts.dev) app.use("*", cors());
112
+ app.get("/api/health", (c2) => c2.json({ ok: true }));
113
+ let overviewCache = null;
114
+ app.get("/api/overview", async (c2) => {
115
+ try {
116
+ if (overviewCache && Date.now() - overviewCache.at < OVERVIEW_CACHE_MS) {
117
+ return c2.json(overviewCache.data);
118
+ }
119
+ const raw = await getOverview(pool, tableName);
120
+ const total = Number(raw.total_24h);
121
+ const data = {
122
+ total_24h: total,
123
+ errors_24h: Number(raw.errors_24h),
124
+ slow_24h: Number(raw.slow_24h),
125
+ p50: Number(raw.p50 ?? 0),
126
+ p95: Number(raw.p95 ?? 0),
127
+ rpm: total / (24 * 60)
128
+ };
129
+ overviewCache = { at: Date.now(), data };
130
+ return c2.json(data);
131
+ } catch (err) {
132
+ return c2.json({ error: err.message }, 500);
133
+ }
134
+ });
135
+ app.get("/api/spans", async (c2) => {
136
+ try {
137
+ const q = c2.req.query();
138
+ const filter = {
139
+ limit: q.limit ? Number(q.limit) : void 0,
140
+ method: q.method || void 0,
141
+ minStatus: q.minStatus ? Number(q.minStatus) : void 0,
142
+ maxStatus: q.maxStatus ? Number(q.maxStatus) : void 0,
143
+ pathLike: q.pathLike || void 0
144
+ };
145
+ const rows = await getSpans(pool, tableName, filter);
146
+ return c2.json(rows);
147
+ } catch (err) {
148
+ return c2.json({ error: err.message }, 500);
149
+ }
150
+ });
151
+ app.get("/api/trace/:id", async (c2) => {
152
+ try {
153
+ const id = c2.req.param("id");
154
+ if (!/^[0-9a-f]{32}$/i.test(id)) {
155
+ return c2.json({ error: "invalid trace id" }, 400);
156
+ }
157
+ const data = await getTrace(pool, tableName, id);
158
+ return c2.json(data);
159
+ } catch (err) {
160
+ return c2.json({ error: err.message }, 500);
161
+ }
162
+ });
163
+ const staticRoot = resolveUiDir(opts.uiDir);
164
+ if (staticRoot && existsSync(staticRoot)) {
165
+ app.use(
166
+ "/*",
167
+ serveStatic({
168
+ root: staticRoot,
169
+ rewriteRequestPath: (p) => p
170
+ })
171
+ );
172
+ const indexHtml = join(staticRoot, "index.html");
173
+ if (existsSync(indexHtml)) {
174
+ const html = readFileSync(indexHtml, "utf8");
175
+ app.get("*", (c2) => c2.html(html));
176
+ }
177
+ }
178
+ const server = serve({
179
+ fetch: app.fetch,
180
+ port: opts.port,
181
+ hostname: opts.host
182
+ });
183
+ const url = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
184
+ return {
185
+ url,
186
+ async stop() {
187
+ server.close();
188
+ await pool.end().catch(() => {
189
+ });
190
+ }
191
+ };
192
+ }
193
+ function resolveUiDir(override) {
194
+ if (override) return resolve(override);
195
+ const here = dirname(fileURLToPath(import.meta.url));
196
+ const candidate = join(here, "ui");
197
+ return candidate;
198
+ }
199
+
200
+ // src/cli.ts
201
+ var require2 = createRequire(import.meta.url);
202
+ var pkg = require2("../package.json");
203
+ var HELP = `
204
+ apitrail studio v${pkg.version} \u2014 standalone dashboard for apitrail
205
+
206
+ Usage:
207
+ apitrail-studio [options]
208
+ pnpm dlx @apitrail/studio [options]
209
+
210
+ Options:
211
+ --db <url> Postgres connection string (or APITRAIL_DATABASE_URL / DATABASE_URL)
212
+ --port <n> HTTP port (default: 4545)
213
+ --host <addr> Bind address (default: 127.0.0.1)
214
+ --table <name> Table name (default: apitrail_spans)
215
+ --no-ssl Disable SSL
216
+ --no-open Don't auto-open the browser
217
+ --dev Development mode (adds CORS, skips UI serving)
218
+ -v, --version Print version
219
+ -h, --help Show this help
220
+
221
+ Environment:
222
+ APITRAIL_DATABASE_URL Preferred
223
+ DATABASE_URL Fallback
224
+ POSTGRES_URL Fallback
225
+ NO_COLOR=1 Disable colors
226
+ `;
227
+ var c = (open, close) => (s) => {
228
+ if (process.env.NO_COLOR) return s;
229
+ if (!process.stdout.isTTY) return s;
230
+ return `\x1B[${open}m${s}\x1B[${close}m`;
231
+ };
232
+ var bold = c(1, 22);
233
+ var dim = c(2, 22);
234
+ var violet = c(35, 39);
235
+ var green = c(32, 39);
236
+ var red = c(31, 39);
237
+ async function main() {
238
+ const { values } = parseArgs({
239
+ args: process.argv.slice(2),
240
+ options: {
241
+ db: { type: "string" },
242
+ port: { type: "string", default: "4545" },
243
+ host: { type: "string", default: "127.0.0.1" },
244
+ table: { type: "string", default: "apitrail_spans" },
245
+ ssl: { type: "boolean", default: true },
246
+ "no-ssl": { type: "boolean", default: false },
247
+ open: { type: "boolean", default: true },
248
+ "no-open": { type: "boolean", default: false },
249
+ dev: { type: "boolean", default: false },
250
+ version: { type: "boolean", short: "v", default: false },
251
+ help: { type: "boolean", short: "h", default: false }
252
+ },
253
+ allowPositionals: false
254
+ });
255
+ if (values.help) {
256
+ console.log(HELP);
257
+ return;
258
+ }
259
+ if (values.version) {
260
+ console.log(pkg.version);
261
+ return;
262
+ }
263
+ const connectionString = values.db ?? process.env.APITRAIL_DATABASE_URL ?? process.env.DATABASE_URL ?? process.env.POSTGRES_URL;
264
+ if (!connectionString) {
265
+ console.error(red("error:"), "no database URL.");
266
+ console.error("Set APITRAIL_DATABASE_URL / DATABASE_URL, or pass --db.");
267
+ process.exit(1);
268
+ }
269
+ const port = Number(values.port);
270
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
271
+ console.error(red("error:"), `invalid port: ${values.port}`);
272
+ process.exit(1);
273
+ }
274
+ const ssl = !values["no-ssl"];
275
+ const openBrowser = !values["no-open"];
276
+ try {
277
+ const { url, stop } = await startServer({
278
+ connectionString,
279
+ port,
280
+ host: values.host ?? "127.0.0.1",
281
+ tableName: values.table,
282
+ ssl,
283
+ dev: values.dev
284
+ });
285
+ console.log();
286
+ console.log(` ${violet("\u25CF")} ${bold("apitrail studio")} ${dim(`v${pkg.version}`)}`);
287
+ console.log(` ${green("\u2192")} ${url}`);
288
+ console.log(` ${dim("Press Ctrl+C to stop.")}`);
289
+ console.log();
290
+ if (openBrowser && !values.dev) {
291
+ try {
292
+ const open = (await import("open")).default;
293
+ await open(url);
294
+ } catch {
295
+ }
296
+ }
297
+ const shutdown = async () => {
298
+ console.log(`
299
+ ${dim("shutting down\u2026")}`);
300
+ await stop();
301
+ process.exit(0);
302
+ };
303
+ process.once("SIGINT", () => void shutdown());
304
+ process.once("SIGTERM", () => void shutdown());
305
+ } catch (err) {
306
+ console.error(red("error:"), err.message);
307
+ if (process.env.APITRAIL_DEBUG) console.error(err.stack);
308
+ process.exit(1);
309
+ }
310
+ }
311
+ main();
312
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/server/index.ts","../src/server/queries.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport { parseArgs } from 'node:util'\nimport { startServer } from './server/index.js'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('../package.json') as { version: string }\n\nconst HELP = `\napitrail studio v${pkg.version} — standalone dashboard for apitrail\n\nUsage:\n apitrail-studio [options]\n pnpm dlx @apitrail/studio [options]\n\nOptions:\n --db <url> Postgres connection string (or APITRAIL_DATABASE_URL / DATABASE_URL)\n --port <n> HTTP port (default: 4545)\n --host <addr> Bind address (default: 127.0.0.1)\n --table <name> Table name (default: apitrail_spans)\n --no-ssl Disable SSL\n --no-open Don't auto-open the browser\n --dev Development mode (adds CORS, skips UI serving)\n -v, --version Print version\n -h, --help Show this help\n\nEnvironment:\n APITRAIL_DATABASE_URL Preferred\n DATABASE_URL Fallback\n POSTGRES_URL Fallback\n NO_COLOR=1 Disable colors\n`\n\nconst c = (open: number, close: number) => (s: string) => {\n if (process.env.NO_COLOR) return s\n if (!process.stdout.isTTY) return s\n return `\\x1b[${open}m${s}\\x1b[${close}m`\n}\nconst bold = c(1, 22)\nconst dim = c(2, 22)\nconst violet = c(35, 39)\nconst green = c(32, 39)\nconst red = c(31, 39)\n\nasync function main(): Promise<void> {\n const { values } = parseArgs({\n args: process.argv.slice(2),\n options: {\n db: { type: 'string' },\n port: { type: 'string', default: '4545' },\n host: { type: 'string', default: '127.0.0.1' },\n table: { type: 'string', default: 'apitrail_spans' },\n ssl: { type: 'boolean', default: true },\n 'no-ssl': { type: 'boolean', default: false },\n open: { type: 'boolean', default: true },\n 'no-open': { type: 'boolean', default: false },\n dev: { type: 'boolean', default: false },\n version: { type: 'boolean', short: 'v', default: false },\n help: { type: 'boolean', short: 'h', default: false },\n },\n allowPositionals: false,\n })\n\n if (values.help) {\n console.log(HELP)\n return\n }\n if (values.version) {\n console.log(pkg.version)\n return\n }\n\n const connectionString =\n values.db ??\n process.env.APITRAIL_DATABASE_URL ??\n process.env.DATABASE_URL ??\n process.env.POSTGRES_URL\n\n if (!connectionString) {\n console.error(red('error:'), 'no database URL.')\n console.error('Set APITRAIL_DATABASE_URL / DATABASE_URL, or pass --db.')\n process.exit(1)\n }\n\n const port = Number(values.port)\n if (!Number.isFinite(port) || port < 1 || port > 65535) {\n console.error(red('error:'), `invalid port: ${values.port}`)\n process.exit(1)\n }\n\n const ssl = !values['no-ssl']\n const openBrowser = !values['no-open']\n\n try {\n const { url, stop } = await startServer({\n connectionString,\n port,\n host: values.host ?? '127.0.0.1',\n tableName: values.table,\n ssl,\n dev: values.dev,\n })\n\n console.log()\n console.log(` ${violet('●')} ${bold('apitrail studio')} ${dim(`v${pkg.version}`)}`)\n console.log(` ${green('→')} ${url}`)\n console.log(` ${dim('Press Ctrl+C to stop.')}`)\n console.log()\n\n if (openBrowser && !values.dev) {\n try {\n const open = (await import('open')).default\n await open(url)\n } catch {\n // Fails silently in CI/headless — user can still visit the URL.\n }\n }\n\n const shutdown = async (): Promise<void> => {\n console.log(`\\n${dim('shutting down…')}`)\n await stop()\n process.exit(0)\n }\n process.once('SIGINT', () => void shutdown())\n process.once('SIGTERM', () => void shutdown())\n } catch (err) {\n console.error(red('error:'), (err as Error).message)\n if (process.env.APITRAIL_DEBUG) console.error((err as Error).stack)\n process.exit(1)\n }\n}\n\nmain()\n","import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { serve } from '@hono/node-server'\nimport { serveStatic } from '@hono/node-server/serve-static'\nimport { Hono } from 'hono'\nimport { cors } from 'hono/cors'\nimport pg from 'pg'\nimport { type SpansFilter, getOverview, getSpans, getTrace } from './queries.js'\n\nexport interface ServerOptions {\n connectionString: string\n port: number\n host: string\n tableName?: string\n ssl?: boolean\n uiDir?: string\n dev?: boolean\n}\n\nconst OVERVIEW_CACHE_MS = 1500 // tiny cache so rapid polling doesn't hammer PG\n\nexport async function startServer(opts: ServerOptions): Promise<{\n url: string\n stop: () => Promise<void>\n}> {\n const { Pool } = pg\n const pool = new Pool({\n connectionString: opts.connectionString,\n ssl: opts.ssl ? { rejectUnauthorized: false } : false,\n max: 5,\n })\n\n // Verify connection upfront — fail fast with a clear error.\n try {\n await pool.query('SELECT 1')\n } catch (err) {\n await pool.end().catch(() => {})\n const e = err as Error\n throw new Error(`Could not connect to Postgres: ${e.message}`)\n }\n\n const tableName = opts.tableName ?? 'apitrail_spans'\n\n const app = new Hono()\n if (opts.dev) app.use('*', cors())\n\n app.get('/api/health', (c) => c.json({ ok: true }))\n\n let overviewCache: { at: number; data: unknown } | null = null\n app.get('/api/overview', async (c) => {\n try {\n if (overviewCache && Date.now() - overviewCache.at < OVERVIEW_CACHE_MS) {\n return c.json(overviewCache.data)\n }\n const raw = await getOverview(pool, tableName)\n const total = Number(raw.total_24h)\n const data = {\n total_24h: total,\n errors_24h: Number(raw.errors_24h),\n slow_24h: Number(raw.slow_24h),\n p50: Number(raw.p50 ?? 0),\n p95: Number(raw.p95 ?? 0),\n rpm: total / (24 * 60),\n }\n overviewCache = { at: Date.now(), data }\n return c.json(data)\n } catch (err) {\n return c.json({ error: (err as Error).message }, 500)\n }\n })\n\n app.get('/api/spans', async (c) => {\n try {\n const q = c.req.query()\n const filter: SpansFilter = {\n limit: q.limit ? Number(q.limit) : undefined,\n method: q.method || undefined,\n minStatus: q.minStatus ? Number(q.minStatus) : undefined,\n maxStatus: q.maxStatus ? Number(q.maxStatus) : undefined,\n pathLike: q.pathLike || undefined,\n }\n const rows = await getSpans(pool, tableName, filter)\n return c.json(rows)\n } catch (err) {\n return c.json({ error: (err as Error).message }, 500)\n }\n })\n\n app.get('/api/trace/:id', async (c) => {\n try {\n const id = c.req.param('id')\n if (!/^[0-9a-f]{32}$/i.test(id)) {\n return c.json({ error: 'invalid trace id' }, 400)\n }\n const data = await getTrace(pool, tableName, id)\n return c.json(data)\n } catch (err) {\n return c.json({ error: (err as Error).message }, 500)\n }\n })\n\n // Serve static UI assets from dist/ui/ — resolve robustly for both dev and\n // published-package layouts.\n const staticRoot = resolveUiDir(opts.uiDir)\n if (staticRoot && existsSync(staticRoot)) {\n app.use(\n '/*',\n serveStatic({\n root: staticRoot,\n rewriteRequestPath: (p) => p,\n }),\n )\n // SPA fallback → serve index.html on unknown routes\n const indexHtml = join(staticRoot, 'index.html')\n if (existsSync(indexHtml)) {\n const html = readFileSync(indexHtml, 'utf8')\n app.get('*', (c) => c.html(html))\n }\n }\n\n const server = serve({\n fetch: app.fetch,\n port: opts.port,\n hostname: opts.host,\n })\n\n const url = `http://${opts.host === '0.0.0.0' ? 'localhost' : opts.host}:${opts.port}`\n\n return {\n url,\n async stop() {\n server.close()\n await pool.end().catch(() => {})\n },\n }\n}\n\nfunction resolveUiDir(override?: string): string | null {\n if (override) return resolve(override)\n // When running from `dist/cli.js` inside the published package:\n // <pkg-root>/dist/cli.js → <pkg-root>/dist/ui/\n const here = dirname(fileURLToPath(import.meta.url))\n const candidate = join(here, 'ui')\n return candidate\n}\n","import type pg from 'pg'\n\nexport interface ServerRow {\n span_id: string\n trace_id: string\n method: string\n path: string\n route: string | null\n status_code: number | null\n duration_ms: string\n start_time: string\n created_at: string\n error_message: string | null\n user_agent: string | null\n client_ip: string | null\n runtime: string\n}\n\nexport interface DetailRow extends ServerRow {\n req_headers: Record<string, string> | null\n req_body: string | null\n res_headers: Record<string, string> | null\n res_body: string | null\n error_stack: string | null\n referer: string | null\n host: string | null\n service_name: string | null\n}\n\nexport interface ChildRow {\n span_id: string\n parent_span_id: string | null\n name: string\n kind: string\n status: string\n start_time: string\n duration_ms: string\n error_message: string | null\n}\n\nexport interface OverviewRow {\n total_24h: string\n errors_24h: string\n slow_24h: string\n p50: number | null\n p95: number | null\n}\n\nexport interface SpansFilter {\n limit?: number\n method?: string\n minStatus?: number\n maxStatus?: number\n pathLike?: string\n}\n\nfunction qi(name: string): string {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(`Invalid identifier: ${JSON.stringify(name)}`)\n }\n return `\"${name}\"`\n}\n\nexport async function getOverview(pool: pg.Pool, table: string): Promise<OverviewRow> {\n const t = qi(table)\n const { rows } = await pool.query<OverviewRow>(`\n SELECT\n count(*) FILTER (WHERE kind = 'SERVER')::text AS total_24h,\n count(*) FILTER (WHERE kind = 'SERVER' AND status_code >= 400)::text AS errors_24h,\n count(*) FILTER (WHERE kind = 'SERVER' AND duration_ms > 500)::text AS slow_24h,\n percentile_disc(0.5) WITHIN GROUP (ORDER BY duration_ms) FILTER (WHERE kind = 'SERVER') AS p50,\n percentile_disc(0.95) WITHIN GROUP (ORDER BY duration_ms) FILTER (WHERE kind = 'SERVER') AS p95\n FROM ${t}\n WHERE created_at > now() - interval '24 hours'\n `)\n return rows[0] ?? { total_24h: '0', errors_24h: '0', slow_24h: '0', p50: null, p95: null }\n}\n\nexport async function getSpans(\n pool: pg.Pool,\n table: string,\n filter: SpansFilter,\n): Promise<ServerRow[]> {\n const t = qi(table)\n const conds: string[] = [`kind = 'SERVER'`]\n const params: (string | number)[] = []\n\n if (filter.method) {\n params.push(filter.method.toUpperCase())\n conds.push(`method = $${params.length}`)\n }\n if (filter.minStatus !== undefined) {\n params.push(filter.minStatus)\n conds.push(`status_code >= $${params.length}`)\n }\n if (filter.maxStatus !== undefined) {\n params.push(filter.maxStatus)\n conds.push(`status_code <= $${params.length}`)\n }\n if (filter.pathLike) {\n params.push(`%${filter.pathLike}%`)\n conds.push(`path ILIKE $${params.length}`)\n }\n\n const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500)\n params.push(limit)\n\n const { rows } = await pool.query<ServerRow>(\n `SELECT span_id, trace_id, method, path, route, status_code, duration_ms,\n start_time, created_at, error_message, user_agent, client_ip, runtime\n FROM ${t}\n WHERE ${conds.join(' AND ')}\n ORDER BY created_at DESC\n LIMIT $${params.length}`,\n params,\n )\n return rows\n}\n\nexport async function getTrace(\n pool: pg.Pool,\n table: string,\n traceId: string,\n): Promise<{ root: DetailRow | null; children: ChildRow[] }> {\n const t = qi(table)\n const [rootRes, childRes] = await Promise.all([\n pool.query<DetailRow>(\n `SELECT *\n FROM ${t}\n WHERE trace_id = $1 AND kind = 'SERVER'\n LIMIT 1`,\n [traceId],\n ),\n pool.query<ChildRow>(\n `SELECT span_id, parent_span_id, name, kind, status, start_time, duration_ms, error_message\n FROM ${t}\n WHERE trace_id = $1 AND kind != 'SERVER'\n ORDER BY start_time ASC`,\n [traceId],\n ),\n ])\n return { root: rootRes.rows[0] ?? null, children: childRes.rows }\n}\n"],"mappings":"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","names":["c","require"]}
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:"Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono:"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--spacing:.25rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wider:.05em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#0a0a0b;--color-border:#26262a}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.right-0{right:calc(var(--spacing) * 0)}.z-30{z-index:30}.z-40{z-index:40}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.h-2{height:calc(var(--spacing) * 2)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-8{height:calc(var(--spacing) * 8)}.h-40{height:calc(var(--spacing) * 40)}.h-full{height:100%}.max-h-80{max-height:calc(var(--spacing) * 80)}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing) * 2)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-\[1600px\]{max-width:1600px}.min-w-\[40px\]{min-width:40px}.min-w-\[52px\]{min-width:52px}.flex-1{flex:1}.animate-ping{animation:var(--animate-ping)}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-amber-900\/50{border-color:#7b330680}@supports (color:color-mix(in lab,red,red)){.border-amber-900\/50{border-color:color-mix(in oklab,var(--color-amber-900) 50%,transparent)}}.border-emerald-900\/50{border-color:#004e3b80}@supports (color:color-mix(in lab,red,red)){.border-emerald-900\/50{border-color:color-mix(in oklab,var(--color-emerald-900) 50%,transparent)}}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-rose-900\/50{border-color:#8b083680}@supports (color:color-mix(in lab,red,red)){.border-rose-900\/50{border-color:color-mix(in oklab,var(--color-rose-900) 50%,transparent)}}.border-rose-900\/60{border-color:#8b083699}@supports (color:color-mix(in lab,red,red)){.border-rose-900\/60{border-color:color-mix(in oklab,var(--color-rose-900) 60%,transparent)}}.border-sky-900\/50{border-color:#024a7080}@supports (color:color-mix(in lab,red,red)){.border-sky-900\/50{border-color:color-mix(in oklab,var(--color-sky-900) 50%,transparent)}}.border-violet-900\/50{border-color:#4d179a80}@supports (color:color-mix(in lab,red,red)){.border-violet-900\/50{border-color:color-mix(in oklab,var(--color-violet-900) 50%,transparent)}}.bg-amber-950\/70{background-color:#461901b3}@supports (color:color-mix(in lab,red,red)){.bg-amber-950\/70{background-color:color-mix(in oklab,var(--color-amber-950) 70%,transparent)}}.bg-emerald-950\/70{background-color:#002c22b3}@supports (color:color-mix(in lab,red,red)){.bg-emerald-950\/70{background-color:color-mix(in oklab,var(--color-emerald-950) 70%,transparent)}}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-800\/60{background-color:#26262699}@supports (color:color-mix(in lab,red,red)){.bg-neutral-800\/60{background-color:color-mix(in oklab,var(--color-neutral-800) 60%,transparent)}}.bg-neutral-800\/80{background-color:#262626cc}@supports (color:color-mix(in lab,red,red)){.bg-neutral-800\/80{background-color:color-mix(in oklab,var(--color-neutral-800) 80%,transparent)}}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-900\/40{background-color:#17171766}@supports (color:color-mix(in lab,red,red)){.bg-neutral-900\/40{background-color:color-mix(in oklab,var(--color-neutral-900) 40%,transparent)}}.bg-neutral-900\/60{background-color:#17171799}@supports (color:color-mix(in lab,red,red)){.bg-neutral-900\/60{background-color:color-mix(in oklab,var(--color-neutral-900) 60%,transparent)}}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-neutral-950\/80{background-color:#0a0a0acc}@supports (color:color-mix(in lab,red,red)){.bg-neutral-950\/80{background-color:color-mix(in oklab,var(--color-neutral-950) 80%,transparent)}}.bg-rose-500\/70{background-color:#ff2357b3}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/70{background-color:color-mix(in oklab,var(--color-rose-500) 70%,transparent)}}.bg-rose-950\/30{background-color:#4d02184d}@supports (color:color-mix(in lab,red,red)){.bg-rose-950\/30{background-color:color-mix(in oklab,var(--color-rose-950) 30%,transparent)}}.bg-rose-950\/70{background-color:#4d0218b3}@supports (color:color-mix(in lab,red,red)){.bg-rose-950\/70{background-color:color-mix(in oklab,var(--color-rose-950) 70%,transparent)}}.bg-sky-950\/70{background-color:#052f4ab3}@supports (color:color-mix(in lab,red,red)){.bg-sky-950\/70{background-color:color-mix(in oklab,var(--color-sky-950) 70%,transparent)}}.bg-violet-400{background-color:var(--color-violet-400)}.bg-violet-500\/70{background-color:#8d54ffb3}@supports (color:color-mix(in lab,red,red)){.bg-violet-500\/70{background-color:color-mix(in oklab,var(--color-violet-500) 70%,transparent)}}.bg-violet-950\/70{background-color:#2f0d68b3}@supports (color:color-mix(in lab,red,red)){.bg-violet-950\/70{background-color:color-mix(in oklab,var(--color-violet-950) 70%,transparent)}}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-400\/80{color:#ff667fcc}@supports (color:color-mix(in lab,red,red)){.text-rose-400\/80{color:color-mix(in oklab,var(--color-rose-400) 80%,transparent)}}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-violet-300{color:var(--color-violet-300)}.uppercase{text-transform:uppercase}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-neutral-600::placeholder{color:var(--color-neutral-600)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media(hover:hover){.hover\:border-neutral-700:hover{border-color:var(--color-neutral-700)}.hover\:bg-neutral-800:hover{background-color:var(--color-neutral-800)}.hover\:bg-neutral-800\/40:hover{background-color:#26262666}@supports (color:color-mix(in lab,red,red)){.hover\:bg-neutral-800\/40:hover{background-color:color-mix(in oklab,var(--color-neutral-800) 40%,transparent)}}.hover\:text-neutral-300:hover{color:var(--color-neutral-300)}}.focus\:border-neutral-600:focus{border-color:var(--color-neutral-600)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:bg-neutral-800\/60:focus-visible{background-color:#26262699}@supports (color:color-mix(in lab,red,red)){.focus-visible\:bg-neutral-800\/60:focus-visible{background-color:color-mix(in oklab,var(--color-neutral-800) 60%,transparent)}}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-violet-500\/50:focus-visible{--tw-ring-color:#8d54ff80}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-violet-500\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-violet-500) 50%, transparent)}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}}:root{color-scheme:dark}html,body,#root{height:100%}body{background:var(--color-bg);color:#ededed;font-feature-settings:"cv11","ss01","ss03"}.tabular{font-variant-numeric:tabular-nums}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#2a2a2e;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#3a3a40}.code{font-family:var(--font-mono);border:1px solid var(--color-border);white-space:pre-wrap;word-break:break-word;background:#070708;border-radius:6px;padding:12px;font-size:12px;line-height:1.6;overflow-x:auto}.waterfall-row{grid-template-columns:minmax(180px,280px) 1fr 70px;align-items:center;gap:12px;padding:6px 0;font-size:12px;display:grid}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}