diff --git a/.gitignore b/.gitignore index 34099b5..2d15cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -250,3 +250,12 @@ website/.docusaurus/ # Claude Code local runtime state (scheduled task locks, session scratch). .claude/ + +# UI: React + Vite build outputs live under ui/. The Python `dist/` rule +# above would swallow ui/dist/, which we want committed so end users don't +# need Node just to view the UI. +ui/node_modules/ +ui/.vite/ +ui/tsconfig.tsbuildinfo +!ui/dist/ +!ui/dist/** diff --git a/bin/lmstack-ui b/bin/lmstack-ui new file mode 100755 index 0000000..4e5a529 --- /dev/null +++ b/bin/lmstack-ui @@ -0,0 +1,771 @@ +#!/usr/bin/env node +// lmstack-ui — read-only local web UI over ~/.lmstack/ +// +// Subcommands: start | stop | status | restart | open | foreground +// Serves ui/dist/ statically and a small JSON API at /api/* on 127.0.0.1. +// Node 20+; no npm deps at runtime. + +import http from 'node:http'; +import { existsSync, readFileSync, statSync, openSync, readSync, closeSync, unlinkSync, mkdirSync } from 'node:fs'; +import { readdir, readFile, stat as statAsync } from 'node:fs/promises'; +import { execFile, spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve, extname } from 'node:path'; +import { homedir } from 'node:os'; +import { promisify } from 'node:util'; + +const execFileP = promisify(execFile); + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(HERE, '..'); +const UI_DIST = join(REPO_ROOT, 'ui', 'dist'); + +const LMSTACK_HOME = process.env.LMSTACK_HOME || join(homedir(), '.lmstack'); +const PID_FILE = join(LMSTACK_HOME, 'ui.pid'); +const PORT_FILE = join(LMSTACK_HOME, 'ui.port'); +const LOG_FILE = join(LMSTACK_HOME, 'ui.log'); + +const DEFAULT_PORT = 7878; +const HOST = '127.0.0.1'; + +// ─── subcommand dispatch ──────────────────────────────────────────────────── + +const argv = process.argv.slice(2); +const cmd = argv[0] || 'status'; +const flags = parseFlags(argv.slice(1)); + +function parseFlags(args) { + const out = {}; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === '--port') out.port = parseInt(args[++i], 10); + else if (a === '--help' || a === '-h') out.help = true; + } + return out; +} + +function usage() { + process.stdout.write([ + 'lmstack-ui — local read-only web UI over ~/.lmstack/', + '', + 'usage: lmstack-ui [--port N]', + '', + 'commands:', + ' start run the server in the background (idempotent)', + ' stop stop the background server', + ' restart stop then start', + ' status print running/stopped and URL', + ' open status + open the URL in a browser', + ' foreground run in the foreground (Ctrl-C to stop)', + '', + ].join('\n')); +} + +switch (cmd) { + case 'help': + case '--help': + case '-h': usage(); process.exit(0); break; + case 'start': await cmdStart(); break; + case 'stop': await cmdStop(); break; + case 'restart': await cmdStop(); await cmdStart(); break; + case 'status': process.exit(await cmdStatus()); break; + case 'open': await cmdOpen(); break; + case 'foreground': await cmdForeground(); break; + case '__daemon': await cmdForeground(); break; // internal + default: usage(); process.exit(2); +} + +// ─── lifecycle ────────────────────────────────────────────────────────────── + +function readPid() { + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10); + return Number.isFinite(pid) ? pid : null; + } catch { return null; } +} + +function readPort() { + try { + const p = parseInt(readFileSync(PORT_FILE, 'utf8').trim(), 10); + return Number.isFinite(p) ? p : null; + } catch { return null; } +} + +function isAlive(pid) { + if (!pid) return false; + try { process.kill(pid, 0); return true; } catch { return false; } +} + +async function cmdStart() { + const running = readPid(); + if (isAlive(running)) { + const port = readPort() || DEFAULT_PORT; + process.stdout.write(`lmstack-ui already running (pid ${running}) at http://${HOST}:${port}/\n`); + return; + } + if (!existsSync(join(UI_DIST, 'index.html'))) { + process.stderr.write( + `lmstack-ui: SPA build missing at ${UI_DIST}/index.html\n` + + `run: cd ${join(REPO_ROOT, 'ui')} && npm install && npm run build\n` + ); + process.exit(1); + } + await ensureDir(LMSTACK_HOME); + + const port = flags.port || DEFAULT_PORT; + const out = openSync(LOG_FILE, 'a'); + const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '__daemon', '--port', String(port)], { + detached: true, + stdio: ['ignore', out, out], + env: { ...process.env, LMSTACK_UI_DAEMON: '1' }, + }); + child.unref(); + + // Wait briefly for the child to write PORT_FILE, then report. + const started = await waitFor(() => isAlive(child.pid) && readPort() != null, 3000); + if (!started) { + process.stderr.write(`lmstack-ui: server failed to start; see ${LOG_FILE}\n`); + process.exit(1); + } + const actualPort = readPort() || port; + process.stdout.write(`lmstack-ui started (pid ${child.pid}) at http://${HOST}:${actualPort}/\n`); +} + +async function cmdStop() { + const pid = readPid(); + if (!isAlive(pid)) { + process.stdout.write('lmstack-ui: not running\n'); + try { await rm(PID_FILE); await rm(PORT_FILE); } catch {} + return; + } + try { process.kill(pid, 'SIGTERM'); } catch {} + const gone = await waitFor(() => !isAlive(pid), 3000); + if (!gone) { + try { process.kill(pid, 'SIGKILL'); } catch {} + } + try { await rm(PID_FILE); await rm(PORT_FILE); } catch {} + process.stdout.write(`lmstack-ui stopped (pid ${pid})\n`); +} + +async function cmdStatus() { + const pid = readPid(); + if (isAlive(pid)) { + const port = readPort() || DEFAULT_PORT; + process.stdout.write(`running http://${HOST}:${port}/ (pid ${pid})\n`); + return 0; + } + process.stdout.write('stopped\n'); + return 1; +} + +async function cmdOpen() { + const rc = await cmdStatus(); + if (rc !== 0) { + process.stderr.write('lmstack-ui: not running; run `lmstack-ui start` first\n'); + process.exit(1); + } + const port = readPort() || DEFAULT_PORT; + const url = `http://${HOST}:${port}/`; + const opener = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open'); + spawn(opener, [url], { detached: true, stdio: 'ignore' }).unref(); +} + +async function cmdForeground() { + await ensureDir(LMSTACK_HOME); + const startPort = flags.port || DEFAULT_PORT; + const server = http.createServer(onRequest); + + const port = await listenOnFreePort(server, startPort, 10); + // eslint-disable-next-line no-console + console.log(`[${new Date().toISOString()}] listening http://${HOST}:${port}/`); + + await writeFile(PID_FILE, String(process.pid)); + await writeFile(PORT_FILE, String(port)); + + const cleanup = () => { + try { server.close(); } catch {} + try { unlinkSyncSafe(PID_FILE); } catch {} + try { unlinkSyncSafe(PORT_FILE); } catch {} + process.exit(0); + }; + process.on('SIGTERM', cleanup); + process.on('SIGINT', cleanup); +} + +function listenOnFreePort(server, start, tries) { + return new Promise((resolvePromise, reject) => { + let port = start; + let left = tries; + const tryOnce = () => { + const onError = (err) => { + if (err.code === 'EADDRINUSE' && left-- > 0) { port++; server.listen(port, HOST, onListen); } + else reject(err); + }; + const onListen = () => { server.off('error', onError); resolvePromise(port); }; + server.once('error', onError); + server.listen(port, HOST, onListen); + }; + tryOnce(); + }); +} + +// ─── HTTP request handler ─────────────────────────────────────────────────── + +async function onRequest(req, res) { + const started = Date.now(); + try { + const url = new URL(req.url || '/', `http://${HOST}`); + const p = url.pathname; + if (p.startsWith('/api/')) { + await routeApi(req, res, url); + } else { + await serveStatic(req, res, p); + } + } catch (err) { + replyError(res, 500, err instanceof Error ? err.message : String(err)); + } finally { + // eslint-disable-next-line no-console + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} ${res.statusCode} ${Date.now() - started}ms`); + } +} + +async function routeApi(req, res, url) { + const p = url.pathname; + if (p === '/api/instances') return replyJson(res, await getInstances()); + const instMatch = p.match(/^\/api\/instances\/([^/]+)$/); + if (instMatch) return replyJson(res, await getInstance(decodeURIComponent(instMatch[1]))); + if (p === '/api/forges') { + return replyJson(res, await getForges({ + role: url.searchParams.get('role') || undefined, + status: url.searchParams.get('status') || undefined, + })); + } + const logMatch = p.match(/^\/api\/forges\/(.+)\/log\/(exec|judge)$/); + if (logMatch) { + const key = decodeURIComponent(logMatch[1]); + const which = logMatch[2]; + const tail = parseInt(url.searchParams.get('tail') || '200', 10); + return replyJson(res, await getLog(key, which, tail)); + } + const forgeMatch = p.match(/^\/api\/forges\/(.+)$/); + if (forgeMatch) return replyJson(res, await getForge(decodeURIComponent(forgeMatch[1]))); + if (p === '/api/ledger') { + return replyJson(res, await getLedger({ + limit: parseInt(url.searchParams.get('limit') || '500', 10), + shape: url.searchParams.get('shape') || undefined, + outcome: url.searchParams.get('outcome') || undefined, + role: url.searchParams.get('role') || undefined, + })); + } + replyError(res, 404, 'not found'); +} + +// ─── static ───────────────────────────────────────────────────────────────── + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.ico': 'image/x-icon', + '.map': 'application/json; charset=utf-8', + '.woff': 'font/woff', + '.woff2':'font/woff2', +}; + +async function serveStatic(req, res, urlPath) { + let rel = urlPath === '/' ? '/index.html' : urlPath; + const target = resolve(UI_DIST, '.' + rel); + if (!target.startsWith(UI_DIST)) return replyError(res, 403, 'forbidden'); + try { + const s = await statAsync(target); + if (s.isDirectory()) return serveStatic(req, res, join(rel, 'index.html')); + const buf = await readFile(target); + const mime = MIME[extname(target)] || 'application/octet-stream'; + res.writeHead(200, { 'content-type': mime, 'cache-control': 'no-cache' }); + res.end(buf); + } catch { + // SPA fallback: unknown routes serve index.html so hash routing works. + if (urlPath !== '/index.html' && !urlPath.startsWith('/assets/')) { + return serveStatic(req, res, '/index.html'); + } + replyError(res, 404, 'not found'); + } +} + +// ─── data layer ───────────────────────────────────────────────────────────── + +function keyToSlug(key) { + return key.replace(/#/g, '-').replace(/\./g, '-'); +} + +function shortRepo(key) { + const m = /^(.+?)__(.+?)#(\d+)$/.exec(key); + return m ? { owner: m[1], repo: m[2], number: m[3] } : null; +} + +async function ensureDir(p) { + try { mkdirSync(p, { recursive: true }); } catch {} +} + +// Best-effort vendor from the role name — matches lmstack's `h1-nvidia`, +// `h2-amd` convention. Only used when probe.json is missing so the UI can +// still render a vendor badge. Overridden by probe.gpu.vendor once present. +function inferVendorFromRole(role) { + if (!role) return null; + if (/nvidia/i.test(role)) return 'nvidia'; + if (/amd/i.test(role) || /rocm/i.test(role) || /radeon/i.test(role)) return 'amd'; + return null; +} + +// Reserved top-level names inside ~/.lmstack/ that are never role directories. +const RESERVED = new Set([ + 'runs', 'worktrees', 'env', 'templates', 'models', 'models-gguf', 'litellm', +]); + +async function listRoles() { + try { + const entries = await readdir(LMSTACK_HOME, { withFileTypes: true }); + const roles = []; + for (const e of entries) { + if (!e.isDirectory()) continue; + if (RESERVED.has(e.name)) continue; + const hasHost = existsSync(join(LMSTACK_HOME, e.name, 'host.yml')); + const hasTasks = existsSync(join(LMSTACK_HOME, e.name, 'tasks')); + if (hasHost || hasTasks) roles.push(e.name); + } + return roles.sort(); + } catch { + return []; + } +} + +// Minimal YAML reader for host.yml — flat key: value plus inline arrays. +// Anything unrecognized falls through as a raw string. host.yml is written by +// our own install skill, so we control the shape. +function parseHostYaml(text) { + const out = {}; + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.replace(/#.*$/, '').trim(); + if (!line) continue; + const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/.exec(line); + if (!m) continue; + const key = m[1]; + let val = m[2].trim(); + if (val === '') { out[key] = ''; continue; } + // inline array [a, b, c] + const arr = /^\[(.*)\]$/.exec(val); + if (arr) { + out[key] = arr[1] + .split(',') + .map((s) => s.trim().replace(/^["']|["']$/g, '')) + .filter(Boolean); + continue; + } + val = val.replace(/^["']|["']$/g, ''); + out[key] = val; + } + return out; +} + +async function readHostYaml(role) { + try { + const text = await readFile(join(LMSTACK_HOME, role, 'host.yml'), 'utf8'); + return parseHostYaml(text); + } catch { return {}; } +} + +async function readProbeJson(role) { + try { + const text = await readFile(join(LMSTACK_HOME, role, 'probe.json'), 'utf8'); + return JSON.parse(text); + } catch { return null; } +} + +async function readClassifyJson(role) { + try { + const text = await readFile(join(LMSTACK_HOME, role, 'classify.json'), 'utf8'); + return JSON.parse(text); + } catch { return null; } +} + +async function readTasksForRole(role) { + const root = join(LMSTACK_HOME, role, 'tasks'); + const records = []; + if (!existsSync(root)) return records; + const repos = await readdir(root, { withFileTypes: true }); + for (const r of repos) { + if (!r.isDirectory()) continue; + const files = await readdir(join(root, r.name), { withFileTypes: true }); + for (const f of files) { + if (!f.isFile() || !f.name.endsWith('.json')) continue; + try { + const raw = await readFile(join(root, r.name, f.name), 'utf8'); + records.push(JSON.parse(raw)); + } catch { /* skip unreadable */ } + } + } + return records; +} + +async function liveTmuxSessions() { + try { + const { stdout } = await execFileP('tmux', ['list-sessions', '-F', '#{session_name}']); + const alive = new Set(); + for (const line of stdout.split(/\r?\n/)) { + const s = line.trim(); + if (s.startsWith('lmstack-')) alive.add(s.slice('lmstack-'.length)); + } + return alive; + } catch { + return new Set(); + } +} + +async function readLedger() { + const path = join(LMSTACK_HOME, 'ledger.jsonl'); + const records = []; + if (!existsSync(path)) return records; + try { + const text = await readFile(path, 'utf8'); + for (const line of text.split(/\r?\n/)) { + const t = line.trim(); + if (!t) continue; + try { records.push(JSON.parse(t)); } catch { /* skip */ } + } + } catch { /* empty */ } + return records; +} + +function deriveStatus(task, aliveSlugs) { + const slug = keyToSlug(task.key); + const declared = task.status || 'queued'; + if (declared === 'running' && !aliveSlugs.has(slug)) return 'stale'; + return declared; +} + +async function collectForges() { + const roles = await listRoles(); + const aliveSlugs = await liveTmuxSessions(); + const all = []; + for (const role of roles) { + const tasks = await readTasksForRole(role); + for (const t of tasks) { + if (!t || !t.key) continue; + const slug = keyToSlug(t.key); + const runInfo = await runMeta(slug); + const status = deriveStatus(t, aliveSlugs); + all.push({ + key: t.key, + role, + status, + tier: t.tier || null, + shape: t.shape || null, + pr: t.pr || null, + title: t.title || null, + url: t.url || null, + judgeRounds: runInfo.judgeCount, + startedAt: runInfo.startedAt, + wallMin: runInfo.wallMin, + }); + } + } + return all; +} + +async function runMeta(slug) { + const runDir = join(LMSTACK_HOME, 'runs', slug); + let startedAt = null; + let endedAt = null; + let judgeCount = 0; + let brief = null; + + if (existsSync(runDir)) { + try { + startedAt = (await readFile(join(runDir, 'started'), 'utf8')).trim() || null; + } catch { /* missing is fine */ } + try { + endedAt = (await readFile(join(runDir, 'ended'), 'utf8')).trim() || null; + } catch { /* missing is fine */ } + try { + brief = await readFile(join(runDir, 'brief.md'), 'utf8'); + } catch { /* missing is fine */ } + try { + const files = await readdir(runDir); + judgeCount = files.filter((n) => /^judge-\d+\.md$/.test(n)).length; + } catch { /* missing */ } + } + + let wallMin = null; + if (startedAt) { + const start = Date.parse(startedAt); + const end = endedAt ? Date.parse(endedAt) : Date.now(); + if (Number.isFinite(start) && Number.isFinite(end) && end >= start) { + wallMin = Math.round((end - start) / 60000); + } + } + return { startedAt, endedAt, wallMin, judgeCount, brief, runDir }; +} + +async function getInstances() { + const roles = await listRoles(); + const all = await collectForges(); + const out = []; + for (const role of roles) { + out.push(await buildInstance(role, all)); + } + return out; +} + +// Merge whatever we can read from host.yml / probe.json / classify.json +// into one shape the UI can render as a full-width card. Missing files are +// reported in `sources` so the UI can render an explicit empty-state hint +// rather than a card full of dashes. +async function buildInstance(role, allForges) { + const host = await readHostYaml(role); + const probe = await readProbeJson(role); + const classify = await readClassifyJson(role); + const forges = (allForges || []).filter((f) => f.role === role); + const counts = countByStatus(forges); + + const activeModels = Array.isArray(host.active_models) + ? host.active_models + : (host.active_models ? [host.active_models] : []); + + // Merge classify.models (with per-model params) with active_models + // (which names the ones currently live on the host). + const classifyModels = Array.isArray(classify?.models) ? classify.models : []; + const models = classifyModels.length + ? classifyModels.map((m) => ({ + slug: m.slug || m.name || null, + hfModel: m.hf_model || null, + engine: m.engine || null, + port: m.port ?? null, + contextTokens: m.max_model_len ?? m.context_tokens ?? null, + maxNumSeqs: m.max_num_seqs ?? null, + vramEstimateGib: m.vram_estimate_gib ?? null, + tier: m.tier || null, + quant: m.quant || null, + active: activeModels.includes(m.slug) || activeModels.includes(m.name), + })) + : activeModels.map((slug) => ({ slug, active: true })); + + return { + role, + connection: host.connection || null, + installedAt: host.installed_at || null, + verdict: host.verdict || classify?.supported === false ? host.verdict || 'unsupported' : host.verdict || (classify ? 'supported' : null), + engine: { + kind: host.engine || classify?.engine || null, + reason: classify?.reason || null, + host: classify?.host || null, + }, + gpu: probe?.gpu + ? { + vendor: probe.gpu.vendor || inferVendorFromRole(role), + model: probe.gpu.model || null, + vramGib: probe.gpu.vram_gib ?? null, + gttGib: probe.gpu.gtt_gib ?? null, + driver: probe.gpu.driver || null, + } + : (inferVendorFromRole(role) + ? { vendor: inferVendorFromRole(role), model: null, vramGib: null, gttGib: null, driver: null } + : null), + os: probe?.os + ? { + pretty: probe.os.pretty || null, + kernel: probe.os.kernel || null, + arch: probe.os.arch || null, + } + : null, + memory: probe?.mem_total_gib != null ? { totalGib: probe.mem_total_gib } : null, + docker: probe?.docker + ? { + present: !!probe.docker.present, + usable: !!probe.docker.usable, + version: probe.docker.version || null, + runtimes: Array.isArray(probe.docker.runtimes) ? probe.docker.runtimes : [], + } + : null, + vulkan: probe?.vulkan + ? { + present: !!probe.vulkan.present, + device: probe.vulkan.device || null, + } + : null, + arithmetic: Array.isArray(classify?.arithmetic) ? classify.arithmetic : [], + warnings: Array.isArray(classify?.warnings) ? classify.warnings : [], + activeModels, + models, + counts, + sources: { + hostYaml: existsSync(join(LMSTACK_HOME, role, 'host.yml')), + probeJson: probe != null, + classifyJson: classify != null, + }, + }; +} + +function countByStatus(forges) { + const c = { running: 0, in_review: 0, queued: 0, merged: 0, failed: 0, cleaned: 0, stale: 0 }; + for (const f of forges) { + if (f.status === 'in-review') c.in_review++; + else if (f.status in c) c[f.status]++; + } + return c; +} + +async function getInstance(role) { + const host = await readHostYaml(role); + const probe = await readProbeJson(role); + return { role, host, probe }; +} + +async function getForges(filters) { + let all = await collectForges(); + if (filters.role) all = all.filter((f) => f.role === filters.role); + if (filters.status) all = all.filter((f) => f.status === filters.status); + // sort: running first, then in-review, then queued, then completed + const order = { running: 0, 'in-review': 1, stale: 2, queued: 3, failed: 4, merged: 5, cleaned: 6 }; + all.sort((a, b) => (order[a.status] ?? 99) - (order[b.status] ?? 99) || a.key.localeCompare(b.key)); + return all; +} + +async function getForge(key) { + const roles = await listRoles(); + let task = null; + let role = null; + for (const r of roles) { + const tasks = await readTasksForRole(r); + const found = tasks.find((t) => t && t.key === key); + if (found) { task = found; role = r; break; } + } + if (!task) { + // Fall back to ledger lookup so a cleaned-up task is still viewable. + const ledger = await readLedger(); + const led = ledger.reverse().find((r) => r.key === key); + if (!led) throw new Error(`unknown forge key: ${key}`); + task = { key, host_role: led.host_role, tier: led.tier, shape: led.shape, pr: led.pr, status: 'cleaned' }; + role = led.host_role; + } + const slug = keyToSlug(key); + const aliveSlugs = await liveTmuxSessions(); + const tmuxAlive = aliveSlugs.has(slug); + const meta = await runMeta(slug); + + // Judge rounds + const judgeRounds = []; + if (existsSync(meta.runDir)) { + const files = (await readdir(meta.runDir)).filter((n) => /^judge-\d+\.md$/.test(n)) + .sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0])); + for (const name of files) { + const n = Number(name.match(/\d+/)[0]); + const text = await readFile(join(meta.runDir, name), 'utf8'); + judgeRounds.push({ n, text }); + } + } + + const worktreePath = join(LMSTACK_HOME, 'worktrees', slug); + const worktreeExists = existsSync(worktreePath); + const branch = `lmstack/${slug}`; + + // Ledger record for this key (most recent) + const ledger = await readLedger(); + const led = [...ledger].reverse().find((r) => r.key === key) || null; + + return { + role, + slug, + task, + run: { + exists: existsSync(meta.runDir), + brief: meta.brief, + judgeRounds, + startedAt: meta.startedAt, + endedAt: meta.endedAt, + tmuxAlive, + worktreePath, + worktreeExists, + branch, + }, + ledger: led, + }; +} + +async function getLog(key, which, tail) { + const slug = keyToSlug(key); + const path = join(LMSTACK_HOME, 'runs', slug, `${which}.log`); + if (!existsSync(path)) return { lines: [], sizeBytes: 0, truncated: false, exists: false, path }; + const s = statSync(path); + const size = s.size; + const window = Math.min(size, 64 * 1024); + const start = size - window; + const fd = openSync(path, 'r'); + const buf = Buffer.alloc(window); + readSync(fd, buf, 0, window, start); + closeSync(fd); + const text = stripAnsi(buf.toString('utf8')); + const allLines = text.split(/\r?\n/); + // If we didn't read from byte 0, the first partial line is likely truncated — drop it. + const linesTrimmed = start > 0 && allLines.length > 0 ? allLines.slice(1) : allLines; + const kept = linesTrimmed.slice(-tail).filter((l, i, arr) => !(i === arr.length - 1 && l === '')); + return { lines: kept, sizeBytes: size, truncated: start > 0 || linesTrimmed.length > tail, exists: true, path }; +} + +// Strip CSI/OSC/other ANSI escape sequences that tmux `pipe-pane` emits. +// Terminal transcripts are otherwise unreadable when rendered as plain text. +function stripAnsi(s) { + // CSI sequences: ESC [ ... final-byte + let out = s.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, ''); + // OSC sequences: ESC ] ... BEL or ESC ] ... ESC \ + out = out.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, ''); + // Two-byte escapes (SS2/SS3/RIS/etc.) and stray ESC + out = out.replace(/\x1b[@-Z\\-_]/g, ''); + return out; +} + +async function getLedger(filters) { + const all = await readLedger(); + let out = all.slice().reverse(); // newest first + if (filters.shape) out = out.filter((r) => r.shape === filters.shape); + if (filters.outcome) out = out.filter((r) => r.outcome === filters.outcome); + if (filters.role) out = out.filter((r) => r.host_role === filters.role); + if (filters.limit) out = out.slice(0, filters.limit); + return out; +} + +// ─── reply helpers ────────────────────────────────────────────────────────── + +function replyJson(res, obj) { + res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }); + res.end(JSON.stringify(obj)); +} +function replyError(res, code, message) { + res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ error: message })); +} + +// ─── tiny fs helpers ──────────────────────────────────────────────────────── + +async function writeFile(path, text) { + const fs = await import('node:fs/promises'); + await fs.writeFile(path, text); +} +async function rm(path) { + const fs = await import('node:fs/promises'); + await fs.rm(path, { force: true }); +} +function unlinkSyncSafe(path) { + try { unlinkSync(path); } catch {} +} +async function waitFor(pred, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { if (await pred()) return true; } catch {} + await new Promise((r) => setTimeout(r, 50)); + } + return false; +} diff --git a/skills/analyze/SKILL.md b/skills/analyze/SKILL.md index 0d48f2a..284d248 100644 --- a/skills/analyze/SKILL.md +++ b/skills/analyze/SKILL.md @@ -80,14 +80,39 @@ If `supported` is `false`, relay `reason` and `remedy`, log the failure, and ## Step 4 — Record and hand off -Write the probe where install will find it. This is the only file this skill -creates: +Write three files into `~/.lmstack//` so downstream commands and the +read-only web UI (`lmstack-ui`) can render the host without re-probing. ```bash mkdir -p "$HOME/.lmstack/" + +# 1. the raw probe — what the machine reported about itself cp /tmp/lmstack-probe.json "$HOME/.lmstack//probe.json" + +# 2. the classifier verdict — engine choice, arithmetic, warnings, models +lmstack-classify --probe /tmp/lmstack-probe.json \ + > "$HOME/.lmstack//classify.json" + +# 3. a minimal host.yml — the identity of this role, so a reader that has +# opened only one file still knows the role, its connection, and the +# engine that will drive it. `install` overwrites this with the final +# version (adding active_models and installed_at) later; this stub is +# for the window between analyze and install. +cat > "$HOME/.lmstack//host.yml" < +connection: +verdict: +engine: +gpu: "" +active_models: [] +YAML ``` +All three files are read-only inputs to the UI and to `install`. Any of them +missing is a "run `/lmstack:analyze ` again" — the UI shows a hint on +the instance card when a file is missing, so a partial write is safe but +visible. + Then log it: ```bash diff --git a/skills/install/SKILL.md b/skills/install/SKILL.md index 53b5951..928bff4 100644 --- a/skills/install/SKILL.md +++ b/skills/install/SKILL.md @@ -258,6 +258,30 @@ the local-only story from end to end. Finish by telling the user what they now have and that `/lmstack:harvest` is what turns it into work. +## Phase 7 — Start the read-only UI + +The last step brings up the local web UI so the user can watch instances and +forges without attaching to tmux. It reads only from `~/.lmstack/`; it never +mutates state. + +```bash +"${CLAUDE_PLUGIN_ROOT}/bin/lmstack-ui" start +``` + +`start` is idempotent — if the server is already running (from a prior install +or a manual start) it prints the URL and exits 0. Include the printed +`http://127.0.0.1:/` in the finish message so the user can click through. + +If the command fails because the SPA has not been built (`ui/dist/` missing — +only happens in a source checkout, not in a released plugin), print the error +and the build hint it emits, but **do not abort the install**. The install has +already succeeded; the UI is a convenience layer on top of it. The user can +start it later with `lmstack-ui start` once the SPA is built, or with +`lmstack-ui foreground` to debug. + +The daemon lives until reboot or explicit `lmstack-ui stop`. There is no +systemd/launchd unit — start it again after a reboot if you want it back. + ## Logging Every phase writes one line through `lmstack-log`, which redacts before it diff --git a/ui/dist/assets/index-BqvLn6SF.js b/ui/dist/assets/index-BqvLn6SF.js new file mode 100644 index 0000000..9896ea7 --- /dev/null +++ b/ui/dist/assets/index-BqvLn6SF.js @@ -0,0 +1,42 @@ +(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const u of i.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function t(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=t(l);fetch(l.href,i)}})();function yc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ls={exports:{}},ul={},is={exports:{}},L={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var qt=Symbol.for("react.element"),xc=Symbol.for("react.portal"),wc=Symbol.for("react.fragment"),kc=Symbol.for("react.strict_mode"),Sc=Symbol.for("react.profiler"),jc=Symbol.for("react.provider"),Ec=Symbol.for("react.context"),Nc=Symbol.for("react.forward_ref"),Cc=Symbol.for("react.suspense"),_c=Symbol.for("react.memo"),Pc=Symbol.for("react.lazy"),Hu=Symbol.iterator;function zc(e){return e===null||typeof e!="object"?null:(e=Hu&&e[Hu]||e["@@iterator"],typeof e=="function"?e:null)}var us={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},os=Object.assign,ss={};function at(e,n,t){this.props=e,this.context=n,this.refs=ss,this.updater=t||us}at.prototype.isReactComponent={};at.prototype.setState=function(e,n){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,n,"setState")};at.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function as(){}as.prototype=at.prototype;function Gi(e,n,t){this.props=e,this.context=n,this.refs=ss,this.updater=t||us}var Ki=Gi.prototype=new as;Ki.constructor=Gi;os(Ki,at.prototype);Ki.isPureReactComponent=!0;var Wu=Array.isArray,cs=Object.prototype.hasOwnProperty,Yi={current:null},fs={key:!0,ref:!0,__self:!0,__source:!0};function ds(e,n,t){var r,l={},i=null,u=null;if(n!=null)for(r in n.ref!==void 0&&(u=n.ref),n.key!==void 0&&(i=""+n.key),n)cs.call(n,r)&&!fs.hasOwnProperty(r)&&(l[r]=n[r]);var s=arguments.length-2;if(s===1)l.children=t;else if(1>>1,J=E[G];if(0>>1;Gl(Nl,z))xnl(lr,Nl)?(E[G]=lr,E[xn]=z,G=xn):(E[G]=Nl,E[yn]=z,G=yn);else if(xnl(lr,z))E[G]=lr,E[xn]=z,G=xn;else break e}}return P}function l(E,P){var z=E.sortIndex-P.sortIndex;return z!==0?z:E.id-P.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var u=Date,s=u.now();e.unstable_now=function(){return u.now()-s}}var a=[],f=[],g=1,m=null,h=3,y=!1,x=!1,v=!1,I=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(E){for(var P=t(f);P!==null;){if(P.callback===null)r(f);else if(P.startTime<=E)r(f),P.sortIndex=P.expirationTime,n(a,P);else break;P=t(f)}}function w(E){if(v=!1,p(E),!x)if(t(a)!==null)x=!0,jl(j);else{var P=t(f);P!==null&&El(w,P.startTime-E)}}function j(E,P){x=!1,v&&(v=!1,d(_),_=-1),y=!0;var z=h;try{for(p(P),m=t(a);m!==null&&(!(m.expirationTime>P)||E&&!Pe());){var G=m.callback;if(typeof G=="function"){m.callback=null,h=m.priorityLevel;var J=G(m.expirationTime<=P);P=e.unstable_now(),typeof J=="function"?m.callback=J:m===t(a)&&r(a),p(P)}else r(a);m=t(a)}if(m!==null)var rr=!0;else{var yn=t(f);yn!==null&&El(w,yn.startTime-P),rr=!1}return rr}finally{m=null,h=z,y=!1}}var N=!1,C=null,_=-1,Q=5,T=-1;function Pe(){return!(e.unstable_now()-TE||125G?(E.sortIndex=z,n(f,E),t(a)===null&&E===t(f)&&(v?(d(_),_=-1):v=!0,El(w,z-G))):(E.sortIndex=J,n(a,E),x||y||(x=!0,jl(j))),E},e.unstable_shouldYield=Pe,e.unstable_wrapCallback=function(E){var P=h;return function(){var z=h;h=P;try{return E.apply(this,arguments)}finally{h=z}}}})(gs);vs.exports=gs;var Bc=vs.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Vc=A,xe=Bc;function k(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,t=1;t"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ei=Object.prototype.hasOwnProperty,Hc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Gu={},Ku={};function Wc(e){return ei.call(Ku,e)?!0:ei.call(Gu,e)?!1:Hc.test(e)?Ku[e]=!0:(Gu[e]=!0,!1)}function Qc(e,n,t,r){if(t!==null&&t.type===0)return!1;switch(typeof n){case"function":case"symbol":return!0;case"boolean":return r?!1:t!==null?!t.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Gc(e,n,t,r){if(n===null||typeof n>"u"||Qc(e,n,t,r))return!0;if(r)return!1;if(t!==null)switch(t.type){case 3:return!n;case 4:return n===!1;case 5:return isNaN(n);case 6:return isNaN(n)||1>n}return!1}function ce(e,n,t,r,l,i,u){this.acceptsBooleans=n===2||n===3||n===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=t,this.propertyName=e,this.type=n,this.sanitizeURL=i,this.removeEmptyString=u}var te={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){te[e]=new ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var n=e[0];te[n]=new ce(n,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){te[e]=new ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){te[e]=new ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){te[e]=new ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){te[e]=new ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){te[e]=new ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){te[e]=new ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){te[e]=new ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var Xi=/[\-:]([a-z])/g;function Ji(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var n=e.replace(Xi,Ji);te[n]=new ce(n,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var n=e.replace(Xi,Ji);te[n]=new ce(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var n=e.replace(Xi,Ji);te[n]=new ce(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){te[e]=new ce(e,1,!1,e.toLowerCase(),null,!1,!1)});te.xlinkHref=new ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){te[e]=new ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function qi(e,n,t,r){var l=te.hasOwnProperty(n)?te[n]:null;(l!==null?l.type!==0:r||!(2s||l[u]!==i[s]){var a=` +`+l[u].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=u&&0<=s);break}}}finally{Pl=!1,Error.prepareStackTrace=t}return(e=e?e.displayName||e.name:"")?St(e):""}function Kc(e){switch(e.tag){case 5:return St(e.type);case 16:return St("Lazy");case 13:return St("Suspense");case 19:return St("SuspenseList");case 0:case 2:case 15:return e=zl(e.type,!1),e;case 11:return e=zl(e.type.render,!1),e;case 1:return e=zl(e.type,!0),e;default:return""}}function li(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case $n:return"Fragment";case Un:return"Portal";case ni:return"Profiler";case bi:return"StrictMode";case ti:return"Suspense";case ri:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ws:return(e.displayName||"Context")+".Consumer";case xs:return(e._context.displayName||"Context")+".Provider";case eu:var n=e.render;return e=e.displayName,e||(e=n.displayName||n.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case nu:return n=e.displayName||null,n!==null?n:li(e.type)||"Memo";case qe:n=e._payload,e=e._init;try{return li(e(n))}catch{}}return null}function Yc(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=n.render,e=e.displayName||e.name||"",n.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return li(n);case 8:return n===bi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n}return null}function pn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ss(e){var n=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function Zc(e){var n=Ss(e)?"checked":"value",t=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),r=""+e[n];if(!e.hasOwnProperty(n)&&typeof t<"u"&&typeof t.get=="function"&&typeof t.set=="function"){var l=t.get,i=t.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return l.call(this)},set:function(u){r=""+u,i.call(this,u)}}),Object.defineProperty(e,n,{enumerable:t.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}function or(e){e._valueTracker||(e._valueTracker=Zc(e))}function js(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var t=n.getValue(),r="";return e&&(r=Ss(e)?e.checked?"true":"false":e.value),e=r,e!==t?(n.setValue(e),!0):!1}function Dr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ii(e,n){var t=n.checked;return V({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:t??e._wrapperState.initialChecked})}function Zu(e,n){var t=n.defaultValue==null?"":n.defaultValue,r=n.checked!=null?n.checked:n.defaultChecked;t=pn(n.value!=null?n.value:t),e._wrapperState={initialChecked:r,initialValue:t,controlled:n.type==="checkbox"||n.type==="radio"?n.checked!=null:n.value!=null}}function Es(e,n){n=n.checked,n!=null&&qi(e,"checked",n,!1)}function ui(e,n){Es(e,n);var t=pn(n.value),r=n.type;if(t!=null)r==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+t):e.value!==""+t&&(e.value=""+t);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}n.hasOwnProperty("value")?oi(e,n.type,t):n.hasOwnProperty("defaultValue")&&oi(e,n.type,pn(n.defaultValue)),n.checked==null&&n.defaultChecked!=null&&(e.defaultChecked=!!n.defaultChecked)}function Xu(e,n,t){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var r=n.type;if(!(r!=="submit"&&r!=="reset"||n.value!==void 0&&n.value!==null))return;n=""+e._wrapperState.initialValue,t||n===e.value||(e.value=n),e.defaultValue=n}t=e.name,t!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,t!==""&&(e.name=t)}function oi(e,n,t){(n!=="number"||Dr(e.ownerDocument)!==e)&&(t==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+t&&(e.defaultValue=""+t))}var jt=Array.isArray;function Xn(e,n,t,r){if(e=e.options,n){n={};for(var l=0;l"+n.valueOf().toString()+"",n=sr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}});function Dt(e,n){if(n){var t=e.firstChild;if(t&&t===e.lastChild&&t.nodeType===3){t.nodeValue=n;return}}e.textContent=n}var Ct={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Xc=["Webkit","ms","Moz","O"];Object.keys(Ct).forEach(function(e){Xc.forEach(function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),Ct[n]=Ct[e]})});function Ps(e,n,t){return n==null||typeof n=="boolean"||n===""?"":t||typeof n!="number"||n===0||Ct.hasOwnProperty(e)&&Ct[e]?(""+n).trim():n+"px"}function zs(e,n){e=e.style;for(var t in n)if(n.hasOwnProperty(t)){var r=t.indexOf("--")===0,l=Ps(t,n[t],r);t==="float"&&(t="cssFloat"),r?e.setProperty(t,l):e[t]=l}}var Jc=V({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ci(e,n){if(n){if(Jc[e]&&(n.children!=null||n.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(n.dangerouslySetInnerHTML!=null){if(n.children!=null)throw Error(k(60));if(typeof n.dangerouslySetInnerHTML!="object"||!("__html"in n.dangerouslySetInnerHTML))throw Error(k(61))}if(n.style!=null&&typeof n.style!="object")throw Error(k(62))}}function fi(e,n){if(e.indexOf("-")===-1)return typeof n.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var di=null;function tu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var pi=null,Jn=null,qn=null;function bu(e){if(e=nr(e)){if(typeof pi!="function")throw Error(k(280));var n=e.stateNode;n&&(n=fl(n),pi(e.stateNode,e.type,n))}}function Ls(e){Jn?qn?qn.push(e):qn=[e]:Jn=e}function Ts(){if(Jn){var e=Jn,n=qn;if(qn=Jn=null,bu(e),n)for(e=0;e>>=0,e===0?32:31-(af(e)/cf|0)|0}var ar=64,cr=4194304;function Et(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ar(e,n){var t=e.pendingLanes;if(t===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,u=t&268435455;if(u!==0){var s=u&~l;s!==0?r=Et(s):(i&=u,i!==0&&(r=Et(i)))}else u=t&~l,u!==0?r=Et(u):i!==0&&(r=Et(i));if(r===0)return 0;if(n!==0&&n!==r&&!(n&l)&&(l=r&-r,i=n&-n,l>=i||l===16&&(i&4194240)!==0))return n;if(r&4&&(r|=t&16),n=e.entangledLanes,n!==0)for(e=e.entanglements,n&=r;0t;t++)n.push(e);return n}function bt(e,n,t){e.pendingLanes|=n,n!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,n=31-Me(n),e[n]=t}function hf(e,n){var t=e.pendingLanes&~n;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=n,e.mutableReadLanes&=n,e.entangledLanes&=n,n=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Pt),so=" ",ao=!1;function Js(e,n){switch(e){case"keyup":return Vf.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function qs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var An=!1;function Wf(e,n){switch(e){case"compositionend":return qs(n);case"keypress":return n.which!==32?null:(ao=!0,so);case"textInput":return e=n.data,e===so&&ao?null:e;default:return null}}function Qf(e,n){if(An)return e==="compositionend"||!cu&&Js(e,n)?(e=Zs(),_r=ou=tn=null,An=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:t,offset:n-e};e=r}e:{for(;t;){if(t.nextSibling){t=t.nextSibling;break e}t=t.parentNode}t=void 0}t=ho(t)}}function ta(e,n){return e&&n?e===n?!0:e&&e.nodeType===3?!1:n&&n.nodeType===3?ta(e,n.parentNode):"contains"in e?e.contains(n):e.compareDocumentPosition?!!(e.compareDocumentPosition(n)&16):!1:!1}function ra(){for(var e=window,n=Dr();n instanceof e.HTMLIFrameElement;){try{var t=typeof n.contentWindow.location.href=="string"}catch{t=!1}if(t)e=n.contentWindow;else break;n=Dr(e.document)}return n}function fu(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&(n==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||n==="textarea"||e.contentEditable==="true")}function ed(e){var n=ra(),t=e.focusedElem,r=e.selectionRange;if(n!==t&&t&&t.ownerDocument&&ta(t.ownerDocument.documentElement,t)){if(r!==null&&fu(t)){if(n=r.start,e=r.end,e===void 0&&(e=n),"selectionStart"in t)t.selectionStart=n,t.selectionEnd=Math.min(e,t.value.length);else if(e=(n=t.ownerDocument||document)&&n.defaultView||window,e.getSelection){e=e.getSelection();var l=t.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=mo(t,i);var u=mo(t,r);l&&u&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(n=n.createRange(),n.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(n),e.extend(u.node,u.offset)):(n.setEnd(u.node,u.offset),e.addRange(n)))}}for(n=[],e=t;e=e.parentNode;)e.nodeType===1&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof t.focus=="function"&&t.focus(),t=0;t=document.documentMode,Bn=null,xi=null,Lt=null,wi=!1;function vo(e,n,t){var r=t.window===t?t.document:t.nodeType===9?t:t.ownerDocument;wi||Bn==null||Bn!==Dr(r)||(r=Bn,"selectionStart"in r&&fu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Lt&&Vt(Lt,r)||(Lt=r,r=Hr(xi,"onSelect"),0Wn||(e.current=Ci[Wn],Ci[Wn]=null,Wn--)}function O(e,n){Wn++,Ci[Wn]=e.current,e.current=n}var hn={},ue=vn(hn),pe=vn(!1),zn=hn;function rt(e,n){var t=e.type.contextTypes;if(!t)return hn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===n)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in t)l[i]=n[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=l),l}function he(e){return e=e.childContextTypes,e!=null}function Qr(){F(pe),F(ue)}function jo(e,n,t){if(ue.current!==hn)throw Error(k(168));O(ue,n),O(pe,t)}function da(e,n,t){var r=e.stateNode;if(n=n.childContextTypes,typeof r.getChildContext!="function")return t;r=r.getChildContext();for(var l in r)if(!(l in n))throw Error(k(108,Yc(e)||"Unknown",l));return V({},t,r)}function Gr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||hn,zn=ue.current,O(ue,e),O(pe,pe.current),!0}function Eo(e,n,t){var r=e.stateNode;if(!r)throw Error(k(169));t?(e=da(e,n,zn),r.__reactInternalMemoizedMergedChildContext=e,F(pe),F(ue),O(ue,e)):F(pe),O(pe,t)}var Ve=null,dl=!1,Hl=!1;function pa(e){Ve===null?Ve=[e]:Ve.push(e)}function dd(e){dl=!0,pa(e)}function gn(){if(!Hl&&Ve!==null){Hl=!0;var e=0,n=M;try{var t=Ve;for(M=1;e>=u,l-=u,He=1<<32-Me(n)+l|t<_?(Q=C,C=null):Q=C.sibling;var T=h(d,C,p[_],w);if(T===null){C===null&&(C=Q);break}e&&C&&T.alternate===null&&n(d,C),c=i(T,c,_),N===null?j=T:N.sibling=T,N=T,C=Q}if(_===p.length)return t(d,C),U&&wn(d,_),j;if(C===null){for(;__?(Q=C,C=null):Q=C.sibling;var Pe=h(d,C,T.value,w);if(Pe===null){C===null&&(C=Q);break}e&&C&&Pe.alternate===null&&n(d,C),c=i(Pe,c,_),N===null?j=Pe:N.sibling=Pe,N=Pe,C=Q}if(T.done)return t(d,C),U&&wn(d,_),j;if(C===null){for(;!T.done;_++,T=p.next())T=m(d,T.value,w),T!==null&&(c=i(T,c,_),N===null?j=T:N.sibling=T,N=T);return U&&wn(d,_),j}for(C=r(d,C);!T.done;_++,T=p.next())T=y(C,d,_,T.value,w),T!==null&&(e&&T.alternate!==null&&C.delete(T.key===null?_:T.key),c=i(T,c,_),N===null?j=T:N.sibling=T,N=T);return e&&C.forEach(function(dt){return n(d,dt)}),U&&wn(d,_),j}function I(d,c,p,w){if(typeof p=="object"&&p!==null&&p.type===$n&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case ur:e:{for(var j=p.key,N=c;N!==null;){if(N.key===j){if(j=p.type,j===$n){if(N.tag===7){t(d,N.sibling),c=l(N,p.props.children),c.return=d,d=c;break e}}else if(N.elementType===j||typeof j=="object"&&j!==null&&j.$$typeof===qe&&_o(j)===N.type){t(d,N.sibling),c=l(N,p.props),c.ref=xt(d,N,p),c.return=d,d=c;break e}t(d,N);break}else n(d,N);N=N.sibling}p.type===$n?(c=Cn(p.props.children,d.mode,w,p.key),c.return=d,d=c):(w=Or(p.type,p.key,p.props,null,d.mode,w),w.ref=xt(d,c,p),w.return=d,d=w)}return u(d);case Un:e:{for(N=p.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){t(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{t(d,c);break}else n(d,c);c=c.sibling}c=Jl(p,d.mode,w),c.return=d,d=c}return u(d);case qe:return N=p._init,I(d,c,N(p._payload),w)}if(jt(p))return x(d,c,p,w);if(ht(p))return v(d,c,p,w);gr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(t(d,c.sibling),c=l(c,p),c.return=d,d=c):(t(d,c),c=Xl(p,d.mode,w),c.return=d,d=c),u(d)):t(d,c)}return I}var it=ga(!0),ya=ga(!1),Zr=vn(null),Xr=null,Kn=null,mu=null;function vu(){mu=Kn=Xr=null}function gu(e){var n=Zr.current;F(Zr),e._currentValue=n}function zi(e,n,t){for(;e!==null;){var r=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,r!==null&&(r.childLanes|=n)):r!==null&&(r.childLanes&n)!==n&&(r.childLanes|=n),e===t)break;e=e.return}}function et(e,n){Xr=e,mu=Kn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&n&&(de=!0),e.firstContext=null)}function Ce(e){var n=e._currentValue;if(mu!==e)if(e={context:e,memoizedValue:n,next:null},Kn===null){if(Xr===null)throw Error(k(308));Kn=e,Xr.dependencies={lanes:0,firstContext:e}}else Kn=Kn.next=e;return n}var jn=null;function yu(e){jn===null?jn=[e]:jn.push(e)}function xa(e,n,t,r){var l=n.interleaved;return l===null?(t.next=t,yu(n)):(t.next=l.next,l.next=t),n.interleaved=t,Ye(e,r)}function Ye(e,n){e.lanes|=n;var t=e.alternate;for(t!==null&&(t.lanes|=n),t=e,e=e.return;e!==null;)e.childLanes|=n,t=e.alternate,t!==null&&(t.childLanes|=n),t=e,e=e.return;return t.tag===3?t.stateNode:null}var be=!1;function xu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function wa(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Qe(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function an(e,n,t){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,R&2){var l=r.pending;return l===null?n.next=n:(n.next=l.next,l.next=n),r.pending=n,Ye(e,t)}return l=r.interleaved,l===null?(n.next=n,yu(r)):(n.next=l.next,l.next=n),r.interleaved=n,Ye(e,t)}function zr(e,n,t){if(n=n.updateQueue,n!==null&&(n=n.shared,(t&4194240)!==0)){var r=n.lanes;r&=e.pendingLanes,t|=r,n.lanes=t,lu(e,t)}}function Po(e,n){var t=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,t===r)){var l=null,i=null;if(t=t.firstBaseUpdate,t!==null){do{var u={eventTime:t.eventTime,lane:t.lane,tag:t.tag,payload:t.payload,callback:t.callback,next:null};i===null?l=i=u:i=i.next=u,t=t.next}while(t!==null);i===null?l=i=n:i=i.next=n}else l=i=n;t={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=t;return}e=t.lastBaseUpdate,e===null?t.firstBaseUpdate=n:e.next=n,t.lastBaseUpdate=n}function Jr(e,n,t,r){var l=e.updateQueue;be=!1;var i=l.firstBaseUpdate,u=l.lastBaseUpdate,s=l.shared.pending;if(s!==null){l.shared.pending=null;var a=s,f=a.next;a.next=null,u===null?i=f:u.next=f,u=a;var g=e.alternate;g!==null&&(g=g.updateQueue,s=g.lastBaseUpdate,s!==u&&(s===null?g.firstBaseUpdate=f:s.next=f,g.lastBaseUpdate=a))}if(i!==null){var m=l.baseState;u=0,g=f=a=null,s=i;do{var h=s.lane,y=s.eventTime;if((r&h)===h){g!==null&&(g=g.next={eventTime:y,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});e:{var x=e,v=s;switch(h=n,y=t,v.tag){case 1:if(x=v.payload,typeof x=="function"){m=x.call(y,m,h);break e}m=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=v.payload,h=typeof x=="function"?x.call(y,m,h):x,h==null)break e;m=V({},m,h);break e;case 2:be=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[s]:h.push(s))}else y={eventTime:y,lane:h,tag:s.tag,payload:s.payload,callback:s.callback,next:null},g===null?(f=g=y,a=m):g=g.next=y,u|=h;if(s=s.next,s===null){if(s=l.shared.pending,s===null)break;h=s,s=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(g===null&&(a=m),l.baseState=a,l.firstBaseUpdate=f,l.lastBaseUpdate=g,n=l.shared.interleaved,n!==null){l=n;do u|=l.lane,l=l.next;while(l!==n)}else i===null&&(l.shared.lanes=0);Rn|=u,e.lanes=u,e.memoizedState=m}}function zo(e,n,t){if(e=n.effects,n.effects=null,e!==null)for(n=0;nt?t:4,e(!0);var r=Ql.transition;Ql.transition={};try{e(!1),n()}finally{M=t,Ql.transition=r}}function Fa(){return _e().memoizedState}function vd(e,n,t){var r=fn(e);if(t={lane:r,action:t,hasEagerState:!1,eagerState:null,next:null},Ua(e))$a(n,t);else if(t=xa(e,n,t,r),t!==null){var l=se();Ie(t,e,r,l),Aa(t,n,r)}}function gd(e,n,t){var r=fn(e),l={lane:r,action:t,hasEagerState:!1,eagerState:null,next:null};if(Ua(e))$a(n,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=n.lastRenderedReducer,i!==null))try{var u=n.lastRenderedState,s=i(u,t);if(l.hasEagerState=!0,l.eagerState=s,Oe(s,u)){var a=n.interleaved;a===null?(l.next=l,yu(n)):(l.next=a.next,a.next=l),n.interleaved=l;return}}catch{}finally{}t=xa(e,n,l,r),t!==null&&(l=se(),Ie(t,e,r,l),Aa(t,n,r))}}function Ua(e){var n=e.alternate;return e===B||n!==null&&n===B}function $a(e,n){Tt=br=!0;var t=e.pending;t===null?n.next=n:(n.next=t.next,t.next=n),e.pending=n}function Aa(e,n,t){if(t&4194240){var r=n.lanes;r&=e.pendingLanes,t|=r,n.lanes=t,lu(e,t)}}var el={readContext:Ce,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},yd={readContext:Ce,useCallback:function(e,n){return Fe().memoizedState=[e,n===void 0?null:n],e},useContext:Ce,useEffect:To,useImperativeHandle:function(e,n,t){return t=t!=null?t.concat([e]):null,Tr(4194308,4,Ra.bind(null,n,e),t)},useLayoutEffect:function(e,n){return Tr(4194308,4,e,n)},useInsertionEffect:function(e,n){return Tr(4,2,e,n)},useMemo:function(e,n){var t=Fe();return n=n===void 0?null:n,e=e(),t.memoizedState=[e,n],e},useReducer:function(e,n,t){var r=Fe();return n=t!==void 0?t(n):n,r.memoizedState=r.baseState=n,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},r.queue=e,e=e.dispatch=vd.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var n=Fe();return e={current:e},n.memoizedState=e},useState:Lo,useDebugValue:_u,useDeferredValue:function(e){return Fe().memoizedState=e},useTransition:function(){var e=Lo(!1),n=e[0];return e=md.bind(null,e[1]),Fe().memoizedState=e,[n,e]},useMutableSource:function(){},useSyncExternalStore:function(e,n,t){var r=B,l=Fe();if(U){if(t===void 0)throw Error(k(407));t=t()}else{if(t=n(),b===null)throw Error(k(349));Tn&30||Ea(r,n,t)}l.memoizedState=t;var i={value:t,getSnapshot:n};return l.queue=i,To(Ca.bind(null,r,i,e),[e]),r.flags|=2048,Xt(9,Na.bind(null,r,i,t,n),void 0,null),t},useId:function(){var e=Fe(),n=b.identifierPrefix;if(U){var t=We,r=He;t=(r&~(1<<32-Me(r)-1)).toString(32)+t,n=":"+n+"R"+t,t=Yt++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(t,{is:r.is}):(e=u.createElement(t),t==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,t),e[Ue]=n,e[Qt]=r,Xa(e,n,!1,!1),n.stateNode=e;e:{switch(u=fi(t,r),t){case"dialog":D("cancel",e),D("close",e),l=r;break;case"iframe":case"object":case"embed":D("load",e),l=r;break;case"video":case"audio":for(l=0;lst&&(n.flags|=128,r=!0,wt(i,!1),n.lanes=4194304)}else{if(!r)if(e=qr(u),e!==null){if(n.flags|=128,r=!0,t=e.updateQueue,t!==null&&(n.updateQueue=t,n.flags|=4),wt(i,!0),i.tail===null&&i.tailMode==="hidden"&&!u.alternate&&!U)return le(n),null}else 2*K()-i.renderingStartTime>st&&t!==1073741824&&(n.flags|=128,r=!0,wt(i,!1),n.lanes=4194304);i.isBackwards?(u.sibling=n.child,n.child=u):(t=i.last,t!==null?t.sibling=u:n.child=u,i.last=u)}return i.tail!==null?(n=i.tail,i.rendering=n,i.tail=n.sibling,i.renderingStartTime=K(),n.sibling=null,t=$.current,O($,r?t&1|2:t&1),n):(le(n),null);case 22:case 23:return Mu(),r=n.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(n.flags|=8192),r&&n.mode&1?ve&1073741824&&(le(n),n.subtreeFlags&6&&(n.flags|=8192)):le(n),null;case 24:return null;case 25:return null}throw Error(k(156,n.tag))}function Cd(e,n){switch(pu(n),n.tag){case 1:return he(n.type)&&Qr(),e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 3:return ut(),F(pe),F(ue),Su(),e=n.flags,e&65536&&!(e&128)?(n.flags=e&-65537|128,n):null;case 5:return ku(n),null;case 13:if(F($),e=n.memoizedState,e!==null&&e.dehydrated!==null){if(n.alternate===null)throw Error(k(340));lt()}return e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 19:return F($),null;case 4:return ut(),null;case 10:return gu(n.type._context),null;case 22:case 23:return Mu(),null;case 24:return null;default:return null}}var xr=!1,ie=!1,_d=typeof WeakSet=="function"?WeakSet:Set,S=null;function Yn(e,n){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(r){W(e,n,r)}else t.current=null}function Ui(e,n,t){try{t()}catch(r){W(e,n,r)}}var Vo=!1;function Pd(e,n){if(ki=Br,e=ra(),fu(e)){if("selectionStart"in e)var t={start:e.selectionStart,end:e.selectionEnd};else e:{t=(t=e.ownerDocument)&&t.defaultView||window;var r=t.getSelection&&t.getSelection();if(r&&r.rangeCount!==0){t=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{t.nodeType,i.nodeType}catch{t=null;break e}var u=0,s=-1,a=-1,f=0,g=0,m=e,h=null;n:for(;;){for(var y;m!==t||l!==0&&m.nodeType!==3||(s=u+l),m!==i||r!==0&&m.nodeType!==3||(a=u+r),m.nodeType===3&&(u+=m.nodeValue.length),(y=m.firstChild)!==null;)h=m,m=y;for(;;){if(m===e)break n;if(h===t&&++f===l&&(s=u),h===i&&++g===r&&(a=u),(y=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=y}t=s===-1||a===-1?null:{start:s,end:a}}else t=null}t=t||{start:0,end:0}}else t=null;for(Si={focusedElem:e,selectionRange:t},Br=!1,S=n;S!==null;)if(n=S,e=n.child,(n.subtreeFlags&1028)!==0&&e!==null)e.return=n,S=e;else for(;S!==null;){n=S;try{var x=n.alternate;if(n.flags&1024)switch(n.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var v=x.memoizedProps,I=x.memoizedState,d=n.stateNode,c=d.getSnapshotBeforeUpdate(n.elementType===n.type?v:Le(n.type,v),I);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=n.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(w){W(n,n.return,w)}if(e=n.sibling,e!==null){e.return=n.return,S=e;break}S=n.return}return x=Vo,Vo=!1,x}function Rt(e,n,t){var r=n.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Ui(n,t,i)}l=l.next}while(l!==r)}}function ml(e,n){if(n=n.updateQueue,n=n!==null?n.lastEffect:null,n!==null){var t=n=n.next;do{if((t.tag&e)===e){var r=t.create;t.destroy=r()}t=t.next}while(t!==n)}}function $i(e){var n=e.ref;if(n!==null){var t=e.stateNode;switch(e.tag){case 5:e=t;break;default:e=t}typeof n=="function"?n(e):n.current=e}}function ba(e){var n=e.alternate;n!==null&&(e.alternate=null,ba(n)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(n=e.stateNode,n!==null&&(delete n[Ue],delete n[Qt],delete n[Ni],delete n[cd],delete n[fd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ec(e){return e.tag===5||e.tag===3||e.tag===4}function Ho(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ec(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ai(e,n,t){var r=e.tag;if(r===5||r===6)e=e.stateNode,n?t.nodeType===8?t.parentNode.insertBefore(e,n):t.insertBefore(e,n):(t.nodeType===8?(n=t.parentNode,n.insertBefore(e,t)):(n=t,n.appendChild(e)),t=t._reactRootContainer,t!=null||n.onclick!==null||(n.onclick=Wr));else if(r!==4&&(e=e.child,e!==null))for(Ai(e,n,t),e=e.sibling;e!==null;)Ai(e,n,t),e=e.sibling}function Bi(e,n,t){var r=e.tag;if(r===5||r===6)e=e.stateNode,n?t.insertBefore(e,n):t.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Bi(e,n,t),e=e.sibling;e!==null;)Bi(e,n,t),e=e.sibling}var ee=null,Te=!1;function Je(e,n,t){for(t=t.child;t!==null;)nc(e,n,t),t=t.sibling}function nc(e,n,t){if($e&&typeof $e.onCommitFiberUnmount=="function")try{$e.onCommitFiberUnmount(ol,t)}catch{}switch(t.tag){case 5:ie||Yn(t,n);case 6:var r=ee,l=Te;ee=null,Je(e,n,t),ee=r,Te=l,ee!==null&&(Te?(e=ee,t=t.stateNode,e.nodeType===8?e.parentNode.removeChild(t):e.removeChild(t)):ee.removeChild(t.stateNode));break;case 18:ee!==null&&(Te?(e=ee,t=t.stateNode,e.nodeType===8?Vl(e.parentNode,t):e.nodeType===1&&Vl(e,t),At(e)):Vl(ee,t.stateNode));break;case 4:r=ee,l=Te,ee=t.stateNode.containerInfo,Te=!0,Je(e,n,t),ee=r,Te=l;break;case 0:case 11:case 14:case 15:if(!ie&&(r=t.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,u=i.destroy;i=i.tag,u!==void 0&&(i&2||i&4)&&Ui(t,n,u),l=l.next}while(l!==r)}Je(e,n,t);break;case 1:if(!ie&&(Yn(t,n),r=t.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=t.memoizedProps,r.state=t.memoizedState,r.componentWillUnmount()}catch(s){W(t,n,s)}Je(e,n,t);break;case 21:Je(e,n,t);break;case 22:t.mode&1?(ie=(r=ie)||t.memoizedState!==null,Je(e,n,t),ie=r):Je(e,n,t);break;default:Je(e,n,t)}}function Wo(e){var n=e.updateQueue;if(n!==null){e.updateQueue=null;var t=e.stateNode;t===null&&(t=e.stateNode=new _d),n.forEach(function(r){var l=Fd.bind(null,e,r);t.has(r)||(t.add(r),r.then(l,l))})}}function ze(e,n){var t=n.deletions;if(t!==null)for(var r=0;rl&&(l=u),r&=~i}if(r=l,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Ld(r/1960))-r,10e?16:e,rn===null)var r=!1;else{if(e=rn,rn=null,rl=0,R&6)throw Error(k(331));var l=R;for(R|=4,S=e.current;S!==null;){var i=S,u=i.child;if(S.flags&16){var s=i.deletions;if(s!==null){for(var a=0;aK()-Tu?Nn(e,0):Lu|=t),me(e,n)}function ac(e,n){n===0&&(e.mode&1?(n=cr,cr<<=1,!(cr&130023424)&&(cr=4194304)):n=1);var t=se();e=Ye(e,n),e!==null&&(bt(e,n,t),me(e,t))}function Dd(e){var n=e.memoizedState,t=0;n!==null&&(t=n.retryLane),ac(e,t)}function Fd(e,n){var t=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(t=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(n),ac(e,t)}var cc;cc=function(e,n,t){if(e!==null)if(e.memoizedProps!==n.pendingProps||pe.current)de=!0;else{if(!(e.lanes&t)&&!(n.flags&128))return de=!1,Ed(e,n,t);de=!!(e.flags&131072)}else de=!1,U&&n.flags&1048576&&ha(n,Yr,n.index);switch(n.lanes=0,n.tag){case 2:var r=n.type;Rr(e,n),e=n.pendingProps;var l=rt(n,ue.current);et(n,t),l=Eu(null,n,r,e,l,t);var i=Nu();return n.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(n.tag=1,n.memoizedState=null,n.updateQueue=null,he(r)?(i=!0,Gr(n)):i=!1,n.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,xu(n),l.updater=hl,n.stateNode=l,l._reactInternals=n,Ti(n,r,e,t),n=Ii(null,n,r,!0,i,t)):(n.tag=0,U&&i&&du(n),oe(null,n,l,t),n=n.child),n;case 16:r=n.elementType;e:{switch(Rr(e,n),e=n.pendingProps,l=r._init,r=l(r._payload),n.type=r,l=n.tag=$d(r),e=Le(r,e),l){case 0:n=Mi(null,n,r,e,t);break e;case 1:n=$o(null,n,r,e,t);break e;case 11:n=Fo(null,n,r,e,t);break e;case 14:n=Uo(null,n,r,Le(r.type,e),t);break e}throw Error(k(306,r,""))}return n;case 0:return r=n.type,l=n.pendingProps,l=n.elementType===r?l:Le(r,l),Mi(e,n,r,l,t);case 1:return r=n.type,l=n.pendingProps,l=n.elementType===r?l:Le(r,l),$o(e,n,r,l,t);case 3:e:{if(Ka(n),e===null)throw Error(k(387));r=n.pendingProps,i=n.memoizedState,l=i.element,wa(e,n),Jr(n,r,null,t);var u=n.memoizedState;if(r=u.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},n.updateQueue.baseState=i,n.memoizedState=i,n.flags&256){l=ot(Error(k(423)),n),n=Ao(e,n,r,t,l);break e}else if(r!==l){l=ot(Error(k(424)),n),n=Ao(e,n,r,t,l);break e}else for(ge=sn(n.stateNode.containerInfo.firstChild),ye=n,U=!0,Re=null,t=ya(n,null,r,t),n.child=t;t;)t.flags=t.flags&-3|4096,t=t.sibling;else{if(lt(),r===l){n=Ze(e,n,t);break e}oe(e,n,r,t)}n=n.child}return n;case 5:return ka(n),e===null&&Pi(n),r=n.type,l=n.pendingProps,i=e!==null?e.memoizedProps:null,u=l.children,ji(r,l)?u=null:i!==null&&ji(r,i)&&(n.flags|=32),Ga(e,n),oe(e,n,u,t),n.child;case 6:return e===null&&Pi(n),null;case 13:return Ya(e,n,t);case 4:return wu(n,n.stateNode.containerInfo),r=n.pendingProps,e===null?n.child=it(n,null,r,t):oe(e,n,r,t),n.child;case 11:return r=n.type,l=n.pendingProps,l=n.elementType===r?l:Le(r,l),Fo(e,n,r,l,t);case 7:return oe(e,n,n.pendingProps,t),n.child;case 8:return oe(e,n,n.pendingProps.children,t),n.child;case 12:return oe(e,n,n.pendingProps.children,t),n.child;case 10:e:{if(r=n.type._context,l=n.pendingProps,i=n.memoizedProps,u=l.value,O(Zr,r._currentValue),r._currentValue=u,i!==null)if(Oe(i.value,u)){if(i.children===l.children&&!pe.current){n=Ze(e,n,t);break e}}else for(i=n.child,i!==null&&(i.return=n);i!==null;){var s=i.dependencies;if(s!==null){u=i.child;for(var a=s.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=Qe(-1,t&-t),a.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var g=f.pending;g===null?a.next=a:(a.next=g.next,g.next=a),f.pending=a}}i.lanes|=t,a=i.alternate,a!==null&&(a.lanes|=t),zi(i.return,t,n),s.lanes|=t;break}a=a.next}}else if(i.tag===10)u=i.type===n.type?null:i.child;else if(i.tag===18){if(u=i.return,u===null)throw Error(k(341));u.lanes|=t,s=u.alternate,s!==null&&(s.lanes|=t),zi(u,t,n),u=i.sibling}else u=i.child;if(u!==null)u.return=i;else for(u=i;u!==null;){if(u===n){u=null;break}if(i=u.sibling,i!==null){i.return=u.return,u=i;break}u=u.return}i=u}oe(e,n,l.children,t),n=n.child}return n;case 9:return l=n.type,r=n.pendingProps.children,et(n,t),l=Ce(l),r=r(l),n.flags|=1,oe(e,n,r,t),n.child;case 14:return r=n.type,l=Le(r,n.pendingProps),l=Le(r.type,l),Uo(e,n,r,l,t);case 15:return Wa(e,n,n.type,n.pendingProps,t);case 17:return r=n.type,l=n.pendingProps,l=n.elementType===r?l:Le(r,l),Rr(e,n),n.tag=1,he(r)?(e=!0,Gr(n)):e=!1,et(n,t),Ba(n,r,l),Ti(n,r,l,t),Ii(null,n,r,!0,e,t);case 19:return Za(e,n,t);case 22:return Qa(e,n,t)}throw Error(k(156,n.tag))};function fc(e,n){return Us(e,n)}function Ud(e,n,t,r){this.tag=e,this.key=t,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ee(e,n,t,r){return new Ud(e,n,t,r)}function Ou(e){return e=e.prototype,!(!e||!e.isReactComponent)}function $d(e){if(typeof e=="function")return Ou(e)?1:0;if(e!=null){if(e=e.$$typeof,e===eu)return 11;if(e===nu)return 14}return 2}function dn(e,n){var t=e.alternate;return t===null?(t=Ee(e.tag,n,e.key,e.mode),t.elementType=e.elementType,t.type=e.type,t.stateNode=e.stateNode,t.alternate=e,e.alternate=t):(t.pendingProps=n,t.type=e.type,t.flags=0,t.subtreeFlags=0,t.deletions=null),t.flags=e.flags&14680064,t.childLanes=e.childLanes,t.lanes=e.lanes,t.child=e.child,t.memoizedProps=e.memoizedProps,t.memoizedState=e.memoizedState,t.updateQueue=e.updateQueue,n=e.dependencies,t.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},t.sibling=e.sibling,t.index=e.index,t.ref=e.ref,t}function Or(e,n,t,r,l,i){var u=2;if(r=e,typeof e=="function")Ou(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case $n:return Cn(t.children,l,i,n);case bi:u=8,l|=8;break;case ni:return e=Ee(12,t,n,l|2),e.elementType=ni,e.lanes=i,e;case ti:return e=Ee(13,t,n,l),e.elementType=ti,e.lanes=i,e;case ri:return e=Ee(19,t,n,l),e.elementType=ri,e.lanes=i,e;case ks:return gl(t,l,i,n);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case xs:u=10;break e;case ws:u=9;break e;case eu:u=11;break e;case nu:u=14;break e;case qe:u=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return n=Ee(u,t,n,l),n.elementType=e,n.type=r,n.lanes=i,n}function Cn(e,n,t,r){return e=Ee(7,e,r,n),e.lanes=t,e}function gl(e,n,t,r){return e=Ee(22,e,r,n),e.elementType=ks,e.lanes=t,e.stateNode={isHidden:!1},e}function Xl(e,n,t){return e=Ee(6,e,null,n),e.lanes=t,e}function Jl(e,n,t){return n=Ee(4,e.children!==null?e.children:[],e.key,n),n.lanes=t,n.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},n}function Ad(e,n,t,r,l){this.tag=n,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Tl(0),this.expirationTimes=Tl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Tl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Du(e,n,t,r,l,i,u,s,a){return e=new Ad(e,n,t,s,a),n===1?(n=1,i===!0&&(n|=8)):n=0,i=Ee(3,null,null,n),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:t,cache:null,transitions:null,pendingSuspenseBoundaries:null},xu(i),e}function Bd(e,n,t){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(mc)}catch(e){console.error(e)}}mc(),ms.exports=we;var Gd=ms.exports,qo=Gd;bl.createRoot=qo.createRoot,bl.hydrateRoot=qo.hydrateRoot;async function Fn(e){const n=await fetch(e);if(!n.ok)throw new Error(`${e} → ${n.status}`);return n.json()}const _n={instances:()=>Fn("/api/instances"),instance:e=>Fn(`/api/instances/${encodeURIComponent(e)}`),forges:(e={})=>{const n=new URLSearchParams;e.role&&n.set("role",e.role),e.status&&n.set("status",e.status);const t=n.toString();return Fn("/api/forges"+(t?"?"+t:""))},forge:e=>Fn(`/api/forges/${encodeURIComponent(e)}`),log:(e,n,t=200)=>Fn(`/api/forges/${encodeURIComponent(e)}/log/${n}?tail=${t}`),ledger:(e={})=>{const n=new URLSearchParams;e.limit&&n.set("limit",String(e.limit)),e.shape&&n.set("shape",e.shape),e.outcome&&n.set("outcome",e.outcome),e.role&&n.set("role",e.role);const t=n.toString();return Fn("/api/ledger"+(t?"?"+t:""))}};function Au(e){if(e==null)return"—";if(e<60)return`${e}m`;const n=Math.floor(e/60),t=e%60;return`${n}h${t?` ${t}m`:""}`}function Bu(e){return e.replace("__","/")}function Pn(e,n=[]){const[t,r]=A.useState(null),[l,i]=A.useState(null),[u,s]=A.useState(!0),[a,f]=A.useState(null),[g,m]=A.useState(0),h=A.useCallback(()=>m(y=>y+1),[]);return A.useEffect(()=>{let y=!1;return s(!0),e().then(x=>{y||(r(x),i(null),f(new Date))}).catch(x=>{y||i(x instanceof Error?x.message:String(x))}).finally(()=>{y||s(!1)}),()=>{y=!0}},[...n,g]),{data:t,error:l,loading:u,loadedAt:a,reload:h}}function Sl(e){return e?`loaded ${e.toLocaleTimeString()}`:""}function Kd(){const{data:e,error:n,loading:t,loadedAt:r,reload:l}=Pn(()=>_n.instances(),[]);return o.jsxs("div",{children:[o.jsxs("div",{className:"section-title",children:[o.jsx("span",{children:"Instances"}),e&&o.jsxs("span",{className:"count",children:["(",e.length," installed)"]}),o.jsx("div",{style:{flex:1}}),o.jsx("span",{className:"refresh-info",children:Sl(r)}),o.jsx("button",{onClick:l,disabled:t,children:"Refresh"})]}),n&&o.jsxs("div",{className:"error",children:["error: ",n]}),!n&&e&&e.length===0&&o.jsxs("div",{className:"empty",children:["No instances installed. Run ",o.jsx("code",{children:"/lmstack:install"})," to add one."]}),o.jsx("div",{className:"inst-list",children:e==null?void 0:e.map(i=>o.jsx(Yd,{inst:i},i.role))})]})}function Yd({inst:e}){var r,l,i,u,s,a,f,g,m,h,y;const n=e.counts.running+e.counts.in_review+e.counts.queued+e.counts.merged+e.counts.failed+e.counts.cleaned+e.counts.stale,t=[!e.sources.hostYaml&&"host.yml",!e.sources.probeJson&&"probe.json",!e.sources.classifyJson&&"classify.json"].filter(Boolean);return o.jsxs("section",{className:"inst",children:[o.jsxs("header",{className:"inst-hdr",children:[o.jsx(Zd,{vendor:((r=e.gpu)==null?void 0:r.vendor)??null}),o.jsxs("div",{className:"inst-hdr-main",children:[o.jsx("h2",{children:e.role}),o.jsxs("div",{className:"inst-sub",children:[(l=e.gpu)!=null&&l.model?o.jsx("span",{children:e.gpu.model}):o.jsx("span",{className:"muted",children:"unknown GPU"}),e.engine.kind&&o.jsx("span",{className:"dot",children:"·"}),e.engine.kind&&o.jsx("span",{children:bo(e.engine.kind)}),e.connection&&o.jsx("span",{className:"dot",children:"·"}),e.connection&&o.jsx("span",{children:e.connection==="local"?"localhost":e.connection})]})]}),o.jsxs("div",{className:"inst-hdr-status",children:[e.verdict&&o.jsx("span",{className:`badge verdict-${e.verdict}`,children:e.verdict}),o.jsxs("a",{href:"#/forges",className:"btn-link",children:["view ",n," forge",n===1?"":"s"," →"]})]})]}),t.length>0&&o.jsxs("div",{className:"inst-hint",children:["Missing on disk: ",t.map(x=>o.jsx("code",{children:x},x)).reduce((x,v,I)=>I===0?[v]:[...x,", ",v],[]),"."," ","Run ",o.jsxs("code",{children:["/lmstack:analyze ",e.connection||""]})," to populate hardware & engine details."]}),o.jsxs("div",{className:"inst-grid",children:[o.jsxs(Sr,{title:"Hardware",children:[o.jsx(H,{k:"GPU",v:(i=e.gpu)==null?void 0:i.model}),o.jsx(H,{k:"VRAM",v:jr((u=e.gpu)==null?void 0:u.vramGib)}),o.jsx(H,{k:"GTT",v:jr((s=e.gpu)==null?void 0:s.gttGib)}),o.jsx(H,{k:"Driver",v:(a=e.gpu)==null?void 0:a.driver}),o.jsx(H,{k:"System RAM",v:jr((f=e.memory)==null?void 0:f.totalGib)})]}),o.jsxs(Sr,{title:"Inference engine",children:[o.jsx(H,{k:"Engine",v:bo(e.engine.kind)}),o.jsx(H,{k:"Role",v:e.engine.host||e.role}),o.jsx(H,{k:"Reason",v:e.engine.reason}),o.jsx(H,{k:"Docker",v:ep(e.docker)}),o.jsx(H,{k:"Runtimes",v:((g=e.docker)==null?void 0:g.runtimes.join(", "))||null}),o.jsx(H,{k:"Vulkan",v:e.vulkan?e.vulkan.present?e.vulkan.device||"yes":"no":null})]}),o.jsxs(Sr,{title:"Operating system",children:[o.jsx(H,{k:"OS",v:(m=e.os)==null?void 0:m.pretty}),o.jsx(H,{k:"Kernel",v:(h=e.os)==null?void 0:h.kernel}),o.jsx(H,{k:"Arch",v:(y=e.os)==null?void 0:y.arch}),o.jsx(H,{k:"Installed",v:e.installedAt})]}),o.jsxs(Sr,{title:"Forge activity",children:[o.jsx(H,{k:"Running",v:e.counts.running}),o.jsx(H,{k:"In review",v:e.counts.in_review}),o.jsx(H,{k:"Queued",v:e.counts.queued}),o.jsx(H,{k:"Merged",v:e.counts.merged}),o.jsx(H,{k:"Cleaned",v:e.counts.cleaned}),o.jsx(H,{k:"Failed",v:e.counts.failed}),e.counts.stale>0&&o.jsx(H,{k:"Stale",v:e.counts.stale})]})]}),e.models.length>0&&o.jsxs("div",{className:"inst-models",children:[o.jsx("h3",{children:"Models"}),o.jsxs("table",{className:"table dense",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"slug"}),o.jsx("th",{children:"hf model"}),o.jsx("th",{children:"context"}),o.jsx("th",{children:"max seqs"}),o.jsx("th",{children:"VRAM est."}),o.jsx("th",{children:"port"}),o.jsx("th",{children:"tier"}),o.jsx("th",{children:"active"})]})}),o.jsx("tbody",{children:e.models.map(x=>o.jsxs("tr",{children:[o.jsx("td",{className:"mono",children:x.slug??"—"}),o.jsx("td",{className:"mono",children:x.hfModel??"—"}),o.jsx("td",{className:"mono",children:bd(x.contextTokens)}),o.jsx("td",{className:"mono",children:x.maxNumSeqs??"—"}),o.jsx("td",{className:"mono",children:jr(x.vramEstimateGib)}),o.jsx("td",{className:"mono",children:x.port??"—"}),o.jsx("td",{className:"mono",children:x.tier??"—"}),o.jsx("td",{children:x.active?o.jsx("span",{className:"badge merged",children:"active"}):o.jsx("span",{className:"badge cleaned",children:"idle"})})]},x.slug??String(Math.random())))})]})]}),(e.arithmetic.length>0||e.warnings.length>0)&&o.jsxs("div",{className:"inst-extra",children:[e.arithmetic.length>0&&o.jsxs("div",{className:"inst-extra-block",children:[o.jsx("h3",{children:"Memory arithmetic"}),o.jsx("pre",{children:e.arithmetic.join(` +`)})]}),e.warnings.length>0&&o.jsxs("div",{className:"inst-extra-block",children:[o.jsx("h3",{children:"Warnings"}),o.jsx("ul",{children:e.warnings.map((x,v)=>o.jsx("li",{children:x},v))})]})]})]})}function Sr({title:e,children:n}){return o.jsxs("div",{className:"inst-sec",children:[o.jsx("h3",{children:e}),o.jsx("dl",{children:n})]})}function H({k:e,v:n}){const t=n==null||n===""||n===void 0;return o.jsxs(o.Fragment,{children:[o.jsx("dt",{children:e}),o.jsx("dd",{className:t?"muted":"",children:t?"—":n})]})}function Zd({vendor:e}){const n=(e||"").toLowerCase();return n==="nvidia"?o.jsxs("div",{className:"vendor vendor-nvidia","aria-label":"NVIDIA",children:[o.jsx(Xd,{}),o.jsx("span",{children:"NVIDIA"})]}):n==="amd"?o.jsxs("div",{className:"vendor vendor-amd","aria-label":"AMD",children:[o.jsx(Jd,{}),o.jsx("span",{children:"AMD"})]}):o.jsxs("div",{className:"vendor vendor-generic","aria-label":"GPU",children:[o.jsx(qd,{}),o.jsx("span",{children:"GPU"})]})}function Xd(){return o.jsxs("svg",{viewBox:"0 0 48 48",fill:"none","aria-hidden":"true",children:[o.jsx("rect",{width:"48",height:"48",rx:"10",fill:"#76b900"}),o.jsx("text",{x:"24",y:"33",textAnchor:"middle",fontFamily:"Inter, Arial, sans-serif",fontWeight:"800",fontSize:"26",fill:"#ffffff",children:"N"})]})}function Jd(){return o.jsxs("svg",{viewBox:"0 0 48 48",fill:"none","aria-hidden":"true",children:[o.jsx("rect",{width:"48",height:"48",rx:"10",fill:"#ed1c24"}),o.jsx("text",{x:"24",y:"33",textAnchor:"middle",fontFamily:"Inter, Arial, sans-serif",fontWeight:"800",fontSize:"26",fill:"#ffffff",children:"A"})]})}function qd(){return o.jsxs("svg",{viewBox:"0 0 48 48",fill:"none","aria-hidden":"true",children:[o.jsx("rect",{width:"48",height:"48",rx:"10",fill:"#656d76"}),o.jsx("text",{x:"24",y:"33",textAnchor:"middle",fontFamily:"Inter, Arial, sans-serif",fontWeight:"800",fontSize:"18",fill:"#ffffff",children:"GPU"})]})}function jr(e){return e==null?null:`${e} GiB`}function bd(e){return e==null?"—":e>=1024?`${(e/1024).toFixed(e%1024===0?0:1)}K`:String(e)}function ep(e){return e?e.present?e.usable?e.version||"usable":`${e.version??"present"} (not usable)`:"not installed":null}function bo(e){return e?{vllm:"vLLM",llamacpp:"llama.cpp"}[e]??e:null}const es={queued:"○ queued",running:"● running","in-review":"◐ in-review",merged:"✓ merged",failed:"✗ failed",cleaned:"· cleaned",stale:"! stale"};function vc({status:e}){const n=e in es?e:"queued";return o.jsx("span",{className:`badge ${n}`,children:es[n]??e})}const np=[{title:"Running",statuses:["running"]},{title:"In review",statuses:["in-review"]},{title:"Queued",statuses:["queued"]},{title:"Completed",statuses:["merged","cleaned"]},{title:"Failed",statuses:["failed"]},{title:"Stale",statuses:["stale"]}];function tp(){var h;const[e,n]=A.useState(""),[t,r]=A.useState(""),{data:l,error:i,loading:u,loadedAt:s,reload:a}=Pn(()=>_n.forges(),[]),f=Pn(()=>_n.instances(),[]),g=A.useMemo(()=>l?l.filter(y=>!(e&&y.role!==e||t&&y.status!==t)):null,[l,e,t]),m=A.useMemo(()=>g?np.map(y=>({title:y.title,forges:g.filter(x=>y.statuses.includes(x.status))})):null,[g]);return o.jsxs("div",{children:[o.jsxs("div",{className:"section-title",children:[o.jsx("span",{children:"Forges"}),g&&o.jsxs("span",{className:"count",children:["(",g.length," shown)"]}),o.jsx("div",{style:{flex:1}}),o.jsx("span",{className:"refresh-info",children:Sl(s)}),o.jsx("button",{onClick:a,disabled:u,children:"Refresh"})]}),o.jsxs("div",{className:"filters",children:[o.jsx("label",{children:"host: "}),o.jsxs("select",{value:e,onChange:y=>n(y.target.value),children:[o.jsx("option",{value:"",children:"all"}),(h=f.data)==null?void 0:h.map(y=>o.jsx("option",{value:y.role,children:y.role},y.role))]}),o.jsx("label",{children:"status: "}),o.jsxs("select",{value:t,onChange:y=>r(y.target.value),children:[o.jsx("option",{value:"",children:"all"}),o.jsx("option",{value:"running",children:"running"}),o.jsx("option",{value:"in-review",children:"in-review"}),o.jsx("option",{value:"queued",children:"queued"}),o.jsx("option",{value:"merged",children:"merged"}),o.jsx("option",{value:"cleaned",children:"cleaned"}),o.jsx("option",{value:"failed",children:"failed"}),o.jsx("option",{value:"stale",children:"stale"})]})]}),i&&o.jsxs("div",{className:"error",children:["error: ",i]}),g&&g.length===0&&!i&&o.jsx("div",{className:"empty",children:"No forges match the current filters."}),m==null?void 0:m.map(y=>y.forges.length>0?o.jsx(rp,{title:y.title,forges:y.forges},y.title):null)]})}function rp({title:e,forges:n}){return o.jsxs("div",{style:{marginTop:16},children:[o.jsxs("div",{className:"section-title",children:[o.jsx("span",{children:e}),o.jsxs("span",{className:"count",children:["(",n.length,")"]})]}),o.jsxs("table",{className:"table",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"status"}),o.jsx("th",{children:"key"}),o.jsx("th",{children:"shape"}),o.jsx("th",{children:"tier"}),o.jsx("th",{children:"host"}),o.jsx("th",{children:"wall"}),o.jsx("th",{children:"judge"}),o.jsx("th",{children:"pr"})]})}),o.jsx("tbody",{children:n.map(t=>o.jsxs("tr",{onClick:()=>{window.location.hash=`#/forge/${encodeURIComponent(t.key)}`},children:[o.jsx("td",{children:o.jsx(vc,{status:t.status})}),o.jsx("td",{className:"key",children:Bu(t.key)}),o.jsx("td",{children:t.shape??"—"}),o.jsx("td",{children:t.tier??"—"}),o.jsx("td",{children:t.role}),o.jsx("td",{className:"mono",children:Au(t.wallMin)}),o.jsx("td",{className:"mono",children:t.judgeRounds||"—"}),o.jsx("td",{className:"mono",children:t.pr??"—"})]},t.key))})]})]})}function lp({forgeKey:e}){const n=Pn(()=>_n.forge(e),[e]),t=Pn(()=>_n.log(e,"exec",200),[e]),r=Pn(()=>_n.log(e,"judge",200),[e]),l=()=>{n.reload(),t.reload(),r.reload()},i=n.loading||t.loading||r.loading;return o.jsxs("div",{children:[o.jsxs("div",{className:"section-title",children:[o.jsx("a",{href:"#/forges",children:"← forges"}),o.jsx("div",{style:{flex:1}}),o.jsx("span",{className:"refresh-info",children:Sl(n.loadedAt)}),o.jsx("button",{onClick:l,disabled:i,children:"Refresh"})]}),n.error&&o.jsxs("div",{className:"error",children:["error: ",n.error]}),n.data&&o.jsx(ip,{data:n.data,execLog:t.data,judgeLog:r.data})]})}function ip({data:e,execLog:n,judgeLog:t}){const{task:r,run:l,ledger:i}=e,u=r.status??"queued",s=r.url,a=r.pr,f=r.tier,g=r.shape;return o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"detail-header",children:[o.jsx("span",{className:"title",children:Bu(e.task.key)}),o.jsx(vc,{status:u}),s&&o.jsx("a",{href:s,target:"_blank",rel:"noreferrer",children:"↗ issue"}),a&&o.jsx("a",{href:a,target:"_blank",rel:"noreferrer",children:"↗ pr"})]}),o.jsxs("div",{className:"detail-meta",children:[o.jsx("span",{className:"k",children:"host"}),o.jsx("span",{className:"v",children:e.role}),o.jsx("span",{className:"k",children:"shape"}),o.jsx("span",{className:"v",children:g??"—"}),o.jsx("span",{className:"k",children:"tier"}),o.jsx("span",{className:"v",children:f??"—"}),o.jsx("span",{className:"k",children:"title"}),o.jsx("span",{className:"v",style:{fontFamily:"inherit"},children:r.title??"—"}),o.jsx("span",{className:"k",children:"started"}),o.jsx("span",{className:"v",children:l.startedAt??"—"}),o.jsx("span",{className:"k",children:"ended"}),o.jsx("span",{className:"v",children:l.endedAt??"—"}),o.jsx("span",{className:"k",children:"tmux session"}),o.jsxs("span",{className:"v",children:["lmstack-",e.slug," ",l.tmuxAlive?"(alive ✓)":"(not running)"]}),o.jsx("span",{className:"k",children:"worktree"}),o.jsxs("span",{className:"v",children:[l.worktreePath??"—"," ",l.worktreePath?l.worktreeExists?"":"(missing)":""]}),o.jsx("span",{className:"k",children:"branch"}),o.jsx("span",{className:"v",children:l.branch??"—"}),i&&o.jsxs(o.Fragment,{children:[o.jsx("span",{className:"k",children:"outcome"}),o.jsxs("span",{className:"v",children:[String(i.outcome??"—")," · wall ",Au(i.wall_min)," · interventions ",String(i.interventions??0)]})]})]}),o.jsxs("div",{className:"panel",children:[o.jsx("h3",{children:"Brief"}),l.brief?o.jsx("pre",{children:l.brief}):o.jsx("div",{className:"empty",children:"no brief.md on disk"})]}),o.jsxs("div",{className:"panel",children:[o.jsxs("h3",{children:["Judge rounds (",l.judgeRounds.length,")"]}),l.judgeRounds.length===0&&o.jsx("div",{className:"empty",children:"(none yet)"}),l.judgeRounds.map(m=>o.jsxs("div",{style:{marginBottom:12},children:[o.jsxs("div",{style:{color:"var(--muted)",fontSize:12,marginBottom:4},children:["round ",m.n]}),o.jsx("pre",{children:m.text})]},m.n))]}),o.jsxs("div",{className:"logs",children:[o.jsx(ns,{title:"lm-exec",log:n}),o.jsx(ns,{title:"lm-judge",log:t})]})]})}function ns({title:e,log:n}){return o.jsxs("div",{className:"log-pane",children:[o.jsxs("h4",{children:[e,n!=null&&n.truncated?" — tail":""]}),!n&&o.jsx("div",{className:"empty",children:"loading…"}),n&&!n.exists&&o.jsxs("div",{className:"empty",children:["no log file at ",n.path]}),n&&n.exists&&n.lines.length===0&&o.jsx("div",{className:"empty",children:"(empty)"}),n&&n.exists&&n.lines.length>0&&o.jsx("pre",{children:n.lines.join(` +`)})]})}function up(){const{data:e,error:n,loading:t,loadedAt:r,reload:l}=Pn(()=>_n.ledger({limit:500}),[]),[i,u]=A.useState(""),[s,a]=A.useState(""),[f,g]=A.useState(""),m=A.useMemo(()=>ql(e==null?void 0:e.map(v=>v.shape).filter(v=>!!v)),[e]),h=A.useMemo(()=>ql(e==null?void 0:e.map(v=>v.outcome).filter(v=>!!v)),[e]),y=A.useMemo(()=>ql(e==null?void 0:e.map(v=>v.host_role).filter(v=>!!v)),[e]),x=A.useMemo(()=>e?e.filter(v=>!(i&&v.shape!==i||s&&v.outcome!==s||f&&v.host_role!==f)):null,[e,i,s,f]);return o.jsxs("div",{children:[o.jsxs("div",{className:"section-title",children:[o.jsx("span",{children:"Ledger"}),x&&o.jsxs("span",{className:"count",children:["(",x.length," of ",(e==null?void 0:e.length)??0," runs)"]}),o.jsx("div",{style:{flex:1}}),o.jsx("span",{className:"refresh-info",children:Sl(r)}),o.jsx("button",{onClick:l,disabled:t,children:"Refresh"})]}),o.jsxs("div",{className:"filters",children:[o.jsx("label",{children:"shape: "}),o.jsxs("select",{value:i,onChange:v=>u(v.target.value),children:[o.jsx("option",{value:"",children:"all"}),m.map(v=>o.jsx("option",{value:v,children:v},v))]}),o.jsx("label",{children:"outcome: "}),o.jsxs("select",{value:s,onChange:v=>a(v.target.value),children:[o.jsx("option",{value:"",children:"all"}),h.map(v=>o.jsx("option",{value:v,children:v},v))]}),o.jsx("label",{children:"role: "}),o.jsxs("select",{value:f,onChange:v=>g(v.target.value),children:[o.jsx("option",{value:"",children:"all"}),y.map(v=>o.jsx("option",{value:v,children:v},v))]})]}),n&&o.jsxs("div",{className:"error",children:["error: ",n]}),x&&x.length===0&&!n&&o.jsx("div",{className:"empty",children:"No ledger entries."}),x&&x.length>0&&o.jsxs("table",{className:"table",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"ended (UTC)"}),o.jsx("th",{children:"key"}),o.jsx("th",{children:"shape"}),o.jsx("th",{children:"tier"}),o.jsx("th",{children:"host"}),o.jsx("th",{children:"outcome"}),o.jsx("th",{children:"wall"}),o.jsx("th",{children:"judge"}),o.jsx("th",{children:"pr"}),o.jsx("th",{children:"int."})]})}),o.jsx("tbody",{children:x.map((v,I)=>o.jsxs("tr",{onClick:()=>{window.location.hash=`#/forge/${encodeURIComponent(v.key)}`},children:[o.jsx("td",{className:"mono",children:op(v.ended??v.ts)}),o.jsx("td",{className:"key",children:Bu(v.key)}),o.jsx("td",{children:v.shape??"—"}),o.jsx("td",{children:v.tier??"—"}),o.jsx("td",{children:v.host_role}),o.jsx("td",{children:v.outcome}),o.jsx("td",{className:"mono",children:Au(v.wall_min)}),o.jsx("td",{className:"mono",children:v.judge_rounds||"—"}),o.jsx("td",{className:"mono",children:v.pr??"—"}),o.jsx("td",{className:"mono",children:v.interventions??0})]},`${v.key}-${v.ts}-${I}`))})]})]})}function ql(e){return e?Array.from(new Set(e)).sort():[]}function op(e){return e?e.replace("T"," ").replace(/\..*Z?$/,"").replace(/Z$/,""):"—"}const sp="https://github.com/ric03uec/lmstack",ap="https://ric03uec.github.io/lmstack/";function ts(){const e=window.location.hash.replace(/^#\/?/,"");return!e||e==="instances"?{name:"instances"}:e==="forges"?{name:"forges"}:e==="ledger"?{name:"ledger"}:e.startsWith("forge/")?{name:"forge",key:decodeURIComponent(e.slice(6))}:{name:"instances"}}function cp(){const[e,n]=A.useState(ts());A.useEffect(()=>{const r=()=>n(ts());return window.addEventListener("hashchange",r),()=>window.removeEventListener("hashchange",r)},[]);const t=r=>e.name===r||r==="forges"&&e.name==="forge";return o.jsxs("div",{className:"app",children:[o.jsxs("header",{className:"header",children:[o.jsxs("a",{className:"brand",href:"#/instances",children:[o.jsx("img",{src:"./lmstack.svg",alt:""}),o.jsx("span",{className:"name",children:"lmstack"}),o.jsx("span",{className:"tagline",children:"put your GPUs to work"})]}),o.jsx("div",{className:"spacer"}),o.jsxs("div",{className:"links",children:[o.jsxs("a",{href:ap,target:"_blank",rel:"noreferrer",title:"Documentation",children:[o.jsx(rs,{})," Docs"]}),o.jsxs("a",{href:sp,target:"_blank",rel:"noreferrer",title:"Source on GitHub",children:[o.jsx(fp,{})," GitHub"]})]})]}),o.jsx("aside",{className:"sidebar",children:o.jsxs("nav",{children:[o.jsxs("a",{href:"#/instances",className:t("instances")?"active":"",children:[o.jsx("span",{className:"icon",children:o.jsx(dp,{})})," Instances"]}),o.jsxs("a",{href:"#/forges",className:t("forges")?"active":"",children:[o.jsx("span",{className:"icon",children:o.jsx(pp,{})})," Forges"]}),o.jsxs("a",{href:"#/ledger",className:t("ledger")?"active":"",children:[o.jsx("span",{className:"icon",children:o.jsx(rs,{})})," Ledger"]})]})}),o.jsxs("main",{className:"main",children:[e.name==="instances"&&o.jsx(Kd,{}),e.name==="forges"&&o.jsx(tp,{}),e.name==="forge"&&o.jsx(lp,{forgeKey:e.key}),e.name==="ledger"&&o.jsx(up,{})]})]})}function fp(){return o.jsx("svg",{viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:o.jsx("path",{d:"M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"})})}function rs(){return o.jsx("svg",{viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:o.jsx("path",{d:"M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574Zm1.504-5.076v5.076a3.75 3.75 0 0 1 1.994-.574H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.248l-.004 2.25Z"})})}function dp(){return o.jsx("svg",{viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:o.jsx("path",{d:"M2 2.75C2 1.784 2.784 1 3.75 1h8.5c.966 0 1.75.784 1.75 1.75v2.5A1.75 1.75 0 0 1 12.25 7h-8.5A1.75 1.75 0 0 1 2 5.25Zm1.75-.25a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Zm-1.75 8c0-.966.784-1.75 1.75-1.75h8.5c.966 0 1.75.784 1.75 1.75v2.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25ZM5 4a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm-1 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"})})}function pp(){return o.jsx("svg",{viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:o.jsx("path",{d:"M9.53.22a.75.75 0 0 1 1.06 0l4.19 4.19a.75.75 0 0 1 0 1.06L13.06 7l1.03 1.03a1.75 1.75 0 0 1 0 2.48l-1.55 1.54a1.75 1.75 0 0 1-2.47 0L8 9l-6.42 6.42a.75.75 0 1 1-1.06-1.06L6.94 8 3.85 4.9a.75.75 0 0 1 0-1.06L5.4 2.29a1.75 1.75 0 0 1 2.47 0L9 3.44 9.53.22Zm-3.72 3.13a.25.25 0 0 0-.35 0L4.47 4.35 8.5 8.38l1.35-1.35Z"})})}bl.createRoot(document.getElementById("root")).render(o.jsx(Ic.StrictMode,{children:o.jsx(cp,{})})); diff --git a/ui/dist/assets/index-C39-GzIs.css b/ui/dist/assets/index-C39-GzIs.css new file mode 100644 index 0000000..056adcf --- /dev/null +++ b/ui/dist/assets/index-C39-GzIs.css @@ -0,0 +1 @@ +:root{--bg: #f6f8fa;--panel: #ffffff;--panel-hi: #f0f3f6;--border: #d0d7de;--border-strong: #afb8c1;--fg: #1f2328;--muted: #656d76;--accent: #0969da;--accent-soft: #ddf4ff;--ok: #1a7f37;--warn: #9a6700;--err: #cf222e;--run: #0969da;--review: #9a6700;--queued: #656d76;--sidebar-w: 220px;--header-h: 56px;--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font-family:var(--font-sans);font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{background:var(--panel);color:var(--fg);border:1px solid var(--border-strong);padding:6px 14px;border-radius:6px;cursor:pointer;font:inherit;font-size:14px}button:hover{border-color:var(--accent);color:var(--accent)}button:disabled{opacity:.5;cursor:default}select,input[type=text]{background:var(--panel);color:var(--fg);border:1px solid var(--border-strong);padding:6px 10px;border-radius:6px;font:inherit;font-size:14px}code,pre{font-family:var(--font-mono)}.app{display:grid;grid-template-columns:var(--sidebar-w) 1fr;grid-template-rows:var(--header-h) 1fr;grid-template-areas:"header header" "sidebar main";min-height:100vh}.header{grid-area:header;background:var(--panel);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:0 20px}.header .brand{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;color:var(--fg)}.header .brand img{width:28px;height:28px;display:block}.header .brand .name{letter-spacing:-.01em}.header .brand .tagline{color:var(--muted);font-size:13.5px;font-weight:400;letter-spacing:0;padding-left:10px;margin-left:4px;border-left:1px solid var(--border)}.header .brand a{color:inherit}.header .spacer{flex:1}.header .links{display:flex;align-items:center;gap:16px}.header .links a{color:var(--muted);font-size:14px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:6px}.header .links a:hover{background:var(--panel-hi);color:var(--fg);text-decoration:none}.header .links svg{width:18px;height:18px}.sidebar{grid-area:sidebar;background:var(--panel);border-right:1px solid var(--border);padding:16px 8px}.sidebar nav{display:flex;flex-direction:column;gap:2px}.sidebar nav a{display:flex;align-items:center;gap:10px;padding:8px 14px;border-radius:6px;color:var(--fg);font-size:15px;font-weight:500}.sidebar nav a:hover{background:var(--panel-hi);text-decoration:none}.sidebar nav a.active{background:var(--accent-soft);color:var(--accent)}.sidebar nav a .icon{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center}.sidebar nav a .icon svg{width:18px;height:18px}.main{grid-area:main;padding:24px 28px;max-width:1400px}.section-title{display:flex;align-items:center;gap:12px;margin:8px 0 14px;font-weight:600;font-size:20px}.section-title .count{color:var(--muted);font-weight:400;font-size:14px}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px}.card{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:16px 18px;box-shadow:0 1px #1f23280a}.card h3{margin:0 0 8px;font-size:17px}.card .row{display:flex;justify-content:space-between;color:var(--muted);font-size:14px;padding:2px 0}.card .row strong{color:var(--fg);font-weight:500;text-align:right;margin-left:12px}.inst-list{display:flex;flex-direction:column;gap:16px}.inst{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:20px 24px;box-shadow:0 1px #1f23280a}.inst-hdr{display:flex;align-items:center;gap:16px;margin-bottom:14px}.vendor{display:flex;flex-direction:column;align-items:center;gap:4px;min-width:56px}.vendor svg{width:48px;height:48px;display:block}.vendor span{font-size:11px;font-weight:600;color:var(--muted);letter-spacing:.4px}.vendor-nvidia span{color:#4b7f00}.vendor-amd span{color:#b31219}.inst-hdr-main{flex:1;min-width:0}.inst-hdr-main h2{margin:0;font-size:22px;font-weight:600;letter-spacing:-.01em;font-family:var(--font-mono)}.inst-sub{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:14px;margin-top:4px;flex-wrap:wrap}.inst-sub .dot{opacity:.5}.inst-sub .muted{font-style:italic}.inst-hdr-status{display:flex;align-items:center;gap:12px}.btn-link{color:var(--accent);font-size:14px;padding:6px 12px;border:1px solid var(--border-strong);border-radius:6px;background:var(--panel)}.btn-link:hover{border-color:var(--accent);background:var(--accent-soft);text-decoration:none}.badge.verdict-supported{color:var(--ok);border-color:var(--ok);background:#dcffe4}.badge.verdict-unsupported{color:var(--err);border-color:var(--err);background:#ffebe9}.inst-hint{background:#fff8c5;border:1px solid #d4a72c;color:#63410b;padding:10px 14px;border-radius:6px;font-size:14px;margin-bottom:14px}.inst-hint code{background:#0000000f;padding:1px 6px;border-radius:3px;font-size:13px}.inst-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px 32px}.inst-sec h3{margin:0 0 8px;font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px}.inst-sec dl{margin:0;display:grid;grid-template-columns:max-content 1fr;gap:4px 14px;font-size:14px}.inst-sec dt{color:var(--muted)}.inst-sec dd{margin:0;color:var(--fg);font-family:var(--font-mono);font-size:13.5px;overflow-wrap:anywhere}.inst-sec dd.muted{color:var(--muted);font-family:var(--font-sans);font-style:italic}.inst-models{margin-top:20px}.inst-models h3{margin:0 0 8px;font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px}.table.dense th,.table.dense td{padding:6px 10px;font-size:13.5px}.table.dense tbody tr{cursor:default}.table.dense tbody tr:hover{background:transparent}.inst-extra{margin-top:20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}.inst-extra-block h3{margin:0 0 6px;font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px}.inst-extra-block pre{margin:0;padding:10px 12px;background:var(--panel-hi);border:1px solid var(--border);border-radius:6px;font-size:12.5px;white-space:pre-wrap}.inst-extra-block ul{margin:0;padding-left:20px;font-size:13.5px}@media (max-width: 900px){.inst-extra{grid-template-columns:1fr}}.table{width:100%;border-collapse:collapse;font-size:14px;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden}.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--border);text-align:left;vertical-align:middle}.table thead th{background:var(--panel-hi);color:var(--muted);font-weight:500;font-size:13px;text-transform:uppercase;letter-spacing:.4px}.table tbody tr{cursor:pointer}.table tbody tr:hover{background:var(--panel-hi)}.table tbody tr:last-child td{border-bottom:0}.table td.key{font-family:var(--font-mono);font-size:13.5px}.table td.mono{font-family:var(--font-mono);color:var(--muted);font-size:13.5px}.badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12.5px;font-weight:500;background:var(--panel-hi);border:1px solid var(--border);color:var(--muted);white-space:nowrap}.badge.running{color:var(--run);border-color:var(--run);background:var(--accent-soft)}.badge.in-review{color:var(--review);border-color:var(--review);background:#fff8c5}.badge.queued{color:var(--queued)}.badge.merged,.badge.completed{color:var(--ok);border-color:var(--ok);background:#dcffe4}.badge.failed{color:var(--err);border-color:var(--err);background:#ffebe9}.badge.cleaned{color:var(--muted)}.badge.stale{color:var(--warn);border-color:var(--warn);background:#fff8c5}.filters{display:flex;gap:14px;align-items:center;margin-bottom:14px;flex-wrap:wrap}.filters label{color:var(--muted);font-size:14px}.detail-header{display:flex;align-items:center;gap:14px;margin:4px 0 14px;flex-wrap:wrap}.detail-header .title{font-size:22px;font-weight:600;font-family:var(--font-mono)}.detail-meta{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:14px;font-size:14px;display:grid;grid-template-columns:max-content 1fr;gap:6px 16px}.detail-meta .k{color:var(--muted)}.detail-meta .v{font-family:var(--font-mono);font-size:13.5px;word-break:break-all}.panel{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:14px}.panel h3{margin:0 0 10px;font-size:13px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.5px}.panel pre{margin:0;white-space:pre-wrap;word-break:break-word;font-size:13.5px;color:var(--fg)}.logs{display:grid;grid-template-columns:1fr 1fr;gap:14px}.log-pane{background:#fff;border:1px solid var(--border);border-radius:8px;height:460px;overflow:auto;padding:10px 14px}.log-pane h4{margin:0 0 8px;font-size:12px;color:var(--muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.5px}.log-pane pre{margin:0;font-size:13px;color:var(--fg);white-space:pre}.empty{color:var(--muted);font-style:italic;padding:8px 0;font-size:14px}.error{color:var(--err);padding:10px 14px;border:1px solid var(--err);border-radius:6px;background:#ffebe9;margin:12px 0}.refresh-info{color:var(--muted);font-size:13px;margin-right:10px}@media (max-width: 720px){.app{grid-template-columns:1fr;grid-template-areas:"header" "main"}.sidebar{display:none}.main{padding:16px}.logs{grid-template-columns:1fr}} diff --git a/ui/dist/index.html b/ui/dist/index.html new file mode 100644 index 0000000..377e670 --- /dev/null +++ b/ui/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + lmstack — put your GPUs to work + + + + +
+ + diff --git a/ui/dist/lmstack.svg b/ui/dist/lmstack.svg new file mode 100644 index 0000000..eef803a --- /dev/null +++ b/ui/dist/lmstack.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..978c3bf --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + lmstack — put your GPUs to work + + +
+ + + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..40a402b --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,1792 @@ +{ + "name": "lmstack-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lmstack-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.400", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", + "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..e9af552 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,22 @@ +{ + "name": "lmstack-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/ui/public/lmstack.svg b/ui/public/lmstack.svg new file mode 100644 index 0000000..eef803a --- /dev/null +++ b/ui/public/lmstack.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/public/vendor/README.md b/ui/public/vendor/README.md new file mode 100644 index 0000000..58ed684 --- /dev/null +++ b/ui/public/vendor/README.md @@ -0,0 +1,17 @@ +# Vendor marks + +Ship an SVG here named after the probe's `gpu.vendor` string and the Instances +page renders it in place of the built-in stylised fallback: + + nvidia.svg # served at /vendor/nvidia.svg + amd.svg # served at /vendor/amd.svg + +`nvidia.svg` and `amd.svg` ship in this directory — their paths come from +[simple-icons](https://simpleicons.org/) (CC0), wrapped in a brand-hue tile +so the SVG file is self-contained: no CSS on the consuming end has to know +the vendor's colour. `VendorMark` in `../../src/routes/Instances.tsx` falls +back to a stylised geometric mark if a file is missing, so the SPA still +renders on a checkout with this directory empty. + +To swap in a different mark — a company brand asset, a different icon set — +overwrite the SVG here. Nothing in the SPA code needs to change. diff --git a/ui/public/vendor/amd.svg b/ui/public/vendor/amd.svg new file mode 100644 index 0000000..c10d52c --- /dev/null +++ b/ui/public/vendor/amd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/public/vendor/nvidia.svg b/ui/public/vendor/nvidia.svg new file mode 100644 index 0000000..0b09ec3 --- /dev/null +++ b/ui/public/vendor/nvidia.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..c0ec5c0 --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react'; +import { Instances } from './routes/Instances'; +import { Forges } from './routes/Forges'; +import { ForgeDetail } from './routes/ForgeDetail'; +import { Ledger } from './routes/Ledger'; + +type Route = + | { name: 'instances' } + | { name: 'forges' } + | { name: 'forge'; key: string } + | { name: 'ledger' }; + +const REPO_URL = 'https://github.com/ric03uec/lmstack'; +const DOCS_URL = 'https://ric03uec.github.io/lmstack/'; + +function parseHash(): Route { + const h = window.location.hash.replace(/^#\/?/, ''); + if (!h || h === 'instances') return { name: 'instances' }; + if (h === 'forges') return { name: 'forges' }; + if (h === 'ledger') return { name: 'ledger' }; + if (h.startsWith('forge/')) return { name: 'forge', key: decodeURIComponent(h.slice('forge/'.length)) }; + return { name: 'instances' }; +} + +export function App() { + const [route, setRoute] = useState(parseHash()); + + useEffect(() => { + const on = () => setRoute(parseHash()); + window.addEventListener('hashchange', on); + return () => window.removeEventListener('hashchange', on); + }, []); + + const isActive = (name: string) => + route.name === name || (name === 'forges' && route.name === 'forge'); + + return ( +
+
+ + + lmstack + put your GPUs to work + +
+ + + +
+ {route.name === 'instances' && } + {route.name === 'forges' && } + {route.name === 'forge' && } + {route.name === 'ledger' && } +
+
+ ); +} + +// ── icons (inline SVG, currentColor) ───────────────────────────────────── + +function GithubIcon() { + return ( + + ); +} + +function BookIcon() { + return ( + + ); +} + +function ServerIcon() { + return ( + + ); +} + +function HammerIcon() { + return ( + + ); +} diff --git a/ui/src/api.ts b/ui/src/api.ts new file mode 100644 index 0000000..fb01ead --- /dev/null +++ b/ui/src/api.ts @@ -0,0 +1,140 @@ +export type ForgeStatus = + | 'queued' + | 'running' + | 'in-review' + | 'merged' + | 'failed' + | 'cleaned' + | 'stale'; + +export interface InstanceModel { + slug: string | null; + hfModel?: string | null; + engine?: string | null; + port?: number | null; + contextTokens?: number | null; + maxNumSeqs?: number | null; + vramEstimateGib?: number | null; + tier?: string | null; + quant?: string | null; + active: boolean; +} + +export interface Instance { + role: string; + connection: string | null; + installedAt: string | null; + verdict: string | null; + engine: { kind: string | null; reason: string | null; host: string | null }; + gpu: { vendor: string | null; model: string | null; vramGib: number | null; gttGib: number | null; driver: string | null } | null; + os: { pretty: string | null; kernel: string | null; arch: string | null } | null; + memory: { totalGib: number } | null; + docker: { present: boolean; usable: boolean; version: string | null; runtimes: string[] } | null; + vulkan: { present: boolean; device: string | null } | null; + arithmetic: string[]; + warnings: string[]; + activeModels: string[]; + models: InstanceModel[]; + counts: { running: number; in_review: number; queued: number; merged: number; failed: number; cleaned: number; stale: number }; + sources: { hostYaml: boolean; probeJson: boolean; classifyJson: boolean }; +} + +export interface ForgeSummary { + key: string; + role: string; + status: ForgeStatus; + tier?: string; + shape?: string; + pr?: string; + wallMin?: number | null; + startedAt?: string | null; + judgeRounds: number; + title?: string; + url?: string; +} + +export interface JudgeRound { n: number; text: string; } + +export interface ForgeDetail { + task: Record & { key: string; status?: string; url?: string; title?: string }; + run: { + exists: boolean; + brief: string | null; + judgeRounds: JudgeRound[]; + startedAt: string | null; + endedAt: string | null; + tmuxAlive: boolean; + worktreePath: string | null; + worktreeExists: boolean; + branch: string | null; + }; + ledger: Record | null; + role: string; + slug: string; +} + +export interface LogTail { + lines: string[]; + sizeBytes: number; + truncated: boolean; + exists: boolean; + path: string; +} + +export interface LedgerRecord { + key: string; + host_role: string; + tier: string | null; + shape: string | null; + judge_rounds: number; + outcome: string; + pr: string | null; + started: string | null; + ended: string | null; + wall_min: number | null; + interventions: number; + ts: string; +} + +async function j(path: string): Promise { + const r = await fetch(path); + if (!r.ok) throw new Error(`${path} → ${r.status}`); + return r.json(); +} + +export const api = { + instances: () => j('/api/instances'), + instance: (role: string) => j>(`/api/instances/${encodeURIComponent(role)}`), + forges: (params: { role?: string; status?: string } = {}) => { + const qs = new URLSearchParams(); + if (params.role) qs.set('role', params.role); + if (params.status) qs.set('status', params.status); + const q = qs.toString(); + return j('/api/forges' + (q ? '?' + q : '')); + }, + forge: (key: string) => j(`/api/forges/${encodeURIComponent(key)}`), + log: (key: string, which: 'exec' | 'judge', tail = 200) => + j(`/api/forges/${encodeURIComponent(key)}/log/${which}?tail=${tail}`), + ledger: (params: { limit?: number; shape?: string; outcome?: string; role?: string } = {}) => { + const qs = new URLSearchParams(); + if (params.limit) qs.set('limit', String(params.limit)); + if (params.shape) qs.set('shape', params.shape); + if (params.outcome) qs.set('outcome', params.outcome); + if (params.role) qs.set('role', params.role); + const q = qs.toString(); + return j('/api/ledger' + (q ? '?' + q : '')); + }, +}; + +export function fmtWall(min: number | null | undefined): string { + if (min == null) return '—'; + if (min < 60) return `${min}m`; + const h = Math.floor(min / 60); + const m = min % 60; + return `${h}h${m ? ` ${m}m` : ''}`; +} + +export function shortKey(key: string): string { + // owner__repo#num → owner/repo#num + return key.replace('__', '/'); +} diff --git a/ui/src/components/StatusBadge.tsx b/ui/src/components/StatusBadge.tsx new file mode 100644 index 0000000..3db39e1 --- /dev/null +++ b/ui/src/components/StatusBadge.tsx @@ -0,0 +1,16 @@ +import type { ForgeStatus } from '../api'; + +const LABEL: Record = { + queued: '○ queued', + running: '● running', + 'in-review': '◐ in-review', + merged: '✓ merged', + failed: '✗ failed', + cleaned: '· cleaned', + stale: '! stale', +}; + +export function StatusBadge({ status }: { status: ForgeStatus | string }) { + const key = (status as ForgeStatus) in LABEL ? (status as ForgeStatus) : ('queued' as ForgeStatus); + return {LABEL[key] ?? status}; +} diff --git a/ui/src/hooks.ts b/ui/src/hooks.ts new file mode 100644 index 0000000..3aa0fe8 --- /dev/null +++ b/ui/src/hooks.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useState } from 'react'; + +export interface Loadable { + data: T | null; + error: string | null; + loading: boolean; + loadedAt: Date | null; + reload: () => void; +} + +export function useAsync(fn: () => Promise, deps: unknown[] = []): Loadable { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [loadedAt, setLoadedAt] = useState(null); + const [tick, setTick] = useState(0); + + const reload = useCallback(() => setTick((t) => t + 1), []); + + useEffect(() => { + let cancelled = false; + setLoading(true); + fn() + .then((v) => { + if (cancelled) return; + setData(v); + setError(null); + setLoadedAt(new Date()); + }) + .catch((e: unknown) => { + if (cancelled) return; + setError(e instanceof Error ? e.message : String(e)); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps, tick]); + + return { data, error, loading, loadedAt, reload }; +} + +export function fmtLoaded(loadedAt: Date | null): string { + if (!loadedAt) return ''; + return `loaded ${loadedAt.toLocaleTimeString()}`; +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..ed0dc63 --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/ui/src/routes/ForgeDetail.tsx b/ui/src/routes/ForgeDetail.tsx new file mode 100644 index 0000000..83628d7 --- /dev/null +++ b/ui/src/routes/ForgeDetail.tsx @@ -0,0 +1,105 @@ +import { api, fmtWall, shortKey } from '../api'; +import { fmtLoaded, useAsync } from '../hooks'; +import { StatusBadge } from '../components/StatusBadge'; + +export function ForgeDetail({ forgeKey }: { forgeKey: string }) { + const detail = useAsync(() => api.forge(forgeKey), [forgeKey]); + const execLog = useAsync(() => api.log(forgeKey, 'exec', 200), [forgeKey]); + const judgeLog = useAsync(() => api.log(forgeKey, 'judge', 200), [forgeKey]); + + const refreshAll = () => { + detail.reload(); + execLog.reload(); + judgeLog.reload(); + }; + + const loading = detail.loading || execLog.loading || judgeLog.loading; + + return ( +
+
+ ← forges +
+ {fmtLoaded(detail.loadedAt)} + +
+ + {detail.error &&
error: {detail.error}
} + {detail.data && } +
+ ); +} + +function Detail({ data, execLog, judgeLog }: { + data: import('../api').ForgeDetail; + execLog: import('../api').LogTail | null; + judgeLog: import('../api').LogTail | null; +}) { + const { task, run, ledger } = data; + const status = (task.status as string) ?? 'queued'; + const url = task.url as string | undefined; + const pr = (task as { pr?: string }).pr; + const tier = (task as { tier?: string }).tier; + const shape = (task as { shape?: string }).shape; + + return ( + <> +
+ {shortKey(data.task.key)} + + {url && ↗ issue} + {pr && ↗ pr} +
+ +
+ host{data.role} + shape{shape ?? '—'} + tier{tier ?? '—'} + title{(task.title as string) ?? '—'} + started{run.startedAt ?? '—'} + ended{run.endedAt ?? '—'} + tmux sessionlmstack-{data.slug} {run.tmuxAlive ? '(alive ✓)' : '(not running)'} + worktree{run.worktreePath ?? '—'} {run.worktreePath ? (run.worktreeExists ? '' : '(missing)') : ''} + branch{run.branch ?? '—'} + {ledger && ( + <> + outcome{String(ledger.outcome ?? '—')} · wall {fmtWall(ledger.wall_min as number | null)} · interventions {String(ledger.interventions ?? 0)} + + )} +
+ +
+

Brief

+ {run.brief ?
{run.brief}
:
no brief.md on disk
} +
+ +
+

Judge rounds ({run.judgeRounds.length})

+ {run.judgeRounds.length === 0 &&
(none yet)
} + {run.judgeRounds.map((r) => ( +
+
round {r.n}
+
{r.text}
+
+ ))} +
+ +
+ + +
+ + ); +} + +function LogPane({ title, log }: { title: string; log: import('../api').LogTail | null }) { + return ( +
+

{title}{log?.truncated ? ' — tail' : ''}

+ {!log &&
loading…
} + {log && !log.exists &&
no log file at {log.path}
} + {log && log.exists && log.lines.length === 0 &&
(empty)
} + {log && log.exists && log.lines.length > 0 &&
{log.lines.join('\n')}
} +
+ ); +} diff --git a/ui/src/routes/Forges.tsx b/ui/src/routes/Forges.tsx new file mode 100644 index 0000000..f003088 --- /dev/null +++ b/ui/src/routes/Forges.tsx @@ -0,0 +1,118 @@ +import { useMemo, useState } from 'react'; +import { api, fmtWall, shortKey, type ForgeStatus, type ForgeSummary } from '../api'; +import { fmtLoaded, useAsync } from '../hooks'; +import { StatusBadge } from '../components/StatusBadge'; + +const GROUPS: { title: string; statuses: ForgeStatus[] }[] = [ + { title: 'Running', statuses: ['running'] }, + { title: 'In review', statuses: ['in-review'] }, + { title: 'Queued', statuses: ['queued'] }, + { title: 'Completed', statuses: ['merged', 'cleaned'] }, + { title: 'Failed', statuses: ['failed'] }, + { title: 'Stale', statuses: ['stale'] }, +]; + +export function Forges() { + const [role, setRole] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const { data, error, loading, loadedAt, reload } = useAsync(() => api.forges(), []); + const instancesReq = useAsync(() => api.instances(), []); + + const filtered = useMemo(() => { + if (!data) return null; + return data.filter((f) => { + if (role && f.role !== role) return false; + if (statusFilter && f.status !== statusFilter) return false; + return true; + }); + }, [data, role, statusFilter]); + + const grouped = useMemo(() => { + if (!filtered) return null; + return GROUPS.map((g) => ({ + title: g.title, + forges: filtered.filter((f) => g.statuses.includes(f.status as ForgeStatus)), + })); + }, [filtered]); + + return ( +
+
+ Forges + {filtered && ({filtered.length} shown)} +
+ {fmtLoaded(loadedAt)} + +
+ +
+ + + + +
+ + {error &&
error: {error}
} + {filtered && filtered.length === 0 && !error && ( +
No forges match the current filters.
+ )} + + {grouped?.map((g) => (g.forges.length > 0 ? ( + + ) : null))} +
+ ); +} + +function ForgeGroup({ title, forges }: { title: string; forges: ForgeSummary[] }) { + return ( +
+
+ {title} + ({forges.length}) +
+ + + + + + + + + + + + + + + {forges.map((f) => ( + { window.location.hash = `#/forge/${encodeURIComponent(f.key)}`; }}> + + + + + + + + + + ))} + +
statuskeyshapetierhostwalljudgepr
{shortKey(f.key)}{f.shape ?? '—'}{f.tier ?? '—'}{f.role}{fmtWall(f.wallMin)}{f.judgeRounds || '—'}{f.pr ?? '—'}
+
+ ); +} diff --git a/ui/src/routes/Instances.tsx b/ui/src/routes/Instances.tsx new file mode 100644 index 0000000..9e89829 --- /dev/null +++ b/ui/src/routes/Instances.tsx @@ -0,0 +1,273 @@ +import { useState, type ReactNode } from 'react'; +import { api, type Instance } from '../api'; +import { fmtLoaded, useAsync } from '../hooks'; + +export function Instances() { + const { data, error, loading, loadedAt, reload } = useAsync(() => api.instances(), []); + + return ( +
+
+ Instances + {data && ({data.length} installed)} +
+ {fmtLoaded(loadedAt)} + +
+ + {error &&
error: {error}
} + {!error && data && data.length === 0 && ( +
No instances installed. Run /lmstack:install to add one.
+ )} + +
+ {data?.map((inst) => )} +
+
+ ); +} + +function InstanceCard({ inst }: { inst: Instance }) { + const total = inst.counts.running + inst.counts.in_review + inst.counts.queued + + inst.counts.merged + inst.counts.failed + inst.counts.cleaned + inst.counts.stale; + const missing = [ + !inst.sources.hostYaml && 'host.yml', + !inst.sources.probeJson && 'probe.json', + !inst.sources.classifyJson && 'classify.json', + ].filter(Boolean) as string[]; + + return ( +
+
+ +
+

{inst.role}

+
+ {inst.gpu?.model ? {inst.gpu.model} : unknown GPU} + {inst.engine.kind && ·} + {inst.engine.kind && {prettyEngine(inst.engine.kind)}} + {inst.connection && ·} + {inst.connection && {inst.connection === 'local' ? 'localhost' : inst.connection}} +
+
+
+ {inst.verdict && {inst.verdict}} + view {total} forge{total === 1 ? '' : 's'} → +
+
+ + {missing.length > 0 && ( +
+ Missing on disk: {missing.map((m) => {m}).reduce((acc, el, i) => (i === 0 ? [el] : [...acc, ', ', el]), [])}. + {' '}Run /lmstack:analyze {inst.connection || ''} to populate hardware & engine details. +
+ )} + +
+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + {inst.counts.stale > 0 && } +
+
+ + {inst.models.length > 0 && ( +
+

Models

+ + + + + + + + + + + + + + + {inst.models.map((m) => ( + + + + + + + + + + + ))} + +
slughf modelcontextmax seqsVRAM est.porttieractive
{m.slug ?? '—'}{m.hfModel ?? '—'}{fmtCtx(m.contextTokens)}{m.maxNumSeqs ?? '—'}{fmtGib(m.vramEstimateGib)}{m.port ?? '—'}{m.tier ?? '—'}{m.active ? active : idle}
+
+ )} + + {(inst.arithmetic.length > 0 || inst.warnings.length > 0) && ( +
+ {inst.arithmetic.length > 0 && ( +
+

Memory arithmetic

+
{inst.arithmetic.join('\n')}
+
+ )} + {inst.warnings.length > 0 && ( +
+

Warnings

+
    {inst.warnings.map((w, i) =>
  • {w}
  • )}
+
+ )} +
+ )} +
+ ); +} + +function Section({ title, children }: { title: string; children: ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function Row({ k, v }: { k: string; v: ReactNode | number | null | undefined }) { + const empty = v == null || v === '' || v === undefined; + return ( + <> +
{k}
+
{empty ? '—' : v}
+ + ); +} + +// Vendor marks: prefer a real logo file at /vendor/.svg (drop the +// official SVG from the vendor's press kit into ui/public/vendor/ and it +// takes over — nothing to change in this file). Fall back to a stylised +// geometric mark when the file is not there, so a fresh checkout still shows +// something the user recognises. +// +// Falling back to a big letter — the previous behaviour, "a big N" and "a big +// A" — read as a placeholder rather than a badge, which is what motivated the +// rewrite. A geometric mark reads as intentional even before the official +// file is added. +function VendorLogo({ vendor }: { vendor: string | null }) { + const v = (vendor || '').toLowerCase(); + const spec = VENDOR_MARKS[v] || VENDOR_MARKS._generic; + return ( +
+ + {spec.label} +
+ ); +} + +type MarkSpec = { label: string; color: string; fallback: JSX.Element }; + +const VENDOR_MARKS: Record = { + nvidia: { label: 'NVIDIA', color: '#76B900', fallback: }, + amd: { label: 'AMD', color: '#000000', fallback: }, + _generic: { label: 'GPU', color: '#656D76', fallback: }, +}; + +function VendorMark({ slug, spec }: { slug: string; spec: MarkSpec }) { + const [failed, setFailed] = useState(false); + if (!slug || slug === '_generic' || failed) return spec.fallback; + // The onError fires when the file is missing, so the fallback SVG + // still renders — no 404 in the console for the common "no override" case + // once the error has been caught. + return ( + setFailed(true)} + /> + ); +} + +// Stylised marks, used until ui/public/vendor/.svg is dropped in. Not +// the trademarked wordmarks — an eye-shaped lens for NVIDIA, a chevroned "A" +// on black for AMD, a card silhouette for the generic case. +function NvidiaMark() { + return ( + + ); +} +function AmdMark() { + return ( + + ); +} +function GpuMark() { + return ( + + ); +} + +function fmtGib(g: number | null | undefined): string | null { + if (g == null) return null; + return `${g} GiB`; +} +function fmtCtx(t: number | null | undefined): string { + if (t == null) return '—'; + if (t >= 1024) return `${(t / 1024).toFixed(t % 1024 === 0 ? 0 : 1)}K`; + return String(t); +} +function fmtDocker(d: Instance['docker']): string | null { + if (!d) return null; + if (!d.present) return 'not installed'; + if (!d.usable) return `${d.version ?? 'present'} (not usable)`; + return d.version || 'usable'; +} +function prettyEngine(k: string | null | undefined): string | null { + if (!k) return null; + const m: Record = { vllm: 'vLLM', llamacpp: 'llama.cpp' }; + return m[k] ?? k; +} diff --git a/ui/src/routes/Ledger.tsx b/ui/src/routes/Ledger.tsx new file mode 100644 index 0000000..c782152 --- /dev/null +++ b/ui/src/routes/Ledger.tsx @@ -0,0 +1,101 @@ +import { useMemo, useState } from 'react'; +import { api, fmtWall, shortKey } from '../api'; +import { fmtLoaded, useAsync } from '../hooks'; + +export function Ledger() { + const { data, error, loading, loadedAt, reload } = useAsync(() => api.ledger({ limit: 500 }), []); + const [shape, setShape] = useState(''); + const [outcome, setOutcome] = useState(''); + const [role, setRole] = useState(''); + + const shapes = useMemo(() => uniq(data?.map((r) => r.shape).filter((v): v is string => !!v)), [data]); + const outcomes = useMemo(() => uniq(data?.map((r) => r.outcome).filter((v): v is string => !!v)), [data]); + const roles = useMemo(() => uniq(data?.map((r) => r.host_role).filter((v): v is string => !!v)), [data]); + + const filtered = useMemo(() => { + if (!data) return null; + return data.filter((r) => { + if (shape && r.shape !== shape) return false; + if (outcome && r.outcome !== outcome) return false; + if (role && r.host_role !== role) return false; + return true; + }); + }, [data, shape, outcome, role]); + + return ( +
+
+ Ledger + {filtered && ({filtered.length} of {data?.length ?? 0} runs)} +
+ {fmtLoaded(loadedAt)} + +
+ +
+ + + + + + +
+ + {error &&
error: {error}
} + {filtered && filtered.length === 0 && !error &&
No ledger entries.
} + {filtered && filtered.length > 0 && ( + + + + + + + + + + + + + + + + + {filtered.map((r, i) => ( + { window.location.hash = `#/forge/${encodeURIComponent(r.key)}`; }}> + + + + + + + + + + + + ))} + +
ended (UTC)keyshapetierhostoutcomewalljudgeprint.
{fmtEnded(r.ended ?? r.ts)}{shortKey(r.key)}{r.shape ?? '—'}{r.tier ?? '—'}{r.host_role}{r.outcome}{fmtWall(r.wall_min)}{r.judge_rounds || '—'}{r.pr ?? '—'}{r.interventions ?? 0}
+ )} +
+ ); +} + +function uniq(arr: string[] | undefined): string[] { + if (!arr) return []; + return Array.from(new Set(arr)).sort(); +} + +function fmtEnded(s: string | null | undefined): string { + if (!s) return '—'; + return s.replace('T', ' ').replace(/\..*Z?$/, '').replace(/Z$/, ''); +} diff --git a/ui/src/styles.css b/ui/src/styles.css new file mode 100644 index 0000000..4e4bd4a --- /dev/null +++ b/ui/src/styles.css @@ -0,0 +1,471 @@ +:root { + --bg: #f6f8fa; + --panel: #ffffff; + --panel-hi: #f0f3f6; + --border: #d0d7de; + --border-strong: #afb8c1; + --fg: #1f2328; + --muted: #656d76; + --accent: #0969da; + --accent-soft: #ddf4ff; + --ok: #1a7f37; + --warn: #9a6700; + --err: #cf222e; + --run: #0969da; + --review: #9a6700; + --queued: #656d76; + --sidebar-w: 220px; + --header-h: 56px; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; +} + +* { box-sizing: border-box; } +html, body, #root { height: 100%; margin: 0; } +body { + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } +button { + background: var(--panel); + color: var(--fg); + border: 1px solid var(--border-strong); + padding: 6px 14px; + border-radius: 6px; + cursor: pointer; + font: inherit; + font-size: 14px; +} +button:hover { border-color: var(--accent); color: var(--accent); } +button:disabled { opacity: 0.5; cursor: default; } +select, input[type="text"] { + background: var(--panel); + color: var(--fg); + border: 1px solid var(--border-strong); + padding: 6px 10px; + border-radius: 6px; + font: inherit; + font-size: 14px; +} +code, pre { font-family: var(--font-mono); } + +/* ─── layout ─────────────────────────────────────────────────────────── */ + +.app { + display: grid; + grid-template-columns: var(--sidebar-w) 1fr; + grid-template-rows: var(--header-h) 1fr; + grid-template-areas: + "header header" + "sidebar main"; + min-height: 100vh; +} + +.header { + grid-area: header; + background: var(--panel); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 12px; + padding: 0 20px; +} +.header .brand { + display: flex; align-items: center; gap: 10px; + font-size: 18px; font-weight: 600; + color: var(--fg); +} +.header .brand img { width: 28px; height: 28px; display: block; } +.header .brand .name { letter-spacing: -0.01em; } +.header .brand .tagline { + color: var(--muted); + font-size: 13.5px; + font-weight: 400; + letter-spacing: 0; + padding-left: 10px; + margin-left: 4px; + border-left: 1px solid var(--border); +} +.header .brand a { color: inherit; } +.header .spacer { flex: 1; } +.header .links { display: flex; align-items: center; gap: 16px; } +.header .links a { + color: var(--muted); + font-size: 14px; + display: inline-flex; align-items: center; gap: 6px; + padding: 6px 10px; + border-radius: 6px; +} +.header .links a:hover { background: var(--panel-hi); color: var(--fg); text-decoration: none; } +.header .links svg { width: 18px; height: 18px; } + +.sidebar { + grid-area: sidebar; + background: var(--panel); + border-right: 1px solid var(--border); + padding: 16px 8px; +} +.sidebar nav { display: flex; flex-direction: column; gap: 2px; } +.sidebar nav a { + display: flex; align-items: center; gap: 10px; + padding: 8px 14px; + border-radius: 6px; + color: var(--fg); + font-size: 15px; + font-weight: 500; +} +.sidebar nav a:hover { background: var(--panel-hi); text-decoration: none; } +.sidebar nav a.active { + background: var(--accent-soft); + color: var(--accent); +} +.sidebar nav a .icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; } +.sidebar nav a .icon svg { width: 18px; height: 18px; } + +.main { + grid-area: main; + padding: 24px 28px; + max-width: 1400px; +} + +/* ─── content ────────────────────────────────────────────────────────── */ + +.section-title { + display: flex; + align-items: center; + gap: 12px; + margin: 8px 0 14px; + font-weight: 600; + font-size: 20px; +} +.section-title .count { + color: var(--muted); + font-weight: normal; + font-size: 14px; +} + +.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 14px; } +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px 18px; + box-shadow: 0 1px 0 rgba(31,35,40,0.04); +} +.card h3 { margin: 0 0 8px; font-size: 17px; } +.card .row { + display: flex; + justify-content: space-between; + color: var(--muted); + font-size: 14px; + padding: 2px 0; +} +.card .row strong { color: var(--fg); font-weight: 500; text-align: right; margin-left: 12px; } + +/* ─── full-width instance card ─────────────────────────────────────── */ + +.inst-list { display: flex; flex-direction: column; gap: 16px; } +.inst { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 10px; + padding: 20px 24px; + box-shadow: 0 1px 0 rgba(31,35,40,0.04); +} +.inst-hdr { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 14px; +} +.vendor { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + min-width: 56px; +} +.vendor svg { width: 48px; height: 48px; display: block; } +.vendor span { font-size: 11px; font-weight: 600; color: var(--muted); letter-spacing: 0.4px; } +.vendor-nvidia span { color: #4b7f00; } +.vendor-amd span { color: #b31219; } + +.inst-hdr-main { flex: 1; min-width: 0; } +.inst-hdr-main h2 { + margin: 0; + font-size: 22px; + font-weight: 600; + letter-spacing: -0.01em; + font-family: var(--font-mono); +} +.inst-sub { + display: flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 14px; + margin-top: 4px; + flex-wrap: wrap; +} +.inst-sub .dot { opacity: 0.5; } +.inst-sub .muted { font-style: italic; } + +.inst-hdr-status { + display: flex; + align-items: center; + gap: 12px; +} +.btn-link { + color: var(--accent); + font-size: 14px; + padding: 6px 12px; + border: 1px solid var(--border-strong); + border-radius: 6px; + background: var(--panel); +} +.btn-link:hover { border-color: var(--accent); background: var(--accent-soft); text-decoration: none; } + +.badge.verdict-supported { color: var(--ok); border-color: var(--ok); background: #dcffe4; } +.badge.verdict-unsupported { color: var(--err); border-color: var(--err); background: #ffebe9; } + +.inst-hint { + background: #fff8c5; + border: 1px solid #d4a72c; + color: #63410b; + padding: 10px 14px; + border-radius: 6px; + font-size: 14px; + margin-bottom: 14px; +} +.inst-hint code { + background: rgba(0,0,0,0.06); + padding: 1px 6px; + border-radius: 3px; + font-size: 13px; +} + +.inst-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 20px 32px; +} +.inst-sec h3 { + margin: 0 0 8px; + font-size: 12px; + color: var(--muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.inst-sec dl { + margin: 0; + display: grid; + grid-template-columns: max-content 1fr; + gap: 4px 14px; + font-size: 14px; +} +.inst-sec dt { color: var(--muted); } +.inst-sec dd { + margin: 0; + color: var(--fg); + font-family: var(--font-mono); + font-size: 13.5px; + overflow-wrap: anywhere; +} +.inst-sec dd.muted { color: var(--muted); font-family: var(--font-sans); font-style: italic; } + +.inst-models { margin-top: 20px; } +.inst-models h3 { + margin: 0 0 8px; + font-size: 12px; + color: var(--muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.table.dense th, .table.dense td { padding: 6px 10px; font-size: 13.5px; } +.table.dense tbody tr { cursor: default; } +.table.dense tbody tr:hover { background: transparent; } + +.inst-extra { + margin-top: 20px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} +.inst-extra-block h3 { + margin: 0 0 6px; + font-size: 12px; + color: var(--muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.inst-extra-block pre { + margin: 0; + padding: 10px 12px; + background: var(--panel-hi); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 12.5px; + white-space: pre-wrap; +} +.inst-extra-block ul { margin: 0; padding-left: 20px; font-size: 13.5px; } +@media (max-width: 900px) { .inst-extra { grid-template-columns: 1fr; } } + +.table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} +.table th, .table td { + padding: 10px 12px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: middle; +} +.table thead th { + background: var(--panel-hi); + color: var(--muted); + font-weight: 500; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.4px; +} +.table tbody tr { cursor: pointer; } +.table tbody tr:hover { background: var(--panel-hi); } +.table tbody tr:last-child td { border-bottom: 0; } +.table td.key { font-family: var(--font-mono); font-size: 13.5px; } +.table td.mono { font-family: var(--font-mono); color: var(--muted); font-size: 13.5px; } + +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 12.5px; + font-weight: 500; + background: var(--panel-hi); + border: 1px solid var(--border); + color: var(--muted); + white-space: nowrap; +} +.badge.running { color: var(--run); border-color: var(--run); background: var(--accent-soft); } +.badge.in-review { color: var(--review); border-color: var(--review); background: #fff8c5; } +.badge.queued { color: var(--queued); } +.badge.merged, .badge.completed { color: var(--ok); border-color: var(--ok); background: #dcffe4; } +.badge.failed { color: var(--err); border-color: var(--err); background: #ffebe9; } +.badge.cleaned { color: var(--muted); } +.badge.stale { color: var(--warn); border-color: var(--warn); background: #fff8c5; } + +.filters { + display: flex; + gap: 14px; + align-items: center; + margin-bottom: 14px; + flex-wrap: wrap; +} +.filters label { color: var(--muted); font-size: 14px; } + +.detail-header { + display: flex; + align-items: center; + gap: 14px; + margin: 4px 0 14px; + flex-wrap: wrap; +} +.detail-header .title { font-size: 22px; font-weight: 600; font-family: var(--font-mono); } + +.detail-meta { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 14px 18px; + margin-bottom: 14px; + font-size: 14px; + display: grid; + grid-template-columns: max-content 1fr; + gap: 6px 16px; +} +.detail-meta .k { color: var(--muted); } +.detail-meta .v { font-family: var(--font-mono); font-size: 13.5px; word-break: break-all; } + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 14px 18px; + margin-bottom: 14px; +} +.panel h3 { + margin: 0 0 10px; + font-size: 13px; + color: var(--muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.panel pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 13.5px; + color: var(--fg); +} + +.logs { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.log-pane { + background: #ffffff; + border: 1px solid var(--border); + border-radius: 8px; + height: 460px; + overflow: auto; + padding: 10px 14px; +} +.log-pane h4 { + margin: 0 0 8px; + font-size: 12px; + color: var(--muted); + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.5px; +} +.log-pane pre { + margin: 0; + font-size: 13px; + color: var(--fg); + white-space: pre; +} + +.empty { + color: var(--muted); + font-style: italic; + padding: 8px 0; + font-size: 14px; +} +.error { + color: var(--err); + padding: 10px 14px; + border: 1px solid var(--err); + border-radius: 6px; + background: #ffebe9; + margin: 12px 0; +} +.refresh-info { color: var(--muted); font-size: 13px; margin-right: 10px; } + +@media (max-width: 720px) { + .app { grid-template-columns: 1fr; grid-template-areas: "header" "main"; } + .sidebar { display: none; } + .main { padding: 16px; } + .logs { grid-template-columns: 1fr; } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..c95ee7f --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..a5d317e --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: false, + }, + server: { + port: 5173, + proxy: { + '/api': 'http://127.0.0.1:7878', + }, + }, +});