From d143aeeab951151e8deb0c8dd0f277f820194690 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 21:20:02 -0400 Subject: [PATCH 01/10] docs: llm generated frontend docs --- docs/README.md | 1 + docs/frontend-architecture.md | 98 + scripts/fake-backend.mjs | 1061 ++++++++ web-interface/package.json | 1 + web-interface/pnpm-lock.yaml | 4599 +++++++++++++++++++++++++++++++++ 5 files changed, 5760 insertions(+) create mode 100644 docs/frontend-architecture.md create mode 100644 scripts/fake-backend.mjs create mode 100644 web-interface/pnpm-lock.yaml diff --git a/docs/README.md b/docs/README.md index 17a1067..ce6536a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ host-specific settings when provisioning another environment. | Authenticate and submit from a terminal | [CLI](../cli/docs/setup.md) | | Run automated checks and read recorded live evidence | [Testing](testing.md) | | Maintain UI components and interactions | [Design system](design-system.md) | +| Understand frontend architecture and backend offloading | [Frontend architecture](frontend-architecture.md) | | Read completed scope, exclusions and release verification | [Execution record](department-rollout-execution.md) | | See what repository cleanup removed and verified | [Cleanup record](repository-cleanup.md) | diff --git a/docs/frontend-architecture.md b/docs/frontend-architecture.md new file mode 100644 index 0000000..41e2c50 --- /dev/null +++ b/docs/frontend-architecture.md @@ -0,0 +1,98 @@ +# Frontend Architecture & Backend Offloading + +Documentation of the current Mist frontend ([`web-interface`](../web-interface)), its structural organization, and a roadmap of client-side processing to offload to the backend. + +## Architecture overview + +```mermaid +flowchart TD + User([User Browser]) --> Shell[Root Shell: __root.tsx] + Shell --> AG[AccountGate: auth.tsx] + AG --> TG[TeamGate: teams.tsx] + TG --> Nav[Navbar.tsx] + TG --> RouterOutlet[TanStack Router Outlet] + + RouterOutlet --> DPage[Dashboard: dashboard.tsx] + RouterOutlet --> JPage[Jobs Page: jobs.tsx] + RouterOutlet --> SPage[Datasets Page: datasets.tsx] + RouterOutlet --> MPage[Machines Page: machines.tsx] + RouterOutlet --> TPage[Teams Page: teams.tsx] + RouterOutlet --> PPage[Profile: profile.tsx] + + JPage --> JSF[JobSubmissionForm.tsx] + JPage --> JC[JobCard.tsx] + JC --> JL[JobLogs.tsx] + JC --> JF[JobFiles.tsx] + + SPage --> TSB[TeamStorageBrowser.tsx] +``` + +### Technology stack + +- **Vite 7.3**: Build tool and development proxy (`/api` and `/auth` forwards). +- **React 19.3**: Functional component architecture with custom hooks. +- **TanStack Router**: File-based routing located in `src/routes/` with code-splitting. +- **Tailwind CSS 4**: Modern styling via `@tailwindcss/vite` and customized Mist theme classes. +- **Better Auth Client**: Authentication state and admin member management. +- **Vitest 5 & Testing Library**: Unit and component tests. + +### Source layout + +| Location | Purpose | +|---|---| +| `src/main.tsx`, `router.tsx` | Router initialization and DOM hydration | +| `src/routes/__root.tsx` | Shell wrapper containing `AccountGate`, `TeamGate`, and `Navbar` | +| `src/routes/` | File-based routes (`dashboard.tsx`, `jobs.tsx`, `datasets.tsx`, `machines.tsx`, `teams.tsx`, `profile.tsx`) | +| `src/auth.tsx` | Better Auth client configuration, `AccountContext`, and `AccountGate` | +| `src/teams.tsx` | `TeamContext`, active team selection, workspace selector | +| `src/api.ts` | Typed HTTP client for jobs, datasets, hardware, and teams | +| `src/hooks/usePolling.ts` | In-flight request deduplication and abortable periodic polling | +| `src/components/jobs/` | `JobSubmissionForm`, `JobCard`, `JobLogs`, `JobFiles` | +| `src/components/` | `HardwarePanel`, `TeamStorageBrowser`, `Modal`, `Picker`, `Tabs`, `Card`, `Buttons` | + +--- + +## Backend offloading plan + +To keep the frontend as lightweight and simple as possible, the following responsibilities currently performed on the client should be handled by the backend: + +1. **Pagination and filtering**: + - `JobsPage`: Replace client-side slicing and array filtering with query parameters on `GET /jobs?status=...&q=...&page=...&limit=...`. + - `DatasetsPage`: Add `GET /datasets?q=...`. + - `AccountPage`: Replace Better Auth `listUsers({ limit: 1000 })` with a paginated endpoint `GET /members?page=...&q=...`. +2. **Metrics aggregation**: + - Provide `GET /metrics/summary` or summary counters in list endpoints to avoid transferring full item arrays just to compute badge totals. +3. **Image catalog and validation**: + - Move registry string parsing and image approval checks from `JobSubmissionForm` to server-side submission validation. +4. **Quota and limit calculation**: + - Replace client-side calculation of `maxDevices` and `maxTimeout` with authoritative constraints provided by the team policy API. +5. **Form parsing**: + - Accept raw multiline environment variable text and shell commands on `POST /jobs`, delegating parsing and validation to the server. +6. **Error status handling**: + - Replace English error string matching (e.g. `isPreparing`) with typed machine-readable status fields (`{ status: "provisioning" }`). +7. **Permission flags**: + - Return explicit capability booleans (`can_cancel`, `can_delete`, `can_manage`) on entities rather than parsing role strings on the client. +8. **Atomic operations**: + - Consolidate multi-call client sequences (e.g. team creation + creator enrollment, password reset + session revocation) into atomic backend operations. +9. **Event-driven updates**: + - Replace high-frequency polling with Server-Sent Events (SSE) or WebSockets for live job status and log streaming. + +--- + +## Testing with the fake backend + +A self-contained testing backend is provided in `scripts/fake-backend.mjs`. It simulates all Mist API endpoints and Better Auth handlers in memory. + +### Running locally + +```bash +# Terminal 1: Start fake backend (default port 3000) +npm --prefix web-interface run fake-backend +``` + +```bash +# Terminal 2: Start frontend dev server (port 3001) +npm --prefix web-interface run dev -- --host 127.0.0.1 --port 3001 --strictPort +``` + +Open http://127.0.0.1:3001 in your browser. Pre-seeded users include `ada@utmist.gitlab.io` (admin), `alan@utmist.gitlab.io`, and `grace@utmist.gitlab.io`. diff --git a/scripts/fake-backend.mjs b/scripts/fake-backend.mjs new file mode 100644 index 0000000..90c8c29 --- /dev/null +++ b/scripts/fake-backend.mjs @@ -0,0 +1,1061 @@ +#!/usr/bin/env node +/** + * Mist Fake Backend Server + * + * A standalone, zero-dependency Node.js HTTP server simulating the Mist Go API + * and Better Auth service. Designed for local frontend development, testing, + * and rapid prototyping without requiring k3s, GPUs, or Better Auth SQLite. + * + * Usage: + * node scripts/fake-backend.mjs + * PORT=3000 node scripts/fake-backend.mjs + * npm --prefix web-interface run fake-backend + */ + +import http from 'node:http' +import { URL } from 'node:url' +import crypto from 'node:crypto' + +const PORT = Number(process.env.PORT || process.env.MIST_FAKE_PORT || 3000) +const HOST = process.env.HOST || '127.0.0.1' + +// --- In-Memory State & Seed Data --- + +const state = { + users: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + role: 'admin', + banned: false, + }, + { + id: 'usr_alan', + name: 'Alan Turing', + email: 'alan@utmist.gitlab.io', + role: 'user', + banned: false, + }, + { + id: 'usr_grace', + name: 'Grace Hopper', + email: 'grace@utmist.gitlab.io', + role: 'user', + banned: false, + }, + ], + currentUserIndex: 0, // Ada Lovelace by default + teams: [ + { + id: 'team-vision', + name: 'Computer Vision', + disabled: false, + policy: { + cpu: '16', + memory: '64Gi', + nvidia: 2, + tenstorrent: 4, + concurrent: 4, + queued: 20, + storage_gib: 300, + model_storage_gib: 600, + runtime_seconds: 86400, + registries: ['docker.io', 'ghcr.io', 'quay.io'], + }, + members: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + common_writer: true, + }, + { + id: 'usr_alan', + name: 'Alan Turing', + email: 'alan@utmist.gitlab.io', + common_writer: false, + }, + ], + grants: [ + { + id: 'grant-nlp-share', + target_team: 'team-nlp', + scope: 'common', + }, + ], + }, + { + id: 'team-nlp', + name: 'Natural Language Processing', + disabled: false, + policy: { + cpu: '8', + memory: '32Gi', + nvidia: 1, + tenstorrent: 2, + concurrent: 2, + queued: 10, + storage_gib: 200, + model_storage_gib: 400, + runtime_seconds: 43200, + registries: ['docker.io', 'ghcr.io'], + }, + members: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + common_writer: true, + }, + { + id: 'usr_grace', + name: 'Grace Hopper', + email: 'grace@utmist.gitlab.io', + common_writer: false, + }, + ], + grants: [], + }, + ], + storageBudgetGiB: 2000, + jobs: [ + { + id: 'job-seed-001', + name: 'ResNet50 Vision Benchmark', + created: new Date(Date.now() - 3600000).toISOString(), + job_state: 'Success', + accelerator: 'nvidia', + device_count: 2, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '8', + memory: '32Gi', + node: 'mist-gpu-node-01', + exit_code: 0, + message: 'Job completed successfully in 1420s', + checkpoint_directory: '/outputs/checkpoints/job-seed-001', + output_directory: '/outputs/runs/job-seed-001', + timeout_seconds: 7200, + team_id: 'team-vision', + creator_id: 'usr_ada', + creator_name: 'Ada Lovelace', + storage_scope: 'common', + can_cancel: false, + }, + { + id: 'job-seed-002', + name: 'BERT Fine-tuning Test', + created: new Date(Date.now() - 1800000).toISOString(), + job_state: 'InProgress', + accelerator: 'nvidia', + device_count: 1, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '4', + memory: '16Gi', + node: 'mist-gpu-node-01', + checkpoint_directory: '/outputs/checkpoints/job-seed-002', + output_directory: '/outputs/runs/job-seed-002', + timeout_seconds: 3600, + team_id: 'team-nlp', + creator_id: 'usr_grace', + creator_name: 'Grace Hopper', + storage_scope: 'usr_grace', + can_cancel: true, + }, + { + id: 'job-seed-003', + name: 'Tenstorrent Wormhole Matrix Multiplication', + created: new Date(Date.now() - 600000).toISOString(), + job_state: 'Scheduled', + accelerator: 'tenstorrent', + device_count: 2, + image: 'ghcr.io/utmist/tt-metal-runtime:latest', + cpu: '4', + memory: '16Gi', + message: 'Queued; waiting for 2 Tenstorrent n300 board allocations', + checkpoint_directory: '/outputs/checkpoints/job-seed-003', + output_directory: '/outputs/runs/job-seed-003', + timeout_seconds: 1800, + team_id: 'team-vision', + creator_id: 'usr_alan', + creator_name: 'Alan Turing', + storage_scope: 'usr_alan', + can_cancel: true, + }, + ], + datasets: [ + { + id: 'ds-cifar100', + name: 'CIFAR-100 Benchmark Dataset', + filename: 'cifar100.zip', + size: 169000000, + files: 100, + sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + created: new Date(Date.now() - 86400000).toISOString(), + state: 'ready', + team_id: 'team-vision', + team_name: 'Computer Vision', + scope: 'common', + member_name: 'Ada Lovelace', + shared: false, + }, + { + id: 'ds-wikitext', + name: 'Wikitext-103 Subset', + filename: 'wikitext.tar.gz', + size: 512000000, + files: 1, + sha256: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', + created: new Date(Date.now() - 43200000).toISOString(), + state: 'ready', + team_id: 'team-nlp', + team_name: 'Natural Language Processing', + scope: 'common', + member_name: 'Grace Hopper', + shared: true, + }, + ], + jobLogs: new Map([ + [ + 'job-seed-001', + `[2026-10-16 20:00:01] [INFO] Initializing PyTorch runtime on NVIDIA node 'mist-gpu-node-01' +[2026-10-16 20:00:03] [INFO] Allocated 2x NVIDIA RTX A6000 GPUs (CUDA devices 0, 1) +[2026-10-16 20:00:05] [INFO] Loading CIFAR-100 dataset from /inputs/cifar100 +[2026-10-16 20:00:12] [INFO] Epoch 1/10: train_loss=2.31, val_acc=68.4% +[2026-10-16 20:10:45] [INFO] Epoch 10/10: train_loss=0.42, val_acc=84.2% +[2026-10-16 20:23:41] [INFO] Checkpoint saved to /outputs/runs/job-seed-001/checkpoint_final.pt +[2026-10-16 20:23:42] [INFO] Training complete. Process exited with code 0.`, + ], + [ + 'job-seed-002', + `[2026-10-16 20:30:00] [INFO] BERT fine-tuning initialized. +[2026-10-16 20:30:02] [INFO] Using GPU 0 (Allocated 1x GPU) +[2026-10-16 20:30:15] [INFO] Warmup steps complete. Training batch 450/2000... +[2026-10-16 20:30:30] [INFO] Current loss: 1.284`, + ], + [ + 'job-seed-003', + `[2026-10-16 20:50:00] [INFO] Job queued in team 'team-vision'. Awaiting Tenstorrent admission controller.`, + ], + ]), + jobFiles: new Map([ + [ + 'job-seed-001', + [ + { + path: 'checkpoint_final.pt', + size: 104857600, + modified: new Date(Date.now() - 3600000).toISOString(), + }, + { + path: 'training_metrics.json', + size: 4096, + modified: new Date(Date.now() - 3600000).toISOString(), + }, + { + path: 'run_summary.txt', + size: 512, + modified: new Date(Date.now() - 3600000).toISOString(), + }, + ], + ], + ]), +} + +// Automatically progress queued jobs to running and completed +setInterval(() => { + for (const job of state.jobs) { + if (job.job_state === 'Scheduled') { + job.job_state = 'InProgress' + job.node = + job.accelerator === 'tenstorrent' + ? 'mist-tt-worker-01' + : 'mist-gpu-node-01' + job.message = 'Pod running on ' + job.node + const logs = + state.jobLogs.get(job.id) || + `[${new Date().toISOString()}] Job submitted.\n` + state.jobLogs.set( + job.id, + logs + + `[${new Date().toISOString()}] Admitted to cluster. Initializing pod on ${job.node}...\n`, + ) + } else if (job.job_state === 'InProgress') { + const elapsed = Date.now() - new Date(job.created).getTime() + if (elapsed > 20000) { + // Complete job after 20s + job.job_state = 'Success' + job.exit_code = 0 + job.message = 'Job completed successfully' + job.can_cancel = false + const logs = state.jobLogs.get(job.id) || '' + state.jobLogs.set( + job.id, + logs + + `[${new Date().toISOString()}] Execution finished with code 0.\nOutputs written to /outputs.\n`, + ) + if (!state.jobFiles.has(job.id)) { + state.jobFiles.set(job.id, [ + { + path: 'output.log', + size: 2048, + modified: new Date().toISOString(), + }, + { + path: 'model_weights.bin', + size: 52428800, + modified: new Date().toISOString(), + }, + ]) + } + } + } + } +}, 5000) + +// --- Helper Functions --- + +function parseJSONBody(req) { + return new Promise((resolve, reject) => { + let data = '' + req.on('data', (chunk) => { + data += chunk + if (data.length > 50 * 1024 * 1024) { + req.destroy() + reject(new Error('Request payload too large')) + } + }) + req.on('end', () => { + if (!data.trim()) return resolve({}) + try { + resolve(JSON.parse(data)) + } catch (err) { + reject(new Error('Malformed JSON: ' + err.message)) + } + }) + req.on('error', reject) + }) +} + +function sendJSON(res, statusCode, payload) { + const body = JSON.stringify(payload) + res.writeHead(statusCode, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, X-Mist-Team, Authorization', + }) + res.end(body) +} + +function sendError(res, statusCode, message) { + sendJSON(res, statusCode, { error: message }) +} + +// --- Request Router --- + +const server = http.createServer(async (req, res) => { + // CORS Preflight + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': + 'Content-Type, X-Mist-Team, Authorization', + 'Access-Control-Max-Age': '86400', + }) + res.end() + return + } + + const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`) + let pathname = parsedUrl.pathname + const query = Object.fromEntries(parsedUrl.searchParams.entries()) + const activeTeam = req.headers['x-mist-team'] || query.team_id || '' + + // Normalize path by stripping optional '/api' prefix (Vite proxy rewrites /api, direct hits do not) + if (pathname.startsWith('/api/')) { + pathname = pathname.slice(4) + } else if (pathname === '/api') { + pathname = '/' + } + + const currentUser = state.users[state.currentUserIndex] || null + + try { + // ---------------------------------------------------- + // AUTH & SESSION ENDPOINTS + // ---------------------------------------------------- + + // Session check used by AccountGate + if (pathname === '/session' && req.method === 'GET') { + sendJSON(res, 200, { + enabled: true, + user: currentUser, + }) + return + } + + // Better Auth: Session retrieval + if (pathname === '/auth/get-session' && req.method === 'GET') { + if (!currentUser) { + sendJSON(res, 200, null) + } else { + sendJSON(res, 200, { + user: currentUser, + session: { + id: 'sess_' + currentUser.id, + userId: currentUser.id, + expiresAt: new Date(Date.now() + 86400000).toISOString(), + }, + }) + } + return + } + + // Better Auth: Email Sign-in + if (pathname === '/auth/sign-in/email' && req.method === 'POST') { + const body = await parseJSONBody(req) + const user = state.users.find( + (u) => u.email.toLowerCase() === (body.email || '').toLowerCase(), + ) + if (!user) { + sendJSON(res, 401, { + error: { message: 'Invalid credentials or user not found' }, + }) + return + } + if (user.banned) { + sendJSON(res, 403, { + error: { message: 'Account is deactivated' }, + }) + return + } + state.currentUserIndex = state.users.findIndex((u) => u.id === user.id) + sendJSON(res, 200, { + user, + session: { + id: 'sess_' + user.id, + userId: user.id, + expiresAt: new Date(Date.now() + 86400000).toISOString(), + }, + }) + return + } + + // Better Auth: Sign-out + if (pathname === '/auth/sign-out' && req.method === 'POST') { + state.currentUserIndex = -1 // signed out + sendJSON(res, 200, { success: true }) + return + } + + // Better Auth: Change Password + if (pathname === '/auth/change-password' && req.method === 'POST') { + await parseJSONBody(req) + sendJSON(res, 200, { success: true }) + return + } + + // Better Auth Admin: List Users + if ( + (pathname === '/auth/admin/list-users' || + pathname === '/auth/admin/listUsers') && + (req.method === 'GET' || req.method === 'POST') + ) { + sendJSON(res, 200, { + users: state.users, + total: state.users.length, + }) + return + } + + // Better Auth Admin: Create User + if (pathname === '/auth/admin/create-user' && req.method === 'POST') { + const body = await parseJSONBody(req) + if (!body.name || !body.email) { + sendError(res, 400, 'Name and email are required') + return + } + const newUser = { + id: 'usr_' + crypto.randomBytes(4).toString('hex'), + name: body.name, + email: body.email, + role: body.role || 'user', + banned: false, + } + state.users.push(newUser) + sendJSON(res, 200, { user: newUser }) + return + } + + // Better Auth Admin: Set User Password + if (pathname === '/auth/admin/set-user-password' && req.method === 'POST') { + await parseJSONBody(req) + sendJSON(res, 200, { success: true }) + return + } + + // Better Auth Admin: Revoke User Sessions + if ( + pathname === '/auth/admin/revoke-user-sessions' && + req.method === 'POST' + ) { + await parseJSONBody(req) + sendJSON(res, 200, { success: true }) + return + } + + // Better Auth Admin: Ban / Unban User + if (pathname === '/auth/admin/ban-user' && req.method === 'POST') { + const body = await parseJSONBody(req) + const target = state.users.find((u) => u.id === body.userId) + if (target) target.banned = true + sendJSON(res, 200, { success: true }) + return + } + if (pathname === '/auth/admin/unban-user' && req.method === 'POST') { + const body = await parseJSONBody(req) + const target = state.users.find((u) => u.id === body.userId) + if (target) target.banned = false + sendJSON(res, 200, { success: true }) + return + } + + // ---------------------------------------------------- + // HARDWARE & IMAGES ENDPOINTS + // ---------------------------------------------------- + + if (pathname === '/hardware' && req.method === 'GET') { + sendJSON(res, 200, { + observed_at: new Date().toISOString(), + machines: [ + { + name: 'mist-gpu-node-01', + address: '10.0.1.10', + ready: true, + schedulable: true, + cpu_allocatable: '32', + memory_allocatable: '128Gi', + }, + { + name: 'mist-tt-worker-01', + address: '10.0.1.11', + ready: true, + schedulable: true, + cpu_allocatable: '16', + memory_allocatable: '64Gi', + }, + ], + pools: [ + { + accelerator: 'nvidia', + node: 'mist-gpu-node-01', + unit: 'GPU', + total: 4, + allocated: state.jobs + .filter( + (j) => + j.accelerator === 'nvidia' && j.job_state === 'InProgress', + ) + .reduce((sum, j) => sum + (j.device_count || 1), 0), + available: Math.max( + 0, + 4 - + state.jobs + .filter( + (j) => + j.accelerator === 'nvidia' && + j.job_state === 'InProgress', + ) + .reduce((sum, j) => sum + (j.device_count || 1), 0), + ), + ready: true, + }, + { + accelerator: 'tenstorrent', + node: 'mist-tt-worker-01', + unit: 'board', + total: 4, + allocated: state.jobs + .filter( + (j) => + j.accelerator === 'tenstorrent' && + j.job_state === 'InProgress', + ) + .reduce((sum, j) => sum + (j.device_count || 1), 0), + available: Math.max( + 0, + 4 - + state.jobs + .filter( + (j) => + j.accelerator === 'tenstorrent' && + j.job_state === 'InProgress', + ) + .reduce((sum, j) => sum + (j.device_count || 1), 0), + ), + ready: true, + chips_per_device: 2, + }, + ], + }) + return + } + + if (pathname === '/images' && req.method === 'GET') { + sendJSON(res, 200, { + self_service: true, + teams_enabled: true, + registries: ['docker.io', 'ghcr.io', 'quay.io', 'localhost:5000'], + images: [ + { + reference: 'docker.io/library/python:3.11-slim', + accelerators: ['cpu'], + }, + { + reference: 'docker.io/library/pytorch-cpu:2.2.0', + accelerators: ['cpu'], + }, + { + reference: 'ghcr.io/utmist/pytorch-cuda:latest', + accelerators: ['nvidia'], + }, + { + reference: 'ghcr.io/utmist/tt-metal-runtime:latest', + accelerators: ['tenstorrent'], + }, + ], + profiles: [ + { + accelerator: 'cpu', + default_image: 'docker.io/library/python:3.11-slim', + device_unit: 'core', + max_devices: 16, + note: 'Standard CPU runtime', + }, + { + accelerator: 'nvidia', + default_image: 'ghcr.io/utmist/pytorch-cuda:latest', + device_unit: 'GPU', + max_devices: 4, + note: 'NVIDIA CUDA 12.4 + PyTorch runtime', + }, + { + accelerator: 'tenstorrent', + default_image: 'ghcr.io/utmist/tt-metal-runtime:latest', + device_unit: 'board', + max_devices: 4, + note: 'Tenstorrent Wormhole n300 PCIe runtime', + }, + ], + }) + return + } + + // ---------------------------------------------------- + // TEAMS ENDPOINTS + // ---------------------------------------------------- + + if (pathname === '/teams' && req.method === 'GET') { + sendJSON(res, 200, { + teams: state.teams, + storage_budget_gib: state.storageBudgetGiB, + }) + return + } + + if (pathname === '/teams' && req.method === 'POST') { + const body = await parseJSONBody(req) + const newTeam = { + id: 'team-' + (body.name || 'unnamed').toLowerCase().replace(/\W+/g, '-'), + name: body.name || 'New Team', + disabled: false, + policy: { + cpu: '8', + memory: '32Gi', + nvidia: 1, + tenstorrent: 2, + concurrent: 2, + queued: 10, + storage_gib: Number(body.storage_gib || 200), + model_storage_gib: Number(body.model_storage_gib || 500), + runtime_seconds: 86400, + registries: ['docker.io', 'ghcr.io'], + }, + members: currentUser + ? [ + { + id: currentUser.id, + name: currentUser.name, + email: currentUser.email, + common_writer: true, + }, + ] + : [], + grants: [], + } + state.teams.push(newTeam) + sendJSON(res, 200, newTeam) + return + } + + const teamMatch = pathname.match(/^\/teams\/([^/]+)$/) + if (teamMatch && req.method === 'PATCH') { + const teamId = teamMatch[1] + const team = state.teams.find((t) => t.id === teamId) + if (!team) return sendError(res, 404, 'Team not found') + const body = await parseJSONBody(req) + if (body.name !== undefined) team.name = body.name + if (body.disabled !== undefined) team.disabled = body.disabled + if (body.policy !== undefined) team.policy = { ...team.policy, ...body.policy } + sendJSON(res, 200, team) + return + } + + const teamMembersMatch = pathname.match(/^\/teams\/([^/]+)\/members$/) + if (teamMembersMatch && req.method === 'POST') { + const teamId = teamMembersMatch[1] + const team = state.teams.find((t) => t.id === teamId) + if (!team) return sendError(res, 404, 'Team not found') + const body = await parseJSONBody(req) + const user = state.users.find((u) => u.id === body.user_id) + if (!user) return sendError(res, 404, 'User not found') + const existing = team.members.find((m) => m.id === user.id) + if (existing) { + existing.common_writer = !!body.common_writer + } else { + team.members.push({ + id: user.id, + name: user.name, + email: user.email, + common_writer: !!body.common_writer, + }) + } + sendJSON(res, 200, team) + return + } + + const teamMemberDeleteMatch = pathname.match(/^\/teams\/([^/]+)\/members\/([^/]+)$/) + if (teamMemberDeleteMatch && req.method === 'DELETE') { + const teamId = teamMemberDeleteMatch[1] + const userId = decodeURIComponent(teamMemberDeleteMatch[2]) + const team = state.teams.find((t) => t.id === teamId) + if (!team) return sendError(res, 404, 'Team not found') + team.members = team.members.filter((m) => m.id !== userId) + sendJSON(res, 200, team) + return + } + + const teamGrantsMatch = pathname.match(/^\/teams\/([^/]+)\/grants$/) + if (teamGrantsMatch && req.method === 'POST') { + const teamId = teamGrantsMatch[1] + const team = state.teams.find((t) => t.id === teamId) + if (!team) return sendError(res, 404, 'Team not found') + const body = await parseJSONBody(req) + const newGrant = { + id: 'grant-' + crypto.randomBytes(3).toString('hex'), + target_team: body.target_team, + scope: body.scope || 'common', + } + team.grants.push(newGrant) + sendJSON(res, 200, team) + return + } + + const teamGrantDeleteMatch = pathname.match(/^\/teams\/([^/]+)\/grants\/([^/]+)$/) + if (teamGrantDeleteMatch && req.method === 'DELETE') { + const teamId = teamGrantDeleteMatch[1] + const grantId = teamGrantDeleteMatch[2] + const team = state.teams.find((t) => t.id === teamId) + if (!team) return sendError(res, 404, 'Team not found') + team.grants = team.grants.filter((g) => g.id !== grantId) + sendJSON(res, 200, team) + return + } + + // ---------------------------------------------------- + // JOBS ENDPOINTS + // ---------------------------------------------------- + + if (pathname === '/jobs' && req.method === 'GET') { + let filtered = state.jobs + if (activeTeam) { + filtered = filtered.filter((j) => j.team_id === activeTeam) + } + sendJSON(res, 200, { + jobs: filtered, + count: filtered.length, + }) + return + } + + if (pathname === '/jobs' && req.method === 'POST') { + const body = await parseJSONBody(req) + const jobId = 'job-' + crypto.randomBytes(4).toString('hex') + const newJob = { + id: jobId, + name: body.name || `Experiment ${jobId}`, + created: new Date().toISOString(), + job_state: 'Scheduled', + accelerator: body.accelerator || 'cpu', + device_count: body.device_count || (body.accelerator === 'cpu' ? 0 : 1), + image: body.image || 'docker.io/library/python:3.11-slim', + cpu: body.cpu || '2', + memory: body.memory || '4Gi', + message: 'Job submitted; awaiting placement', + checkpoint_directory: `/outputs/checkpoints/${jobId}`, + output_directory: `/outputs/runs/${jobId}`, + working_directory: body.working_directory || '/workspace', + timeout_seconds: body.timeout_seconds || 3600, + team_id: activeTeam || 'legacy', + creator_id: currentUser?.id || 'usr_anonymous', + creator_name: currentUser?.name || 'Local Developer', + storage_scope: body.storage_scope || currentUser?.id || 'common', + can_cancel: true, + } + state.jobs.unshift(newJob) + state.jobLogs.set( + jobId, + `[${newJob.created}] Job created: ${newJob.name}\n[${newJob.created}] Image: ${newJob.image}\n[${newJob.created}] Queueing job on Kubernetes admission controller...\n`, + ) + sendJSON(res, 200, { + job_id: jobId, + job: newJob, + }) + return + } + + const jobCancelMatch = pathname.match(/^\/jobs\/([^/]+)\/cancel$/) + if (jobCancelMatch && req.method === 'POST') { + const jobId = decodeURIComponent(jobCancelMatch[1]) + const job = state.jobs.find((j) => j.id === jobId) + if (!job) return sendError(res, 404, 'Job not found') + job.job_state = 'Cancelled' + job.message = 'Cancelled by user' + job.can_cancel = false + const logs = state.jobLogs.get(jobId) || '' + state.jobLogs.set( + jobId, + logs + `[${new Date().toISOString()}] Job cancelled by user.\n`, + ) + sendJSON(res, 200, job) + return + } + + const jobLogsMatch = pathname.match(/^\/jobs\/([^/]+)\/logs$/) + if (jobLogsMatch && req.method === 'GET') { + const jobId = decodeURIComponent(jobLogsMatch[1]) + const logs = state.jobLogs.get(jobId) || 'No logs recorded for this job yet.' + sendJSON(res, 200, { logs }) + return + } + + const jobFilesMatch = pathname.match(/^\/jobs\/([^/]+)\/files$/) + if (jobFilesMatch && req.method === 'GET') { + const jobId = decodeURIComponent(jobFilesMatch[1]) + const files = state.jobFiles.get(jobId) || [] + sendJSON(res, 200, { files }) + return + } + + const jobDownloadMatch = pathname.match(/^\/jobs\/([^/]+)\/files\/download$/) + if (jobDownloadMatch && req.method === 'GET') { + const filePath = query.path || 'output.txt' + res.writeHead(200, { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Disposition': `attachment; filename="${filePath.split('/').pop()}"`, + 'Access-Control-Allow-Origin': '*', + }) + res.end(`[Mock file content for ${filePath} from job]`) + return + } + + // ---------------------------------------------------- + // STORAGE & DATASETS ENDPOINTS + // ---------------------------------------------------- + + if (pathname === '/storage' && req.method === 'GET') { + sendJSON(res, 200, { + enabled: true, + capacity_bytes: 1099511627776, // 1 TiB + available_bytes: 879609302220, // 800 GiB + model_capacity_bytes: 2199023255552, // 2 TiB + model_available_bytes: 1759218604441, // 1.6 TiB + dataset_allocation_gib: 500, + model_allocation_gib: 1000, + upload_limit_bytes: 5368709120, // 5 GiB + }) + return + } + + if (pathname === '/storage/folders' && req.method === 'GET') { + const folders = [] + for (const t of state.teams) { + folders.push({ + team_id: t.id, + team_name: t.name, + scope: 'common', + name: 'Common Space', + shared: false, + writable: true, + }) + for (const m of t.members) { + folders.push({ + team_id: t.id, + team_name: t.name, + scope: m.id, + name: `${m.name}'s Folder`, + shared: false, + writable: m.id === currentUser?.id, + }) + } + } + sendJSON(res, 200, { folders }) + return + } + + if (pathname === '/storage/files' && req.method === 'GET') { + if (query.download === 'true') { + const filePath = query.path || 'downloaded-file.dat' + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${filePath.split('/').pop()}"`, + 'Access-Control-Allow-Origin': '*', + }) + res.end(`[Mock content of ${filePath} in team ${query.source_team}]`) + return + } + sendJSON(res, 200, { + files: [ + { + path: 'data/train_manifest.json', + size: 15360, + modified: new Date(Date.now() - 7200000).toISOString(), + }, + { + path: 'models/model_final.pt', + size: 89128960, + modified: new Date(Date.now() - 3600000).toISOString(), + }, + ], + }) + return + } + + if (pathname === '/datasets' && req.method === 'GET') { + let filtered = state.datasets + if (activeTeam) { + filtered = filtered.filter((d) => !d.team_id || d.team_id === activeTeam) + } + sendJSON(res, 200, { + datasets: filtered, + upload_limit_bytes: 5368709120, + }) + return + } + + // Dataset upload (streaming / raw binary or multipart) + if (pathname === '/datasets' && req.method === 'POST') { + const filename = query.filename || 'dataset.bin' + const name = query.name || filename + const scope = query.scope || currentUser?.id || 'common' + const activeTeamObj = state.teams.find((t) => t.id === activeTeam) + + let totalBytes = 0 + req.on('data', (chunk) => { + totalBytes += chunk.length + }) + + req.on('end', () => { + const newDs = { + id: 'ds-' + crypto.randomBytes(4).toString('hex'), + name, + filename, + size: totalBytes || 1048576, + files: query.format === 'zip' ? 12 : 1, + sha256: crypto.randomBytes(32).toString('hex'), + created: new Date().toISOString(), + state: 'ready', + team_id: activeTeam || 'legacy', + team_name: activeTeamObj?.name || 'Legacy Workspace', + scope, + member_name: currentUser?.name || 'Local Pilot', + shared: false, + } + state.datasets.unshift(newDs) + sendJSON(res, 200, newDs) + }) + return + } + + const datasetDeleteMatch = pathname.match(/^\/datasets\/([^/]+)$/) + if (datasetDeleteMatch && req.method === 'DELETE') { + const datasetId = datasetDeleteMatch[1] + state.datasets = state.datasets.filter((d) => d.id !== datasetId) + sendJSON(res, 200, { success: true }) + return + } + + const datasetFilesMatch = pathname.match(/^\/datasets\/([^/]+)\/files$/) + if (datasetFilesMatch && req.method === 'GET') { + sendJSON(res, 200, { + files: [ + { + path: 'metadata.json', + size: 2048, + modified: new Date().toISOString(), + }, + { + path: 'samples/sample_001.png', + size: 45056, + modified: new Date().toISOString(), + }, + ], + }) + return + } + + const datasetDownloadMatch = pathname.match(/^\/datasets\/([^/]+)\/files\/download$/) + if (datasetDownloadMatch && req.method === 'GET') { + const filePath = query.path || 'dataset-file.dat' + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${filePath.split('/').pop()}"`, + 'Access-Control-Allow-Origin': '*', + }) + res.end(`[Mock content of dataset file ${filePath}]`) + return + } + + // Health check + if (pathname === '/healthz' && req.method === 'GET') { + sendJSON(res, 200, { status: 'ok', executor: 'fake-backend' }) + return + } + + // 404 Fallback + sendError(res, 404, `Endpoint not found: ${req.method} ${pathname}`) + } catch (err) { + console.error('[Fake Backend Error]', err) + sendError(res, 500, 'Internal Server Error: ' + err.message) + } +}) + +server.listen(PORT, HOST, () => { + console.log(`======================================================`) + console.log(`Mist Fake Backend running at http://${HOST}:${PORT}`) + console.log(`Ready for web-interface testing!`) + console.log(`- Simulated User: ${state.users[state.currentUserIndex]?.name} (${state.users[state.currentUserIndex]?.role})`) + console.log(`- Teams: ${state.teams.map((t) => t.name).join(', ')}`) + console.log(`- Jobs: ${state.jobs.length} pre-populated`) + console.log(`======================================================`) +}) + +process.on('SIGINT', () => { + server.close(() => { + console.log('\nFake backend stopped.') + process.exit(0) + }) +}) diff --git a/web-interface/package.json b/web-interface/package.json index dbb88b7..5200d90 100644 --- a/web-interface/package.json +++ b/web-interface/package.json @@ -14,6 +14,7 @@ "preview": "vite preview", "test": "vitest run", "lint": "eslint", + "fake-backend": "node ../scripts/fake-backend.mjs", "format": "prettier --check .", "check": "prettier --write . && eslint --fix" }, diff --git a/web-interface/pnpm-lock.yaml b/web-interface/pnpm-lock.yaml new file mode 100644 index 0000000..08effeb --- /dev/null +++ b/web-interface/pnpm-lock.yaml @@ -0,0 +1,4599 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.3.3(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + '@tanstack/react-devtools': + specifier: latest + version: 0.10.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(csstype@3.2.3)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(solid-js@1.9.17) + '@tanstack/react-router': + specifier: latest + version: 1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/react-router-devtools': + specifier: latest + version: 1.167.2(@tanstack/react-router@1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@tanstack/router-core@1.171.38)(csstype@3.2.3)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/router-plugin': + specifier: ^1.132.0 + version: 1.168.47(@tanstack/react-router@1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(esbuild@0.28.2)(rollup@4.64.7)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + better-auth: + specifier: 1.7.7 + version: 1.7.7(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(solid-js@1.9.17)(vitest@5.0.5(@types/node@22.20.5)(jsdom@28.1.0(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))) + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@19.3.1) + react: + specifier: ^19.2.0 + version: 19.3.1 + react-dom: + specifier: ^19.2.0 + version: 19.3.1(react@19.3.1) + tailwindcss: + specifier: ^4.1.18 + version: 4.3.3 + devDependencies: + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.20(tailwindcss@4.3.3) + '@tanstack/devtools-vite': + specifier: latest + version: 0.8.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + '@tanstack/eslint-config': + specifier: latest + version: 0.4.0(@typescript-eslint/utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.2 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@types/node': + specifier: ^22.10.2 + version: 22.20.5 + '@types/react': + specifier: ^19.2.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^5.1.4 + version: 5.2.0(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + jsdom: + specifier: ^28.1.0 + version: 28.1.0(@noble/hashes@2.4.0) + prettier: + specifier: ^3.8.1 + version: 3.9.10 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + vitest: + specifier: ^5.0.3 + version: 5.0.5(@types/node@22.20.5)(jsdom@28.1.0(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + +packages: + + '@acemir/cssom@0.9.32': + resolution: {integrity: sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@better-auth/core@1.7.7': + resolution: {integrity: sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==} + peerDependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/api': ^1.9.0 + better-call: 1.4.0 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.7.7': + resolution: {integrity: sha512-qon0U94PR5FQysuyGoAlGVQpIIiNg5dtDpwjE63sN/HjoRWZadZzHPcS9mn7JsJyQZK6iTNhd7LD0AaXt2f78w==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.7.7': + resolution: {integrity: sha512-9FONOCgOcrQI9wJ5v1oDGIg9sT7pa9RjZZQ4fsCTudo8R87SrIGNUvUxDFTJhZvUblvFfGaIBR9Vb8LA+8bqLQ==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.7.7': + resolution: {integrity: sha512-FqKFEe+b5tXXV0gRbyirca+qXgMpLOeJ8Wk19yykb/scQKy0WJ4k22WzEV3TTPctQV9DZhwCXPlDtQhnlOv5Gg==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.7.7': + resolution: {integrity: sha512-ZnVDuRrXqbf0oHphrEN27oTImNH5NTO26dtWatq5cXyYAsH7goof9QIoTOmxTbBxUQahKBw9T6+9h6n+RtOE+g==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.7.7': + resolution: {integrity: sha512-4YcnrcVdvWiAZw0sZl63tWUClPXg5r3QsLn7C2sG7kKEBY06zOng0ibecyxRBixKq1zE1ceHuEUqez5Rhi+nQA==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.7.7': + resolution: {integrity: sha512-PaRA6i+kAioVYmza2gHPcdrxP0hAotph4KV9hft2GeNJq+AZXDiBu3FuLM9HxvcmiZeUT74Vufqd9ypmB8veTA==} + peerDependencies: + '@better-auth/core': ^1.7.7 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + + '@better-auth/utils@0.5.0': + resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} + + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} + hasBin: true + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@napi-rs/wasm-runtime@1.2.5': + resolution: {integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@neodrag/core@3.0.0-next.11': + resolution: {integrity: sha512-3WQWxyrbxiaK9zS5JU2wJsW2gpoQlZBXVghduBh61JpqaeE0T0cte8R0qYK2RuJo3J2TYQYqxO19CpG/C1i5eg==} + + '@neodrag/solid@3.0.0-next.11': + resolution: {integrity: sha512-vCBIn/pimjWMQ6vhTS2/O1XNAwzVtc4eUhdbQ91WykbZWWqQ5NocDXt/1OdYrEkeRzJcpCv8wEz5PnMkgKP81Q==} + peerDependencies: + '@neodrag/core': 3.0.0-next.11 + solid-js: ^1.0.0 + + '@noble/ciphers@2.4.0': + resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@oxc-parser/binding-android-arm-eabi@0.120.0': + resolution: {integrity: sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.120.0': + resolution: {integrity: sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.120.0': + resolution: {integrity: sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.120.0': + resolution: {integrity: sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.120.0': + resolution: {integrity: sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + resolution: {integrity: sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + resolution: {integrity: sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + resolution: {integrity: sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.120.0': + resolution: {integrity: sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + resolution: {integrity: sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + resolution: {integrity: sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + resolution: {integrity: sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + resolution: {integrity: sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.120.0': + resolution: {integrity: sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.120.0': + resolution: {integrity: sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.120.0': + resolution: {integrity: sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.120.0': + resolution: {integrity: sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + resolution: {integrity: sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + resolution: {integrity: sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + resolution: {integrity: sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.64.7': + resolution: {integrity: sha512-GFF2tHk0NqYVObrK0+/ps/LEXsV/v2q6Lyiy7mZr+Ej24fpQScvqK2ziz9NHeEQYnKWKym38Ml7dZbPtC9tuAw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.7': + resolution: {integrity: sha512-BOymcXJFYYXqluDidj6sLelNjXruC3u1BqVxLn2OPCXH9xaPSOTH+MS4uychsGI1NU7tJoiv9KvqijDHFe+hhQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.7': + resolution: {integrity: sha512-jgVpGweIbls2H5jfBB70tnX52abMd5pJr3xmtoDILx6JdmGl+Un+tMb9R+MbHPVB46tTBpY8emtn7M71SnQhEQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.7': + resolution: {integrity: sha512-AVMpt94mrXH7WjyWKmd9nT4RC5yM0AN3Lt6A89xVIsuLDi3dDX6dxMytLbW36FlppY9JsoqUoS3LUUpUrYxiMQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.7': + resolution: {integrity: sha512-Nje29c/MyajxRNFGU05oyeTDS6SWdMg/wWMSlZSBTBju5BKz2NVc0kFiFSQGzRELNiB+CBk/ZvAVsWsENgjGag==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.7': + resolution: {integrity: sha512-xxgN5/kMzCNz/NYJ4t49B0/WS6InNlWv6xvgwUQqNnWM9WJMMwMUGWrAxdNEgYWdHL0sMnuh8I87StGrIMTA+w==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.7': + resolution: {integrity: sha512-5U/9gVn/X657I8+NHoHO9Wnfyd0lEXwqbslobRoB9TC8SwYzOitcIr6TLE7531Q/NWs3L4mb+YOGzlf3vHIeYw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.64.7': + resolution: {integrity: sha512-hcDS0a/Y8ggM3dcOfzH7UpSaogCViWTtNDSl0EXpWIj8g8/r86ITqptjil2hayTXS0Umzb+Yt3mjgpQVFVf2+A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.64.7': + resolution: {integrity: sha512-awbAr4N7Qd/yjZAQBEEPzgcPveYG2Gi5L6gq1/Bp+3SMRjn5F68Wv1zKsntzvvoioBR6yKvhLtWqu11am1TsxA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.64.7': + resolution: {integrity: sha512-Ut7xQJfeLhzu2yN3rHzVvL9ccyPJvN4aCdP1V7hJ5lGxX/eCzgB9SiMk/Md3J+rWWZgvoYsDW4BUFggJr32pBA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.64.7': + resolution: {integrity: sha512-trqoni/vHkuM5oo5iQhoKPh6fjjD7E80pMgCXWgGxU8vSRPUlwvG8GeYvEEcB6eJAxNa99WH9iVHPHelgtKiHA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.64.7': + resolution: {integrity: sha512-gvlUy0XFIwtUWiQmhc+4GT2Knm3KosinpUVhr7CtTFA78gFDnE5NWwMPzHrJsYpKkh2LlY7yvBIboN1FAV6Xeg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.64.7': + resolution: {integrity: sha512-8yS/kSXTJ/47Z62JW+C2AJ3vd/8zZw016vFjtNpZwmZlwJumLua4s1i78dkYuSSqQ+0aIMxJcBp/4uIwp2ehjA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.64.7': + resolution: {integrity: sha512-mxVSKvvty0hOP1AFo/l4Kpv3i3HK0zx4rYUrirZDToYlWhS3xzmIkq3vUJ7rtrM2cPyXveDyDT6M1FfXZ9hTRA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.64.7': + resolution: {integrity: sha512-lEse1zra1CagM+uHH2bl28GInDsZZ45NJzHei2OMaXQFzZWKBO1hFo07rS0xB6ayV4WU360UrSmPDSyCMpYsUQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.64.7': + resolution: {integrity: sha512-d/cqCoiG9WMZ5VuJZ5Qdlv9g97QyoGwnmvHccvGhKSacTy7xFrAS+jMcJSBdMNOw1oacSkl6p7K6O/DRYTKL3g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.64.7': + resolution: {integrity: sha512-kVEA//CeDOZZP1XCH2JBgqXb9+ALlwuA/4civaazMVdgzaSQC9WM38w65zG/LLk0lA7F9Od19CSBZW7J9mCbNg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.64.7': + resolution: {integrity: sha512-14oLfDsOSr0TSntT/34jRg/V8B2XW/Jomh9zX9CjIM9gwwNlI1OCymS/GVn0DV32h67eRf7U0GGem0rvfXgNwQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.64.7': + resolution: {integrity: sha512-dFVtPfjZqAnUPpPY4Bfg+87yo1IBW9HiY8iCFtg49Pn46MfZTebSHZh8PL6SOsgyt9LQP8Nj/sE6jm3aWpySmQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.64.7': + resolution: {integrity: sha512-Dm9CZ2SM3u2NgvBj4awW/gGqkOwzH8f6MSYIJE6+bKcKLI+T0+LhpD6vtetTfrIIx+rLXDf5RJqBP6v7V5O+wg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.7': + resolution: {integrity: sha512-oaxsvS2mL6+78741CT2HCORBvy1USg3Wqpt5qvZwy3aMXRMl4zxyDo8maeFRWJkF3dp0Wbi71fKH5Gr9YI3vYA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.7': + resolution: {integrity: sha512-UKZz1Y4mxUqkWh60uH7B1aZn6UcAimPk4ipRktpb3wfZq7JrzZYyAXTwiafkb3T+kd5P3Sf+vzcRdsDks4WtjQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.7': + resolution: {integrity: sha512-WWkSuC1J+mZJnsDh0mRaCAFSWfQdl6y3fDjZeWElI6zHo8CcOSG5lY3yurKJjEq+FrX0WD60sFrVVpbHqzMmrw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.7': + resolution: {integrity: sha512-u9bp2yXCI3/BUjUxY6uWknAY32pfvvDCPysyUTySotx0dj3ydlFHrRQYfn/b/kZgnfjyEwySMtI7Ga054uiysQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.7': + resolution: {integrity: sha512-EhX6N3XPMEnVU5cRfNgzPOKcSucsbu3nNNWuoAaEuKIqmddBKHN5b9d4rPe65T1I+FaOT176JKiZNsUBp3rqbA==} + cpu: [x64] + os: [win32] + + '@solid-primitives/event-listener@2.4.6': + resolution: {integrity: sha512-5I0YJcTVYIWoMmgBSROBZGcz+ymhew/pGTg2dHW74BUjFKsV8Li4bOZYl0YAGP4mHw5o4UBd9/BEesqBci3wxw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/keyboard@1.3.7': + resolution: {integrity: sha512-558RPNYnXx4nGh537DSqAn4xMrC8iFipl/5+xzgzWoTNFst4RnUN3BOLmtDjJ0UGGoQXVMALYR3bNOHM0xnt1Q==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/resize-observer@2.2.0': + resolution: {integrity: sha512-9Fuu/EWBeGj+atGHRJp70HKhdfalmpjwxY8a32NZixdLNmfCJ45AfhLQNr6uOzETbbiMx4iCKlTrJ8KZCHC2Ww==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/rootless@1.5.4': + resolution: {integrity: sha512-TOIZa1VUfVJ+9nkCcRajw3U4t9vBOP1HxX1WHNTbXq32mXwlqTvUnC4CRIilohcryBkT9u2ZkhUDSHRTaGp55g==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/static-store@0.1.4': + resolution: {integrity: sha512-LgtVaVBtB7EbmS4+M0b8xY5Iq6pUWXBsIC4VgtrFKDGDdyCaDt88sHk0fUlx1Enxm/XZnZyLXJABRoa39RjJqA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.4.1': + resolution: {integrity: sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg==} + peerDependencies: + solid-js: ^1.6.12 + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stylistic/eslint-plugin@5.10.0': + resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/devtools-bundler-core@0.1.3': + resolution: {integrity: sha512-F0tlxIyfFqXkZ1mJP1EjtkiSeJA+ztXY2AYOHf7r4goCIEAOp86N9PFJ/yv8vu1TnmxGS6vKsZCS3Kyls9xQQA==} + engines: {node: '>=18'} + + '@tanstack/devtools-client@0.0.8': + resolution: {integrity: sha512-cG3iZkGWCwN330bLBKa8+9r4Of2AXNoz2zUqcsy/4XsD3105ghVBx78cGyvJj9fSclNomPxoqAnDGXXhg1WLvA==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-bus@0.4.3': + resolution: {integrity: sha512-NeegBt5/n2E5q4DbrXHqECBq42+kDi6JBOp8/+RNqkIE+P4hJpbM36kGyjnGQFMWOoku31qhMyX9/48VuTTdmg==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-client@0.5.0': + resolution: {integrity: sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA==} + engines: {node: '>=18'} + hasBin: true + + '@tanstack/devtools-ui@0.7.1': + resolution: {integrity: sha512-3xQ/ezZ2qVNszhjpCN2N3jn7uHc2J1PMgcyjHzH4XZBt9xAQyMMcPNoR2cd7rzReyxWoJpZUWiDBmOJiCtLj9A==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/devtools-vite@0.8.5': + resolution: {integrity: sha512-xaifCEmiwwzizlbp973oISXNsnmuU/BugLa66gryAZaPJJ/qo1kJd2DxY5X960eHwmyIS3SN0KYrL3/aRhucmA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@tanstack/devtools@0.15.0': + resolution: {integrity: sha512-KyS7Qi82Eq9yG8C+uae3ikAPb+D5rfzVhvOOjXVAHZ/64s6TWJlDWpREw1FrHIYZaI7paErpsExSSKlF5BKA/A==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/eslint-config@0.4.0': + resolution: {integrity: sha512-V+Cd81W/f65dqKJKpytbwTGx9R+IwxKAHsG/uJ3nSLYEh36hlAr54lRpstUhggQB8nf/cP733cIw8DuD2dzQUg==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + + '@tanstack/history@1.162.5': + resolution: {integrity: sha512-w1+YvwmTkwt5n8yUqEH3IWZ9DsmorCZW/EQ3KYHnqEgIkOZpH54NvW6/Epq3Dzb8H0ifIeYX8PExvrNLIH5FIA==} + engines: {node: '>=20.19'} + + '@tanstack/react-devtools@0.10.13': + resolution: {integrity: sha512-r0cdB/14+st2Vxl/1Bodlyq4VC8pf9SykLQDYbGHDc1+V146WQo5B3KlYLkfyxD9Q/6+9NPRQHZyl8fvZVMdRA==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=16.8' + '@types/react-dom': '>=16.8' + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-router-devtools@1.167.2': + resolution: {integrity: sha512-VuoqB7OhDUrBa4WOFSKiAQ8xI8sJpeZdo9sYE1hQa89iMuVFUQu1iWE4iCiQhB+6TYOZDZ9e+iHKxYjop4ZVYw==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/react-router': ^1.170.36 + '@tanstack/router-core': ^1.171.30 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.170.45': + resolution: {integrity: sha512-6tuiIAFb82zsk/YxveJRyGjbsm6kIfgUuoH7qCp2R2aSHveW6W0t/6j+Sn4m1iIlA2lLU4cvooqks08/YMOThw==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.11.2': + resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.171.38': + resolution: {integrity: sha512-XRZVxiO6BIexICmEo3XfPmpoyyLhbjgaMJsONz4gIf5oYys0UiaG1BJGRyj9x8QZPDZcBhvAMuTYN70wSxV0gA==} + engines: {node: '>=20.19'} + + '@tanstack/router-devtools-core@1.168.2': + resolution: {integrity: sha512-pCCdQ5M/NUeWdiAbPmjyfcNdls6G4vaVlIQbPf1IqZk1FLYFr00aQvi578yUuWFBNM2KNS2QcwM1uPucbtGf5Q==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/router-core': ^1.171.30 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.167.45': + resolution: {integrity: sha512-oCtNqtsMNcBJkPIOu1O7a68jnxt4TkVJj3jHrW3VgiZdBeaj/lBJQh50yvzmNRswRcyIkRg0g30g25bbLRrAGw==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.47': + resolution: {integrity: sha512-1qvMto2Id0Uulux4qd+WttCbOFloIEQ5x82m68T4knt6NmUDQC66P/u/5yzN0FWGSO0u2U2nbDSIQoR6g7HNmQ==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.45 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.4': + resolution: {integrity: sha512-3TSH3rwAQTqQOtTBF8IXMFXmGNJWnjwIm+l87uguyC/of0pSRKcU9GJrozaf0fY9it8dLQvUZJA2MOlfqbW1sA==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.11.2': + resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} + + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.20.5': + resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript-eslint/eslint-plugin@8.71.2': + resolution: {integrity: sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.71.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.71.2': + resolution: {integrity: sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.71.2': + resolution: {integrity: sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.71.2': + resolution: {integrity: sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.71.2': + resolution: {integrity: sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.71.2': + resolution: {integrity: sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.71.2': + resolution: {integrity: sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.71.2': + resolution: {integrity: sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.71.2': + resolution: {integrity: sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.71.2': + resolution: {integrity: sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansis@4.4.0: + resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==} + engines: {node: '>=14'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.29: + resolution: {integrity: sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-auth@1.7.7: + resolution: {integrity: sha512-Zhgxi/c5ylmfB/sX+nwnBbsFf/j6k0L8DvL6CJXZSrXgxc5pPMC/e7j812zNd4L4B4ELmdURCPw3X8nAOUdcjw==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.4.0: + resolution: {integrity: sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + caniuse-lite@1.0.30001816: + resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + comment-parser@1.4.9: + resolution: {integrity: sha512-+2AvZKjJaNq9GQljm3tr0utwrig5BL+jgJGvz5rDpGKNIp+ojcRXWUwBbh/sGIi1QCprR6PsKFakub+w1v+4hQ==} + engines: {node: '>= 12.0.0'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + electron-to-chromium@1.5.458: + resolution: {integrity: sha512-/hIH2gRSmxrf4/X4Nw/t/Wc8PVS22l8/3HnWjWTWsTOM5dsnMUPZF/+u+yr3LqweNuTBJE1rq6qAQzKkZVwMfA==} + + enhanced-resolve@5.26.0: + resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} + engines: {node: '>=10.13.0'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import-x@4.17.1: + resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-n@17.24.0: + resolution: {integrity: sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.13.0: + resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@17.13.0: + resolution: {integrity: sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==} + engines: {node: '>=18'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + goober@2.1.19: + resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} + peerDependencies: + csstype: ^3.0.10 + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isbot@5.2.2: + resolution: {integrity: sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kysely@0.29.6: + resolution: {integrity: sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==} + engines: {node: '>=22.0.0'} + + launch-editor@2.14.2: + resolution: {integrity: sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.545.0: + resolution: {integrity: sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.5.5: + resolution: {integrity: sha512-FixtE239Gl6Gz0ZGCK0WzvNwObNCIwVT+L7zQcf1QgcA6AzBxblzqe9OT88qiucNkhsjSOqiogSaF5jVbJiqSA==} + engines: {node: ^20.0.0 || >=22.0.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} + engines: {node: '>=18'} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxc-parser@0.120.0: + resolution: {integrity: sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==} + engines: {node: ^20.19.0 || >=22.12.0} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.64.7: + resolution: {integrity: sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.6.9: + resolution: {integrity: sha512-kiDK/Tb8IWZ61WEU8pA2PDmAuw37gKph9Y77OZlD56mg0Y2g0Bg7gZ1HeHhs5d40w+nRoUq/wj08V26ilbez9g==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.6.9: + resolution: {integrity: sha512-UeEzHwAK+yl0YLbsvROV1t5yMYEwZZ0CfKYxDvUgS2fO9Vxdf8djqVIB+OuVpuawRNPqw68LE1Du/GMsW2ZPqA==} + engines: {node: '>=10'} + + set-cookie-parser@3.1.4: + resolution: {integrity: sha512-iEmdEew+Tn1BwMGMP8LzGBEPC7Mf1sySjglCeEPb/Vchm9bnGezePvacYcId+fGprQzIng+TBkzqfJ/qS7AsBg==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} + engines: {node: '>= 0.4'} + + solid-js@1.9.17: + resolution: {integrity: sha512-4GAmLOWPfNV8H9Jfn2LSFTpNEG6jiOKPiwd5lnLGQtehHaX6l3TOLzJqQ7mqwB/3k1Yy3zyumFVopmc/tm34ow==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tldts-core@7.4.18: + resolution: {integrity: sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==} + + tldts@7.4.18: + resolution: {integrity: sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + deprecated: unmaintained + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.71.2: + resolution: {integrity: sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.30.1: + resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==} + engines: {node: '>=20.18.1'} + + unplugin@3.4.0: + resolution: {integrity: sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rsbuild/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rsbuild/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.3.7: + resolution: {integrity: sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vue-eslint-parser@10.4.1: + resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@acemir/cssom@0.9.32': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.10': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5)': + dependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@opentelemetry/semantic-conventions': 1.43.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.4.0(zod@4.6.7) + jose: 6.2.12 + kysely: 0.29.6 + nanostores: 1.5.5 + zod: 4.6.7 + + '@better-auth/drizzle-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + + '@better-auth/kysely-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(kysely@0.29.6)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + optionalDependencies: + kysely: 0.29.6 + + '@better-auth/memory-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + + '@better-auth/prisma-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + + '@better-auth/telemetry@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)': + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-auth/utils@0.5.0': + dependencies: + '@noble/hashes': 2.4.0 + + '@better-fetch/fetch@1.3.2': {} + + '@bramus/specificity@2.4.3': + dependencies: + css-tree: 3.2.1 + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@csstools/color-helpers@6.1.2': {} + + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.2': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0(jiti@2.7.0))': + dependencies: + eslint: 10.13.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.13.0(jiti@2.7.0))': + optionalDependencies: + eslint: 10.13.0(jiti@2.7.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.16.0(@noble/hashes@2.4.0)': + optionalDependencies: + '@noble/hashes': 2.4.0 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@neodrag/core@3.0.0-next.11': {} + + '@neodrag/solid@3.0.0-next.11(@neodrag/core@3.0.0-next.11)(solid-js@1.9.17)': + dependencies: + '@neodrag/core': 3.0.0-next.11 + solid-js: 1.9.17 + + '@noble/ciphers@2.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@oxc-parser/binding-android-arm-eabi@0.120.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.120.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + optional: true + + '@oxc-project/types@0.120.0': {} + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.64.7': + optional: true + + '@rollup/rollup-android-arm64@4.64.7': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.7': + optional: true + + '@rollup/rollup-darwin-x64@4.64.7': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.7': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.7': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.7': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.7': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.7': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.7': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.7': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.7': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.7': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.7': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.7': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.7': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.7': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.7': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.7': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.7': + optional: true + + '@solid-primitives/event-listener@2.4.6(solid-js@1.9.17)': + dependencies: + '@solid-primitives/utils': 6.4.1(solid-js@1.9.17) + solid-js: 1.9.17 + + '@solid-primitives/keyboard@1.3.7(solid-js@1.9.17)': + dependencies: + '@solid-primitives/event-listener': 2.4.6(solid-js@1.9.17) + '@solid-primitives/rootless': 1.5.4(solid-js@1.9.17) + '@solid-primitives/utils': 6.4.1(solid-js@1.9.17) + solid-js: 1.9.17 + + '@solid-primitives/resize-observer@2.2.0(solid-js@1.9.17)': + dependencies: + '@solid-primitives/event-listener': 2.4.6(solid-js@1.9.17) + '@solid-primitives/rootless': 1.5.4(solid-js@1.9.17) + '@solid-primitives/static-store': 0.1.4(solid-js@1.9.17) + '@solid-primitives/utils': 6.4.1(solid-js@1.9.17) + solid-js: 1.9.17 + + '@solid-primitives/rootless@1.5.4(solid-js@1.9.17)': + dependencies: + '@solid-primitives/utils': 6.4.1(solid-js@1.9.17) + solid-js: 1.9.17 + + '@solid-primitives/static-store@0.1.4(solid-js@1.9.17)': + dependencies: + '@solid-primitives/utils': 6.4.1(solid-js@1.9.17) + solid-js: 1.9.17 + + '@solid-primitives/utils@6.4.1(solid-js@1.9.17)': + dependencies: + solid-js: 1.9.17 + + '@standard-schema/spec@1.1.0': {} + + '@stylistic/eslint-plugin@5.10.0(eslint@10.13.0(jiti@2.7.0))': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(jiti@2.7.0)) + '@typescript-eslint/types': 8.71.2 + eslint: 10.13.0(jiti@2.7.0) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.7 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.26.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.2 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/typography@0.5.20(tailwindcss@4.3.3)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + + '@tanstack/devtools-bundler-core@0.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@tanstack/devtools-client': 0.0.8 + '@tanstack/devtools-event-bus': 0.4.3 + chalk: 5.6.2 + launch-editor: 2.14.2 + magic-string: 0.30.21 + oxc-parser: 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + picomatch: 4.0.7 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - utf-8-validate + + '@tanstack/devtools-client@0.0.8': + dependencies: + '@tanstack/devtools-event-client': 0.5.0 + + '@tanstack/devtools-event-bus@0.4.3': + dependencies: + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-event-client@0.5.0': {} + + '@tanstack/devtools-ui@0.7.1(csstype@3.2.3)(solid-js@1.9.17)': + dependencies: + clsx: 2.1.1 + dayjs: 1.11.23 + goober: 2.1.19(csstype@3.2.3) + solid-js: 1.9.17 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-vite@0.8.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tanstack/devtools-bundler-core': 0.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@tanstack/devtools-client': 0.0.8 + '@tanstack/devtools-event-bus': 0.4.3 + chalk: 5.6.2 + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - utf-8-validate + + '@tanstack/devtools@0.15.0(csstype@3.2.3)(solid-js@1.9.17)': + dependencies: + '@neodrag/core': 3.0.0-next.11 + '@neodrag/solid': 3.0.0-next.11(@neodrag/core@3.0.0-next.11)(solid-js@1.9.17) + '@solid-primitives/event-listener': 2.4.6(solid-js@1.9.17) + '@solid-primitives/keyboard': 1.3.7(solid-js@1.9.17) + '@solid-primitives/resize-observer': 2.2.0(solid-js@1.9.17) + '@tanstack/devtools-client': 0.0.8 + '@tanstack/devtools-event-bus': 0.4.3 + '@tanstack/devtools-ui': 0.7.1(csstype@3.2.3)(solid-js@1.9.17) + clsx: 2.1.1 + goober: 2.1.19(csstype@3.2.3) + solid-js: 1.9.17 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + + '@tanstack/eslint-config@0.4.0(@typescript-eslint/utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint/js': 10.0.1(eslint@10.13.0(jiti@2.7.0)) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.13.0(jiti@2.7.0)) + eslint: 10.13.0(jiti@2.7.0) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0)) + eslint-plugin-n: 17.24.0(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + globals: 17.13.0 + typescript-eslint: 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)) + transitivePeerDependencies: + - '@typescript-eslint/utils' + - eslint-import-resolver-node + - supports-color + - typescript + + '@tanstack/history@1.162.5': {} + + '@tanstack/react-devtools@0.10.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(csstype@3.2.3)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(solid-js@1.9.17)': + dependencies: + '@tanstack/devtools': 0.15.0(csstype@3.2.3)(solid-js@1.9.17) + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + transitivePeerDependencies: + - bufferutil + - csstype + - solid-js + - utf-8-validate + + '@tanstack/react-router-devtools@1.167.2(@tanstack/react-router@1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(@tanstack/router-core@1.171.38)(csstype@3.2.3)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/react-router': 1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/router-devtools-core': 1.168.2(@tanstack/router-core@1.171.38)(csstype@3.2.3) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@tanstack/router-core': 1.171.38 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router@1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/history': 1.162.5 + '@tanstack/react-store': 0.11.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/router-core': 1.171.38 + isbot: 5.2.2 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@tanstack/react-store@0.11.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/store': 0.11.2 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + use-sync-external-store: 1.7.0(react@19.3.1) + + '@tanstack/router-core@1.171.38': + dependencies: + '@tanstack/history': 1.162.5 + cookie-es: 3.1.1 + seroval: 1.6.9 + seroval-plugins: 1.6.9(seroval@1.6.9) + + '@tanstack/router-devtools-core@1.168.2(@tanstack/router-core@1.171.38)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.171.38 + clsx: 2.1.1 + goober: 2.1.19(csstype@3.2.3) + optionalDependencies: + csstype: 3.2.3 + + '@tanstack/router-generator@1.167.45': + dependencies: + '@babel/types': 7.29.8 + '@tanstack/router-core': 1.171.38 + '@tanstack/router-utils': 1.162.4 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.9.10 + zod: 4.6.7 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.47(@tanstack/react-router@1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(esbuild@0.28.2)(rollup@4.64.7)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + '@tanstack/router-core': 1.171.38 + '@tanstack/router-generator': 1.167.45 + '@tanstack/router-utils': 1.162.4 + chokidar: 5.0.0 + unplugin: 3.4.0(esbuild@0.28.2)(rollup@4.64.7)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + zod: 4.6.7 + optionalDependencies: + '@tanstack/react-router': 1.170.45(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - supports-color + - unloader + + '@tanstack/router-utils@1.162.4': + dependencies: + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + ansis: 4.4.0 + babel-dead-code-elimination: 1.0.12 + diff: 9.0.0 + pathe: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.11.2': {} + + '@tanstack/virtual-file-routes@1.162.0': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.10 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@babel/runtime': 7.29.10 + '@testing-library/dom': 10.4.2 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.20.5': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.71.2(@typescript-eslint/parser@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.2 + '@typescript-eslint/type-utils': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.2 + eslint: 10.13.0(jiti@2.7.0) + ignore: 7.0.12 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.71.2 + '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/typescript-estree': 8.71.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.2 + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.71.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.71.2(typescript@5.9.3) + '@typescript-eslint/types': 8.71.2 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.71.2': + dependencies: + '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/visitor-keys': 8.71.2 + + '@typescript-eslint/tsconfig-utils@8.71.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/typescript-estree': 8.71.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.71.2': {} + + '@typescript-eslint/typescript-estree@8.71.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.71.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.71.2(typescript@5.9.3) + '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/visitor-keys': 8.71.2 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.71.2 + '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/typescript-estree': 8.71.2(typescript@5.9.3) + eslint: 10.13.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.71.2': + dependencies: + '@typescript-eslint/types': 8.71.2 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitejs/plugin-react@5.2.0(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + '@vitest/mocker@5.0.5(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.5 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + + '@vitest/spy@5.0.5': {} + + acorn-jsx@5.3.2(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + + acorn@8.19.0: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + ansis@4.4.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.29: {} + + better-auth@1.7.7(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(solid-js@1.9.17)(vitest@5.0.5(@types/node@22.20.5)(jsdom@28.1.0(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0))): + dependencies: + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) + '@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(kysely@0.29.6) + '@better-auth/memory-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.7))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.2 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.0(zod@4.6.7) + defu: 6.1.7 + jose: 6.2.12 + kysely: 0.29.6 + nanostores: 1.5.5 + zod: 4.6.7 + optionalDependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + solid-js: 1.9.17 + vitest: 5.0.5(@types/node@22.20.5)(jsdom@28.1.0(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + transitivePeerDependencies: + - '@opentelemetry/api' + + better-call@1.4.0(zod@4.6.7): + dependencies: + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + rou3: 0.9.2 + set-cookie-parser: 3.1.4 + optionalDependencies: + zod: 4.6.7 + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.29.3: + dependencies: + baseline-browser-mapping: 2.11.29 + caniuse-lite: 1.0.30001816 + electron-to-chromium: 1.5.458 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.3) + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + caniuse-lite@1.0.30001816: {} + + chai@6.3.0: {} + + chalk@5.6.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + clsx@2.1.1: {} + + comment-parser@1.4.9: {} + + convert-source-map@2.0.0: {} + + cookie-es@3.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + cssesc@3.0.0: {} + + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.3 + + csstype@3.2.3: {} + + data-urls@7.0.0(@noble/hashes@2.4.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.4.0) + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.23: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + defu@6.1.7: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + diff@9.0.0: {} + + dom-accessibility-api@0.5.16: {} + + electron-to-chromium@1.5.458: {} + + enhanced-resolve@5.26.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.1.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@10.13.0(jiti@2.7.0)): + dependencies: + eslint: 10.13.0(jiti@2.7.0) + semver: 7.8.5 + + eslint-import-context@0.1.9(unrs-resolver@1.12.2): + dependencies: + get-tsconfig: 4.14.3 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.12.2 + + eslint-plugin-es-x@7.8.0(eslint@10.13.0(jiti@2.7.0)): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + eslint: 10.13.0(jiti@2.7.0) + eslint-compat-utils: 0.5.1(eslint@10.13.0(jiti@2.7.0)) + + eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0)): + dependencies: + '@typescript-eslint/types': 8.71.2 + comment-parser: 1.4.9 + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + is-glob: 4.0.3 + minimatch: 10.2.6 + semver: 7.8.5 + stable-hash-x: 0.2.0 + unrs-resolver: 1.12.2 + optionalDependencies: + '@typescript-eslint/utils': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-n@17.24.0(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(jiti@2.7.0)) + enhanced-resolve: 5.26.0 + eslint: 10.13.0(jiti@2.7.0) + eslint-plugin-es-x: 7.8.0(eslint@10.13.0(jiti@2.7.0)) + get-tsconfig: 4.14.3 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.8.5 + ts-declaration-location: 1.0.7(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.13.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@15.15.0: {} + + globals@17.13.0: {} + + globrex@0.1.2: {} + + goober@2.1.19(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + graceful-fs@4.2.11: {} + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + html-encoding-sniffer@6.0.0(@noble/hashes@2.4.0): + dependencies: + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + transitivePeerDependencies: + - '@noble/hashes' + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.12: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + isbot@5.2.2: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + jose@6.2.12: {} + + js-tokens@4.0.0: {} + + jsdom@28.1.0(@noble/hashes@2.4.0): + dependencies: + '@acemir/cssom': 0.9.32 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.3 + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + cssstyle: 6.2.0 + data-urls: 7.0.0(@noble/hashes@2.4.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.4.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.30.1 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.4.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + + jsesc@3.1.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kysely@0.29.6: {} + + launch-editor@2.14.2: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.12.0 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@11.5.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.545.0(react@19.3.1): + dependencies: + react: 19.3.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mdn-data@2.27.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + nanostores@1.5.5: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.59: {} + + obug@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + oxc-parser@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + dependencies: + '@oxc-project/types': 0.120.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.120.0 + '@oxc-parser/binding-android-arm64': 0.120.0 + '@oxc-parser/binding-darwin-arm64': 0.120.0 + '@oxc-parser/binding-darwin-x64': 0.120.0 + '@oxc-parser/binding-freebsd-x64': 0.120.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.120.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.120.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.120.0 + '@oxc-parser/binding-linux-arm64-musl': 0.120.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.120.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-musl': 0.120.0 + '@oxc-parser/binding-openharmony-arm64': 0.120.0 + '@oxc-parser/binding-wasm32-wasi': 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxc-parser/binding-win32-arm64-msvc': 0.120.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.120.0 + '@oxc-parser/binding-win32-x64-msvc': 0.120.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.30: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prelude-ls@1.2.1: {} + + prettier@3.9.10: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react-is@17.0.2: {} + + react-refresh@0.18.0: {} + + react@19.3.1: {} + + readdirp@5.1.1: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + rollup@4.64.7: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.7 + '@rollup/rollup-android-arm64': 4.64.7 + '@rollup/rollup-darwin-arm64': 4.64.7 + '@rollup/rollup-darwin-x64': 4.64.7 + '@rollup/rollup-freebsd-arm64': 4.64.7 + '@rollup/rollup-freebsd-x64': 4.64.7 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.7 + '@rollup/rollup-linux-arm-musleabihf': 4.64.7 + '@rollup/rollup-linux-arm64-gnu': 4.64.7 + '@rollup/rollup-linux-arm64-musl': 4.64.7 + '@rollup/rollup-linux-loong64-gnu': 4.64.7 + '@rollup/rollup-linux-loong64-musl': 4.64.7 + '@rollup/rollup-linux-ppc64-gnu': 4.64.7 + '@rollup/rollup-linux-ppc64-musl': 4.64.7 + '@rollup/rollup-linux-riscv64-gnu': 4.64.7 + '@rollup/rollup-linux-riscv64-musl': 4.64.7 + '@rollup/rollup-linux-s390x-gnu': 4.64.7 + '@rollup/rollup-linux-x64-gnu': 4.64.7 + '@rollup/rollup-linux-x64-musl': 4.64.7 + '@rollup/rollup-openbsd-x64': 4.64.7 + '@rollup/rollup-openharmony-arm64': 4.64.7 + '@rollup/rollup-win32-arm64-msvc': 4.64.7 + '@rollup/rollup-win32-ia32-msvc': 4.64.7 + '@rollup/rollup-win32-x64-gnu': 4.64.7 + '@rollup/rollup-win32-x64-msvc': 4.64.7 + fsevents: 2.3.3 + + rou3@0.9.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + seroval-plugins@1.6.9(seroval@1.6.9): + dependencies: + seroval: 1.6.9 + + seroval@1.6.9: {} + + set-cookie-parser@3.1.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.12.0: {} + + solid-js@1.9.17: + dependencies: + csstype: 3.2.3 + seroval: 1.6.9 + seroval-plugins: 1.6.9(seroval@1.6.9) + + source-map-js@1.2.2: {} + + stable-hash-x@0.2.0: {} + + std-env@4.3.0: {} + + symbol-tree@3.2.4: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@6.2.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tldts-core@7.4.18: {} + + tldts@7.4.18: + dependencies: + tldts-core: 7.4.18 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.18 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.7 + typescript: 5.9.3 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/parser@8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.71.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.2(eslint@10.13.0(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.13.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici@7.30.1: {} + + unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.7)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.28.2 + rollup: 4.64.7 + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.3.4(browserslist@4.29.3): + dependencies: + browserslist: 4.29.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.7.0(react@19.3.1): + dependencies: + react: 19.3.1 + + util-deprecate@1.0.2: {} + + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.30 + rollup: 4.64.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.5 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vitest@5.0.5(@types/node@22.20.5)(jsdom@28.1.0(@noble/hashes@2.4.0))(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.5(vite@7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 7.3.7(@types/node@22.20.5)(jiti@2.7.0)(lightningcss@1.32.0) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 22.20.5 + jsdom: 28.1.0(@noble/hashes@2.4.0) + transitivePeerDependencies: + - msw + + vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)): + dependencies: + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1(@noble/hashes@2.4.0): + dependencies: + '@exodus/bytes': 1.16.0(@noble/hashes@2.4.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@3.2.1: {} + + word-wrap@1.2.5: {} + + ws@8.22.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod@4.6.7: {} From aebdb7c7382bcdf9355fab8582e49c7ad42c0108 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 22:31:22 -0400 Subject: [PATCH 02/10] feat(fake-backend): add configuration script and hypothetical scenario presets --- .gitignore | 1 + docs/frontend-architecture.md | 27 ++ scripts/configure-fake-backend.mjs | 545 ++++++++++++++++++++++++ scripts/fake-backend-scenarios.mjs | 643 ++++++++++++++++++++++++++++ scripts/fake-backend.mjs | 660 +++++++++++------------------ web-interface/package.json | 1 + 6 files changed, 1457 insertions(+), 420 deletions(-) create mode 100644 scripts/configure-fake-backend.mjs create mode 100644 scripts/fake-backend-scenarios.mjs diff --git a/.gitignore b/.gitignore index 40ae98d..f456dba 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ *.py[cod] data/ .direnv/ +.fake-backend-config.json diff --git a/docs/frontend-architecture.md b/docs/frontend-architecture.md index 41e2c50..f27ed6a 100644 --- a/docs/frontend-architecture.md +++ b/docs/frontend-architecture.md @@ -96,3 +96,30 @@ npm --prefix web-interface run dev -- --host 127.0.0.1 --port 3001 --strictPort ``` Open http://127.0.0.1:3001 in your browser. Pre-seeded users include `ada@utmist.gitlab.io` (admin), `alan@utmist.gitlab.io`, and `grace@utmist.gitlab.io`. + +### Configuring hypothetical scenarios and fake information + +A companion configure tool is provided at `scripts/configure-fake-backend.mjs` to set up hypothetical cluster topologies, error conditions, and user setups: + +```bash +# Interactive menu (choose scenarios, add fake nodes, switch active user) +npm --prefix web-interface run configure-fake-backend + +# Switch to a preset scenario directly +node scripts/configure-fake-backend.mjs --scenario high-load +node scripts/configure-fake-backend.mjs --scenario unhealthy +node scripts/configure-fake-backend.mjs --scenario empty-slate +node scripts/configure-fake-backend.mjs --scenario large-cluster +node scripts/configure-fake-backend.mjs --scenario default + +# Inspect current cluster topology & status +node scripts/configure-fake-backend.mjs --status + +# Add custom fake machines, users, or teams +node scripts/configure-fake-backend.mjs --add-machine name=dgx-custom,ip=10.99.1.5,cpu=64,ram=256Gi,gpu=8 +node scripts/configure-fake-backend.mjs --add-user name="Marie Curie",email=curie@rad.edu,role=admin +node scripts/configure-fake-backend.mjs --set-user curie@rad.edu +``` + +If the fake backend is running, `configure-fake-backend.mjs` updates the live server immediately without requiring a restart, while also saving to `.fake-backend-config.json` for persistence. + diff --git a/scripts/configure-fake-backend.mjs b/scripts/configure-fake-backend.mjs new file mode 100644 index 0000000..f5a8027 --- /dev/null +++ b/scripts/configure-fake-backend.mjs @@ -0,0 +1,545 @@ +#!/usr/bin/env node +/** + * Mist Fake Backend Configuration CLI + * + * Configures hypothetical scenarios, fake users, machines, and hardware pools + * for local testing of the web interface. + * + * Usage: + * node scripts/configure-fake-backend.mjs # Interactive mode + * node scripts/configure-fake-backend.mjs --scenario high-load # Switch scenario + * node scripts/configure-fake-backend.mjs --status # Show current topology + * node scripts/configure-fake-backend.mjs --list-scenarios # List all presets + * node scripts/configure-fake-backend.mjs --reset # Reset to default + */ + +import fs from 'node:fs' +import path from 'node:path' +import http from 'node:http' +import readline from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +import { + getScenario, + SCENARIO_DESCRIPTIONS, + SCENARIO_FACTORIES, +} from './fake-backend-scenarios.mjs' + +const CONFIG_FILE = path.resolve(process.cwd(), '.fake-backend-config.json') + +// Parse CLI flags +const argv = process.argv.slice(2) +function getFlag(name) { + const idx = argv.indexOf(name) + return idx !== -1 && argv[idx + 1] ? argv[idx + 1] : null +} +function hasFlag(name) { + return argv.includes(name) +} + +const PORT = Number( + getFlag('--port') || process.env.PORT || process.env.MIST_FAKE_PORT || 3000, +) +const HOST = process.env.HOST || '127.0.0.1' + +// --- Server Communication Helpers --- + +function requestJSON(method, endpointPath, body = null) { + return new Promise((resolve, reject) => { + const data = body ? JSON.stringify(body) : null + const req = http.request( + { + hostname: HOST, + port: PORT, + path: endpointPath, + method, + headers: { + 'Content-Type': 'application/json', + ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}), + }, + timeout: 1500, + }, + (res) => { + let resData = '' + res.on('data', (chunk) => (resData += chunk)) + res.on('end', () => { + try { + resolve({ + status: res.statusCode, + data: resData ? JSON.parse(resData) : null, + }) + } catch (e) { + resolve({ status: res.statusCode, raw: resData }) + } + }) + }, + ) + req.on('timeout', () => { + req.destroy() + reject(new Error('Connection timed out')) + }) + req.on('error', (err) => reject(err)) + if (data) req.write(data) + req.end() + }) +} + +async function checkLiveServer() { + try { + const res = await requestJSON('GET', '/_fake/state') + if (res.status === 200 && res.data) { + return res.data + } + } catch {} + return null +} + +function loadLocalConfigFile() { + if (fs.existsSync(CONFIG_FILE)) { + try { + return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) + } catch {} + } + return null +} + +function saveLocalConfigFile(state) { + fs.writeFileSync(CONFIG_FILE, JSON.stringify(state, null, 2), 'utf-8') +} + +async function applyState(state) { + // 1. Save to local config file + saveLocalConfigFile(state) + + // 2. If live server running, update it dynamically + const live = await checkLiveServer() + if (live) { + try { + await requestJSON('POST', '/_fake/state', state) + console.log(`\n✓ Live fake backend at http://${HOST}:${PORT} updated instantly!`) + console.log(`✓ Saved configuration to .fake-backend-config.json`) + return + } catch (err) { + console.warn(`\n(Live update failed: ${err.message})`) + } + } + console.log(`\n✓ Saved configuration to .fake-backend-config.json`) + console.log(` (The fake backend is offline. Changes will take effect when it starts.)`) +} + +function parseKeyValue(inputStr) { + const result = {} + const pairs = inputStr.split(',') + for (const pair of pairs) { + const [k, v] = pair.split('=').map((s) => s.trim()) + if (k && v !== undefined) { + result[k] = v.replace(/^["']|["']$/g, '') + } + } + return result +} + +function printStatus(state) { + console.log('\n======================================================') + console.log(`Scenario: ${state.activeScenario || state.name || 'custom'}`) + const activeUser = + state.users?.[state.currentUserIndex] || state.currentUser || null + console.log( + `Active User: ${activeUser ? `${activeUser.name} <${activeUser.email}> [${activeUser.role}]` : 'None'}`, + ) + console.log(`Total Users: ${state.users?.length ?? 0}`) + console.log('------------------------------------------------------') + console.log('Machines & Nodes:') + for (const m of state.machines || []) { + const status = m.ready ? (m.schedulable ? 'Ready' : 'Placement Paused') : 'Offline' + console.log(` • ${m.name.padEnd(22)} ${m.address.padEnd(14)} [${status.padEnd(16)}] ${m.cpu_allocatable} CPU · ${m.memory_allocatable} RAM`) + } + console.log('Hardware Accelerator Pools:') + for (const p of state.pools || []) { + const avail = p.available === null ? 'Unknown' : p.available + const msg = p.message ? ` (${p.message})` : '' + console.log(` • ${p.accelerator.toUpperCase().padEnd(12)} Node: ${p.node.padEnd(20)} Total: ${p.total} Allocated: ${p.allocated} Available: ${avail}${msg}`) + } + console.log('Teams:') + for (const t of state.teams || []) { + console.log(` • ${t.name.padEnd(26)} Members: ${t.members?.length ?? 0} Policy: ${t.policy?.storage_gib ?? 0} GiB Datasets / ${t.policy?.model_storage_gib ?? 0} GiB Models`) + } + console.log('Jobs in History:') + const states = {} + for (const j of state.jobs || []) { + states[j.job_state] = (states[j.job_state] || 0) + 1 + } + console.log(` Total: ${state.jobs?.length ?? 0} (${Object.entries(states).map(([k, v]) => `${k}: ${v}`).join(', ') || '0'})`) + console.log('======================================================\n') +} + +// --- CLI Commands --- + +async function main() { + if (hasFlag('--help') || hasFlag('-h')) { + console.log(` +Mist Fake Backend Configurator + +Commands & Options: + --scenario Load a preset scenario (default, high-load, unhealthy, empty-slate, large-cluster) + --list-scenarios List all available built-in scenarios + --status Display current cluster topology, machines, users, and jobs + --set-user Set which user is active in the web session + --add-user Add a user: name="...",email="...",role=admin|user + --add-machine Add a machine: name="...",ip="...",cpu=32,ram=128Gi,gpu=4,tt=0 + --add-team Add a team: name="...",storage=300,models=600 + --config Load configuration from a JSON file + --export Export active configuration to a JSON file + --reset Reset back to the default scenario + --port Target port (default: 3000) + +Run with no flags for the interactive menu. +`) + return + } + + if (hasFlag('--list-scenarios')) { + console.log('\nAvailable Hypothetical Scenarios:') + for (const [name, desc] of Object.entries(SCENARIO_DESCRIPTIONS)) { + console.log(` • ${name.padEnd(16)} : ${desc}`) + } + console.log('') + return + } + + // Current state resolution + let currentState = (await checkLiveServer()) || loadLocalConfigFile() || getScenario('default') + + if (hasFlag('--status')) { + const live = await checkLiveServer() + console.log(live ? `[Backend Server: ONLINE at http://${HOST}:${PORT}]` : `[Backend Server: OFFLINE (reading config file)]`) + printStatus(currentState) + return + } + + if (hasFlag('--scenario')) { + const name = getFlag('--scenario') + try { + const scenario = getScenario(name) + await applyState(scenario) + printStatus(scenario) + } catch (err) { + console.error(`Error: ${err.message}`) + process.exit(1) + } + return + } + + if (hasFlag('--reset')) { + const defaultScenario = getScenario('default') + await applyState(defaultScenario) + console.log('Reset to default scenario successfully.') + return + } + + if (hasFlag('--config')) { + const filePath = getFlag('--config') + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`) + process.exit(1) + } + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) + await applyState(data) + console.log(`Loaded configuration from ${filePath}.`) + return + } + + if (hasFlag('--export')) { + const outPath = getFlag('--export') + fs.writeFileSync(outPath, JSON.stringify(currentState, null, 2), 'utf-8') + console.log(`Exported configuration to ${outPath}.`) + return + } + + if (hasFlag('--set-user')) { + const target = getFlag('--set-user') + const idx = (currentState.users || []).findIndex( + (u) => u.id === target || u.email.toLowerCase() === target.toLowerCase(), + ) + if (idx === -1) { + console.error(`User '${target}' not found. Available: ${currentState.users.map((u) => u.email).join(', ')}`) + process.exit(1) + } + currentState.currentUserIndex = idx + await applyState(currentState) + console.log(`Active user set to: ${currentState.users[idx].name} (${currentState.users[idx].email})`) + return + } + + if (hasFlag('--add-user')) { + const kv = parseKeyValue(getFlag('--add-user')) + if (!kv.name || !kv.email) { + console.error('Error: name and email are required (e.g. name="Marie Curie",email="curie@rad.edu",role=admin)') + process.exit(1) + } + const newUser = { + id: 'usr_' + Date.now().toString(36), + name: kv.name, + email: kv.email, + role: kv.role || 'user', + banned: false, + } + currentState.users = currentState.users || [] + currentState.users.push(newUser) + await applyState(currentState) + console.log(`Added user: ${newUser.name} <${newUser.email}> (${newUser.role})`) + return + } + + if (hasFlag('--add-machine')) { + const kv = parseKeyValue(getFlag('--add-machine')) + if (!kv.name) { + console.error('Error: machine name is required (e.g. name=dgx-a100,ip=10.0.1.50,cpu=64,ram=256Gi,gpu=8)') + process.exit(1) + } + const newMachine = { + name: kv.name, + address: kv.ip || kv.address || '10.0.1.' + Math.floor(Math.random() * 200 + 20), + ready: kv.ready !== 'false', + schedulable: kv.schedulable !== 'false', + cpu_allocatable: kv.cpu || '32', + memory_allocatable: kv.ram || '128Gi', + } + currentState.machines = currentState.machines || [] + currentState.machines.push(newMachine) + + // Add GPU or TT pools if requested + currentState.pools = currentState.pools || [] + if (Number(kv.gpu) > 0) { + currentState.pools.push({ + accelerator: 'nvidia', + node: newMachine.name, + unit: 'GPU', + total: Number(kv.gpu), + allocated: 0, + available: Number(kv.gpu), + ready: true, + }) + } + if (Number(kv.tt) > 0) { + currentState.pools.push({ + accelerator: 'tenstorrent', + node: newMachine.name, + unit: 'board', + total: Number(kv.tt), + allocated: 0, + available: Number(kv.tt), + ready: true, + chips_per_device: 2, + }) + } + await applyState(currentState) + console.log(`Added machine: ${newMachine.name} with ${kv.gpu || 0} GPUs and ${kv.tt || 0} Tenstorrent boards.`) + return + } + + if (hasFlag('--add-team')) { + const kv = parseKeyValue(getFlag('--add-team')) + if (!kv.name) { + console.error('Error: team name is required (e.g. name="Quantum ML",storage=500,models=1000)') + process.exit(1) + } + const activeUser = currentState.users?.[currentState.currentUserIndex] + const newTeam = { + id: 'team-' + kv.name.toLowerCase().replace(/\W+/g, '-'), + name: kv.name, + disabled: false, + policy: { + cpu: kv.cpu || '16', + memory: kv.memory || '64Gi', + nvidia: Number(kv.gpu ?? 2), + tenstorrent: Number(kv.tt ?? 2), + concurrent: 4, + queued: 20, + storage_gib: Number(kv.storage || 300), + model_storage_gib: Number(kv.models || 600), + runtime_seconds: 86400, + registries: ['docker.io', 'ghcr.io'], + }, + members: activeUser + ? [{ id: activeUser.id, name: activeUser.name, email: activeUser.email, common_writer: true }] + : [], + grants: [], + } + currentState.teams = currentState.teams || [] + currentState.teams.push(newTeam) + await applyState(currentState) + console.log(`Added team: ${newTeam.name}`) + return + } + + // --- Interactive Terminal Menu --- + const rl = readline.createInterface({ input, output }) + try { + while (true) { + const live = await checkLiveServer() + console.clear() + console.log('======================================================') + console.log(' MIST FAKE BACKEND CONFIGURATOR') + console.log('======================================================') + console.log(`Backend Server: ${live ? `ONLINE (http://${HOST}:${PORT})` : 'OFFLINE (saving to config file)'}`) + console.log(`Active Scenario: ${currentState.activeScenario || currentState.name || 'custom'}`) + const u = currentState.users?.[currentState.currentUserIndex] + console.log(`Active User: ${u ? `${u.name} <${u.email}> (${u.role})` : 'None'}`) + console.log(`Machines: ${currentState.machines?.map((m) => m.name).join(', ') || 'None'}`) + console.log(`Teams: ${currentState.teams?.map((t) => t.name).join(', ') || 'None'}`) + console.log('------------------------------------------------------') + console.log('Select an action:') + console.log(' 1) Switch to a hypothetical scenario preset') + console.log(' 2) Switch active logged-in user') + console.log(' 3) Add a fake machine / node') + console.log(' 4) Add a fake user') + console.log(' 5) Add a fake team') + console.log(' 6) View full cluster status') + console.log(' 7) Reset to default scenario') + console.log(' 8) Exit') + console.log('------------------------------------------------------') + + const choice = (await rl.question('Enter choice [1-8]: ')).trim() + + if (choice === '1') { + console.log('\nAvailable Hypothetical Scenarios:') + const keys = Object.keys(SCENARIO_DESCRIPTIONS) + keys.forEach((key, i) => { + console.log(` ${i + 1}) ${key.padEnd(16)} : ${SCENARIO_DESCRIPTIONS[key]}`) + }) + const sel = (await rl.question('\nSelect scenario [1-5]: ')).trim() + const idx = parseInt(sel, 10) - 1 + if (keys[idx]) { + currentState = getScenario(keys[idx]) + await applyState(currentState) + await rl.question('\nPress Enter to continue...') + } + } else if (choice === '2') { + console.log('\nAvailable Users:') + currentState.users.forEach((user, i) => { + const mark = i === currentState.currentUserIndex ? ' (ACTIVE)' : '' + console.log(` ${i + 1}) ${user.name} <${user.email}> [${user.role}]${mark}`) + }) + const sel = (await rl.question('\nSelect user number: ')).trim() + const idx = parseInt(sel, 10) - 1 + if (currentState.users[idx]) { + currentState.currentUserIndex = idx + await applyState(currentState) + } + } else if (choice === '3') { + console.log('\n--- Add Fake Machine ---') + const name = (await rl.question('Machine name (e.g. dgx-h100-01): ')).trim() + if (!name) continue + const ip = (await rl.question('IP Address (default 10.0.1.25): ')).trim() || '10.0.1.25' + const cpu = (await rl.question('Allocatable CPU cores (default 32): ')).trim() || '32' + const ram = (await rl.question('Allocatable RAM (default 128Gi): ')).trim() || '128Gi' + const gpu = parseInt((await rl.question('NVIDIA GPUs count (default 0): ')).trim() || '0', 10) + const tt = parseInt((await rl.question('Tenstorrent boards count (default 0): ')).trim() || '0', 10) + + const machine = { + name, + address: ip, + ready: true, + schedulable: true, + cpu_allocatable: cpu, + memory_allocatable: ram, + } + currentState.machines = currentState.machines || [] + currentState.machines.push(machine) + + currentState.pools = currentState.pools || [] + if (gpu > 0) { + currentState.pools.push({ + accelerator: 'nvidia', + node: name, + unit: 'GPU', + total: gpu, + allocated: 0, + available: gpu, + ready: true, + }) + } + if (tt > 0) { + currentState.pools.push({ + accelerator: 'tenstorrent', + node: name, + unit: 'board', + total: tt, + allocated: 0, + available: tt, + ready: true, + chips_per_device: 2, + }) + } + await applyState(currentState) + await rl.question('\nMachine added! Press Enter to continue...') + } else if (choice === '4') { + console.log('\n--- Add Fake User ---') + const name = (await rl.question('Full name (e.g. Marie Curie): ')).trim() + if (!name) continue + const email = (await rl.question('Email (e.g. marie@utmist.gitlab.io): ')).trim() + if (!email) continue + const role = (await rl.question('Role (admin / user, default user): ')).trim() || 'user' + currentState.users = currentState.users || [] + currentState.users.push({ + id: 'usr_' + Date.now().toString(36), + name, + email, + role, + banned: false, + }) + await applyState(currentState) + await rl.question('\nUser added! Press Enter to continue...') + } else if (choice === '5') { + console.log('\n--- Add Fake Team ---') + const name = (await rl.question('Team name (e.g. Quantum Computing Lab): ')).trim() + if (!name) continue + const storage = parseInt((await rl.question('Dataset storage in GiB (default 300): ')).trim() || '300', 10) + const models = parseInt((await rl.question('Model storage in GiB (default 600): ')).trim() || '600', 10) + const activeUser = currentState.users?.[currentState.currentUserIndex] + + const newTeam = { + id: 'team-' + name.toLowerCase().replace(/\W+/g, '-'), + name, + disabled: false, + policy: { + cpu: '16', + memory: '64Gi', + nvidia: 2, + tenstorrent: 2, + concurrent: 4, + queued: 20, + storage_gib: storage, + model_storage_gib: models, + runtime_seconds: 86400, + registries: ['docker.io', 'ghcr.io'], + }, + members: activeUser + ? [{ id: activeUser.id, name: activeUser.name, email: activeUser.email, common_writer: true }] + : [], + grants: [], + } + currentState.teams = currentState.teams || [] + currentState.teams.push(newTeam) + await applyState(currentState) + await rl.question('\nTeam added! Press Enter to continue...') + } else if (choice === '6') { + printStatus(currentState) + await rl.question('\nPress Enter to return to menu...') + } else if (choice === '7') { + currentState = getScenario('default') + await applyState(currentState) + await rl.question('\nReset to default scenario! Press Enter to continue...') + } else if (choice === '8') { + break + } + } + } finally { + rl.close() + } +} + +main().catch((err) => { + console.error('Fatal error:', err) + process.exit(1) +}) diff --git a/scripts/fake-backend-scenarios.mjs b/scripts/fake-backend-scenarios.mjs new file mode 100644 index 0000000..b9a0636 --- /dev/null +++ b/scripts/fake-backend-scenarios.mjs @@ -0,0 +1,643 @@ +/** + * Mist Fake Backend - Built-in Hypothetical Scenarios + * + * Provides modular scenario generators for testing various cluster topologies, + * load conditions, failure modes, and organizational structures. + */ + +export const SCENARIO_DESCRIPTIONS = { + default: 'Standard balanced cluster with 1 GPU node, 1 Tenstorrent node, 2 teams, and 3 users.', + 'high-load': 'Resource-exhausted cluster: 100% GPU/TT saturation, long waiting queue, and 95% full storage.', + unhealthy: 'Degraded cluster with offline nodes, failed jobs, driver error messages, and storage errors.', + 'empty-slate': 'Clean empty cluster with 0 jobs, 0 datasets, and 0 teams for onboarding / first-run testing.', + 'large-cluster': 'Multi-node enterprise setup with 5 machines (H100s, Wormholes), 5 teams, and 30+ jobs.', +} + +export function createDefaultScenario() { + const now = Date.now() + return { + name: 'default', + description: SCENARIO_DESCRIPTIONS.default, + currentUserIndex: 0, + storageBudgetGiB: 2000, + users: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + role: 'admin', + banned: false, + }, + { + id: 'usr_alan', + name: 'Alan Turing', + email: 'alan@utmist.gitlab.io', + role: 'user', + banned: false, + }, + { + id: 'usr_grace', + name: 'Grace Hopper', + email: 'grace@utmist.gitlab.io', + role: 'user', + banned: false, + }, + ], + machines: [ + { + name: 'mist-gpu-node-01', + address: '10.0.1.10', + ready: true, + schedulable: true, + cpu_allocatable: '32', + memory_allocatable: '128Gi', + }, + { + name: 'mist-tt-worker-01', + address: '10.0.1.11', + ready: true, + schedulable: true, + cpu_allocatable: '16', + memory_allocatable: '64Gi', + }, + ], + pools: [ + { + accelerator: 'nvidia', + node: 'mist-gpu-node-01', + unit: 'GPU', + total: 4, + allocated: 1, + available: 3, + ready: true, + }, + { + accelerator: 'tenstorrent', + node: 'mist-tt-worker-01', + unit: 'board', + total: 4, + allocated: 0, + available: 4, + ready: true, + chips_per_device: 2, + }, + ], + storage: { + enabled: true, + capacity_bytes: 1099511627776, // 1 TiB + available_bytes: 879609302220, // 800 GiB + model_capacity_bytes: 2199023255552, // 2 TiB + model_available_bytes: 1759218604441, // 1.6 TiB + dataset_allocation_gib: 500, + model_allocation_gib: 1000, + upload_limit_bytes: 5368709120, // 5 GiB + }, + teams: [ + { + id: 'team-vision', + name: 'Computer Vision', + disabled: false, + policy: { + cpu: '16', + memory: '64Gi', + nvidia: 2, + tenstorrent: 4, + concurrent: 4, + queued: 20, + storage_gib: 300, + model_storage_gib: 600, + runtime_seconds: 86400, + registries: ['docker.io', 'ghcr.io', 'quay.io'], + }, + members: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + common_writer: true, + }, + { + id: 'usr_alan', + name: 'Alan Turing', + email: 'alan@utmist.gitlab.io', + common_writer: false, + }, + ], + grants: [ + { + id: 'grant-nlp-share', + target_team: 'team-nlp', + scope: 'common', + }, + ], + }, + { + id: 'team-nlp', + name: 'Natural Language Processing', + disabled: false, + policy: { + cpu: '8', + memory: '32Gi', + nvidia: 1, + tenstorrent: 2, + concurrent: 2, + queued: 10, + storage_gib: 200, + model_storage_gib: 400, + runtime_seconds: 43200, + registries: ['docker.io', 'ghcr.io'], + }, + members: [ + { + id: 'usr_ada', + name: 'Ada Lovelace', + email: 'ada@utmist.gitlab.io', + common_writer: true, + }, + { + id: 'usr_grace', + name: 'Grace Hopper', + email: 'grace@utmist.gitlab.io', + common_writer: false, + }, + ], + grants: [], + }, + ], + jobs: [ + { + id: 'job-seed-001', + name: 'ResNet50 Vision Benchmark', + created: new Date(now - 3600000).toISOString(), + job_state: 'Success', + accelerator: 'nvidia', + device_count: 2, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '8', + memory: '32Gi', + node: 'mist-gpu-node-01', + exit_code: 0, + message: 'Job completed successfully in 1420s', + checkpoint_directory: '/outputs/checkpoints/job-seed-001', + output_directory: '/outputs/runs/job-seed-001', + timeout_seconds: 7200, + team_id: 'team-vision', + creator_id: 'usr_ada', + creator_name: 'Ada Lovelace', + storage_scope: 'common', + can_cancel: false, + }, + { + id: 'job-seed-002', + name: 'BERT Fine-tuning Test', + created: new Date(now - 1800000).toISOString(), + job_state: 'InProgress', + accelerator: 'nvidia', + device_count: 1, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '4', + memory: '16Gi', + node: 'mist-gpu-node-01', + checkpoint_directory: '/outputs/checkpoints/job-seed-002', + output_directory: '/outputs/runs/job-seed-002', + timeout_seconds: 3600, + team_id: 'team-nlp', + creator_id: 'usr_grace', + creator_name: 'Grace Hopper', + storage_scope: 'usr_grace', + can_cancel: true, + }, + { + id: 'job-seed-003', + name: 'Tenstorrent Wormhole Matrix Multiplication', + created: new Date(now - 600000).toISOString(), + job_state: 'Scheduled', + accelerator: 'tenstorrent', + device_count: 2, + image: 'ghcr.io/utmist/tt-metal-runtime:latest', + cpu: '4', + memory: '16Gi', + message: 'Queued; waiting for 2 Tenstorrent n300 board allocations', + checkpoint_directory: '/outputs/checkpoints/job-seed-003', + output_directory: '/outputs/runs/job-seed-003', + timeout_seconds: 1800, + team_id: 'team-vision', + creator_id: 'usr_alan', + creator_name: 'Alan Turing', + storage_scope: 'usr_alan', + can_cancel: true, + }, + ], + datasets: [ + { + id: 'ds-cifar100', + name: 'CIFAR-100 Benchmark Dataset', + filename: 'cifar100.zip', + size: 169000000, + files: 100, + sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + created: new Date(now - 86400000).toISOString(), + state: 'ready', + team_id: 'team-vision', + team_name: 'Computer Vision', + scope: 'common', + member_name: 'Ada Lovelace', + shared: false, + }, + { + id: 'ds-wikitext', + name: 'Wikitext-103 Subset', + filename: 'wikitext.tar.gz', + size: 512000000, + files: 1, + sha256: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', + created: new Date(now - 43200000).toISOString(), + state: 'ready', + team_id: 'team-nlp', + team_name: 'Natural Language Processing', + scope: 'common', + member_name: 'Grace Hopper', + shared: true, + }, + ], + } +} + +export function createHighLoadScenario() { + const now = Date.now() + const base = createDefaultScenario() + base.name = 'high-load' + base.description = SCENARIO_DESCRIPTIONS['high-load'] + + // Saturated hardware: 0 available + base.pools = [ + { + accelerator: 'nvidia', + node: 'mist-gpu-node-01', + unit: 'GPU', + total: 4, + allocated: 4, + available: 0, + ready: true, + }, + { + accelerator: 'tenstorrent', + node: 'mist-tt-worker-01', + unit: 'board', + total: 4, + allocated: 4, + available: 0, + ready: true, + chips_per_device: 2, + }, + ] + + // Storage 95% consumed + base.storage = { + enabled: true, + capacity_bytes: 1073741824000, // 1 TiB + available_bytes: 32212254720, // 30 GiB left (near capacity!) + model_capacity_bytes: 2147483648000, + model_available_bytes: 53687091200, // 50 GiB left + dataset_allocation_gib: 980, + model_allocation_gib: 1950, + upload_limit_bytes: 2147483648, + } + + // 4 running jobs taking all accelerators + 8 queued jobs + base.jobs = [ + { + id: 'job-hl-run-01', + name: 'LLM Multi-GPU Pretraining Node 1', + created: new Date(now - 7200000).toISOString(), + job_state: 'InProgress', + accelerator: 'nvidia', + device_count: 2, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '16', + memory: '64Gi', + node: 'mist-gpu-node-01', + checkpoint_directory: '/outputs/checkpoints/job-hl-run-01', + output_directory: '/outputs/runs/job-hl-run-01', + team_id: 'team-vision', + creator_name: 'Ada Lovelace', + can_cancel: true, + }, + { + id: 'job-hl-run-02', + name: 'Diffusion Video Upscaler', + created: new Date(now - 3600000).toISOString(), + job_state: 'InProgress', + accelerator: 'nvidia', + device_count: 2, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '16', + memory: '64Gi', + node: 'mist-gpu-node-01', + checkpoint_directory: '/outputs/checkpoints/job-hl-run-02', + output_directory: '/outputs/runs/job-hl-run-02', + team_id: 'team-nlp', + creator_name: 'Grace Hopper', + can_cancel: true, + }, + { + id: 'job-hl-run-03', + name: 'TT Wormhole Full-Board Stress Test', + created: new Date(now - 5400000).toISOString(), + job_state: 'InProgress', + accelerator: 'tenstorrent', + device_count: 4, + image: 'ghcr.io/utmist/tt-metal-runtime:latest', + cpu: '16', + memory: '64Gi', + node: 'mist-tt-worker-01', + checkpoint_directory: '/outputs/checkpoints/job-hl-run-03', + output_directory: '/outputs/runs/job-hl-run-03', + team_id: 'team-vision', + creator_name: 'Alan Turing', + can_cancel: true, + }, + // Queued jobs + ...Array.from({ length: 8 }, (_, i) => ({ + id: `job-hl-wait-0${i + 1}`, + name: `Queued Experiment #${i + 1}`, + created: new Date(now - (8 - i) * 300000).toISOString(), + job_state: 'Scheduled', + accelerator: i % 2 === 0 ? 'nvidia' : 'tenstorrent', + device_count: (i % 2) + 1, + image: i % 2 === 0 ? 'ghcr.io/utmist/pytorch-cuda:latest' : 'ghcr.io/utmist/tt-metal-runtime:latest', + cpu: '4', + memory: '16Gi', + message: `Waiting for resources (Position in queue: ${i + 1})`, + checkpoint_directory: `/outputs/checkpoints/wait-${i + 1}`, + output_directory: `/outputs/runs/wait-${i + 1}`, + team_id: i % 2 === 0 ? 'team-vision' : 'team-nlp', + creator_name: i % 3 === 0 ? 'Ada Lovelace' : i % 3 === 1 ? 'Alan Turing' : 'Grace Hopper', + can_cancel: true, + })), + ] + + return base +} + +export function createUnhealthyScenario() { + const now = Date.now() + const base = createDefaultScenario() + base.name = 'unhealthy' + base.description = SCENARIO_DESCRIPTIONS.unhealthy + + base.machines = [ + { + name: 'mist-gpu-node-01', + address: '10.0.1.10', + ready: false, + schedulable: false, + cpu_allocatable: '0', + memory_allocatable: '0Gi', + }, + { + name: 'mist-tt-worker-01', + address: '10.0.1.11', + ready: true, + schedulable: false, // Placement paused! + cpu_allocatable: '16', + memory_allocatable: '64Gi', + }, + ] + + base.pools = [ + { + accelerator: 'nvidia', + node: 'mist-gpu-node-01', + unit: 'GPU', + total: 4, + allocated: 0, + available: null, + ready: false, + message: 'Node offline; NVIDIA driver communication lost (NVML error 999)', + }, + { + accelerator: 'tenstorrent', + node: 'mist-tt-worker-01', + unit: 'board', + total: 4, + allocated: 0, + available: 0, + ready: false, + chips_per_device: 2, + message: 'Node cordon active: scheduling paused for driver kernel patch', + }, + ] + + base.jobs = [ + { + id: 'job-fail-001', + name: 'Vision Model OOM Failure', + created: new Date(now - 7200000).toISOString(), + job_state: 'Failure', + accelerator: 'nvidia', + device_count: 2, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '8', + memory: '16Gi', + node: 'mist-gpu-node-01', + exit_code: 137, + message: 'Command terminated by SIGKILL (Out of Memory - cgroup limit exceeded)', + checkpoint_directory: '/outputs/checkpoints/job-fail-001', + output_directory: '/outputs/runs/job-fail-001', + team_id: 'team-vision', + creator_name: 'Ada Lovelace', + can_cancel: false, + }, + { + id: 'job-fail-002', + name: 'Faulty Python Script Crash', + created: new Date(now - 3600000).toISOString(), + job_state: 'Failure', + accelerator: 'cpu', + device_count: 0, + image: 'docker.io/library/python:3.11-slim', + cpu: '2', + memory: '4Gi', + node: 'mist-tt-worker-01', + exit_code: 1, + message: 'RuntimeError: ModuleNotFoundError: No module named torch_geometric', + checkpoint_directory: '/outputs/checkpoints/job-fail-002', + output_directory: '/outputs/runs/job-fail-002', + team_id: 'team-nlp', + creator_name: 'Grace Hopper', + can_cancel: false, + }, + { + id: 'job-stuck-003', + name: 'Stuck Queued Job', + created: new Date(now - 1800000).toISOString(), + job_state: 'Scheduled', + accelerator: 'nvidia', + device_count: 1, + image: 'ghcr.io/utmist/pytorch-cuda:latest', + cpu: '4', + memory: '16Gi', + message: '0/2 nodes are available: 1 node is offline, 1 node has untolerated taint', + checkpoint_directory: '/outputs/checkpoints/job-stuck-003', + output_directory: '/outputs/runs/job-stuck-003', + team_id: 'team-vision', + creator_name: 'Alan Turing', + can_cancel: true, + }, + ] + + return base +} + +export function createEmptySlateScenario() { + return { + name: 'empty-slate', + description: SCENARIO_DESCRIPTIONS['empty-slate'], + currentUserIndex: 0, + storageBudgetGiB: 1000, + users: [ + { + id: 'usr_newcomer', + name: 'New Researcher', + email: 'researcher@utmist.gitlab.io', + role: 'admin', + banned: false, + }, + ], + machines: [ + { + name: 'lab-cluster-master', + address: '192.168.1.10', + ready: true, + schedulable: true, + cpu_allocatable: '16', + memory_allocatable: '64Gi', + }, + ], + pools: [ + { + accelerator: 'nvidia', + node: 'lab-cluster-master', + unit: 'GPU', + total: 2, + allocated: 0, + available: 2, + ready: true, + }, + ], + storage: { + enabled: true, + capacity_bytes: 536870912000, // 500 GiB + available_bytes: 536870912000, + model_capacity_bytes: 1073741824000, + model_available_bytes: 1073741824000, + dataset_allocation_gib: 0, + model_allocation_gib: 0, + upload_limit_bytes: 5368709120, + }, + teams: [], + jobs: [], + datasets: [], + } +} + +export function createLargeClusterScenario() { + const now = Date.now() + const base = createDefaultScenario() + base.name = 'large-cluster' + base.description = SCENARIO_DESCRIPTIONS['large-cluster'] + base.storageBudgetGiB = 10000 + + base.users = [ + { id: 'usr_ada', name: 'Ada Lovelace', email: 'ada@utmist.gitlab.io', role: 'admin', banned: false }, + { id: 'usr_alan', name: 'Alan Turing', email: 'alan@utmist.gitlab.io', role: 'user', banned: false }, + { id: 'usr_grace', name: 'Grace Hopper', email: 'grace@utmist.gitlab.io', role: 'user', banned: false }, + { id: 'usr_curie', name: 'Marie Curie', email: 'marie@utmist.gitlab.io', role: 'admin', banned: false }, + { id: 'usr_feynman', name: 'Richard Feynman', email: 'feynman@utmist.gitlab.io', role: 'user', banned: false }, + { id: 'usr_shannon', name: 'Claude Shannon', email: 'shannon@utmist.gitlab.io', role: 'user', banned: false }, + { id: 'usr_neumann', name: 'John von Neumann', email: 'neumann@utmist.gitlab.io', role: 'user', banned: true }, + ] + + base.machines = [ + { name: 'dgx-h100-node-01', address: '10.10.1.1', ready: true, schedulable: true, cpu_allocatable: '64', memory_allocatable: '512Gi' }, + { name: 'dgx-h100-node-02', address: '10.10.1.2', ready: true, schedulable: true, cpu_allocatable: '64', memory_allocatable: '512Gi' }, + { name: 'tt-wormhole-box-01', address: '10.10.1.3', ready: true, schedulable: true, cpu_allocatable: '32', memory_allocatable: '128Gi' }, + { name: 'tt-wormhole-box-02', address: '10.10.1.4', ready: true, schedulable: true, cpu_allocatable: '32', memory_allocatable: '128Gi' }, + { name: 'control-plane-k3s', address: '10.10.1.100', ready: true, schedulable: true, cpu_allocatable: '16', memory_allocatable: '64Gi' }, + ] + + base.pools = [ + { accelerator: 'nvidia', node: 'dgx-h100-node-01', unit: 'GPU', total: 8, allocated: 4, available: 4, ready: true }, + { accelerator: 'nvidia', node: 'dgx-h100-node-02', unit: 'GPU', total: 8, allocated: 6, available: 2, ready: true }, + { accelerator: 'tenstorrent', node: 'tt-wormhole-box-01', unit: 'board', total: 4, allocated: 2, available: 2, ready: true, chips_per_device: 2 }, + { accelerator: 'tenstorrent', node: 'tt-wormhole-box-02', unit: 'board', total: 4, allocated: 1, available: 3, ready: true, chips_per_device: 2 }, + ] + + base.teams = [ + { + id: 'team-cv', + name: 'Computer Vision Lab', + disabled: false, + policy: { cpu: '32', memory: '128Gi', nvidia: 8, tenstorrent: 4, concurrent: 8, queued: 40, storage_gib: 1000, model_storage_gib: 2000, runtime_seconds: 86400, registries: ['docker.io', 'ghcr.io'] }, + members: [{ id: 'usr_ada', name: 'Ada Lovelace', email: 'ada@utmist.gitlab.io', common_writer: true }, { id: 'usr_curie', name: 'Marie Curie', email: 'marie@utmist.gitlab.io', common_writer: true }], + grants: [], + }, + { + id: 'team-llm', + name: 'Foundation Models & NLP', + disabled: false, + policy: { cpu: '64', memory: '256Gi', nvidia: 8, tenstorrent: 2, concurrent: 6, queued: 30, storage_gib: 2000, model_storage_gib: 4000, runtime_seconds: 86400, registries: ['docker.io', 'ghcr.io'] }, + members: [{ id: 'usr_grace', name: 'Grace Hopper', email: 'grace@utmist.gitlab.io', common_writer: true }, { id: 'usr_shannon', name: 'Claude Shannon', email: 'shannon@utmist.gitlab.io', common_writer: false }], + grants: [], + }, + { + id: 'team-robotics', + name: 'Autonomous Robotics', + disabled: false, + policy: { cpu: '16', memory: '64Gi', nvidia: 4, tenstorrent: 4, concurrent: 4, queued: 20, storage_gib: 800, model_storage_gib: 1200, runtime_seconds: 43200, registries: ['docker.io'] }, + members: [{ id: 'usr_alan', name: 'Alan Turing', email: 'alan@utmist.gitlab.io', common_writer: true }, { id: 'usr_feynman', name: 'Richard Feynman', email: 'feynman@utmist.gitlab.io', common_writer: false }], + grants: [], + }, + ] + + // Generate 25 jobs across all states + const jobStates = ['Success', 'InProgress', 'Scheduled', 'Failure', 'Cancelled'] + base.jobs = Array.from({ length: 25 }, (_, i) => ({ + id: `job-enterprise-${String(i + 1).padStart(3, '0')}`, + name: `Model Exploration #${i + 1}`, + created: new Date(now - (25 - i) * 1200000).toISOString(), + job_state: jobStates[i % jobStates.length], + accelerator: i % 3 === 0 ? 'nvidia' : i % 3 === 1 ? 'tenstorrent' : 'cpu', + device_count: i % 3 === 2 ? 0 : (i % 4) + 1, + image: i % 3 === 0 ? 'ghcr.io/utmist/pytorch-cuda:latest' : i % 3 === 1 ? 'ghcr.io/utmist/tt-metal-runtime:latest' : 'docker.io/library/python:3.11-slim', + cpu: String((i % 4 + 1) * 4), + memory: `${(i % 4 + 1) * 16}Gi`, + node: i % 2 === 0 ? 'dgx-h100-node-01' : 'tt-wormhole-box-01', + exit_code: jobStates[i % jobStates.length] === 'Failure' ? 1 : jobStates[i % jobStates.length] === 'Success' ? 0 : undefined, + message: jobStates[i % jobStates.length] === 'Failure' ? 'Killed by cluster controller' : undefined, + checkpoint_directory: `/outputs/checkpoints/job-enterprise-${i + 1}`, + output_directory: `/outputs/runs/job-enterprise-${i + 1}`, + team_id: i % 2 === 0 ? 'team-cv' : 'team-llm', + creator_name: base.users[i % base.users.length].name, + can_cancel: jobStates[i % jobStates.length] === 'InProgress' || jobStates[i % jobStates.length] === 'Scheduled', + })) + + return base +} + +export const SCENARIO_FACTORIES = { + default: createDefaultScenario, + 'high-load': createHighLoadScenario, + unhealthy: createUnhealthyScenario, + 'empty-slate': createEmptySlateScenario, + 'large-cluster': createLargeClusterScenario, +} + +export function getScenario(name) { + const factory = SCENARIO_FACTORIES[name] + if (!factory) { + throw new Error(`Unknown scenario '${name}'. Available: ${Object.keys(SCENARIO_FACTORIES).join(', ')}`) + } + return factory() +} diff --git a/scripts/fake-backend.mjs b/scripts/fake-backend.mjs index 90c8c29..00d33f5 100644 --- a/scripts/fake-backend.mjs +++ b/scripts/fake-backend.mjs @@ -8,282 +8,181 @@ * * Usage: * node scripts/fake-backend.mjs + * node scripts/fake-backend.mjs --scenario high-load + * node scripts/fake-backend.mjs --config ./custom-config.json * PORT=3000 node scripts/fake-backend.mjs * npm --prefix web-interface run fake-backend */ import http from 'node:http' +import fs from 'node:fs' +import path from 'node:path' import { URL } from 'node:url' import crypto from 'node:crypto' +import { + getScenario, + SCENARIO_DESCRIPTIONS, + SCENARIO_FACTORIES, +} from './fake-backend-scenarios.mjs' + +// Parse command line arguments +const args = process.argv.slice(2) +function getArg(flag) { + const idx = args.indexOf(flag) + return idx !== -1 && args[idx + 1] ? args[idx + 1] : null +} -const PORT = Number(process.env.PORT || process.env.MIST_FAKE_PORT || 3000) +const PORT = Number( + getArg('--port') || process.env.PORT || process.env.MIST_FAKE_PORT || 3000, +) const HOST = process.env.HOST || '127.0.0.1' +const DEFAULT_CONFIG_FILE = path.resolve(process.cwd(), '.fake-backend-config.json') -// --- In-Memory State & Seed Data --- - -const state = { - users: [ - { - id: 'usr_ada', - name: 'Ada Lovelace', - email: 'ada@utmist.gitlab.io', - role: 'admin', - banned: false, - }, - { - id: 'usr_alan', - name: 'Alan Turing', - email: 'alan@utmist.gitlab.io', - role: 'user', - banned: false, - }, - { - id: 'usr_grace', - name: 'Grace Hopper', - email: 'grace@utmist.gitlab.io', - role: 'user', - banned: false, - }, - ], - currentUserIndex: 0, // Ada Lovelace by default - teams: [ - { - id: 'team-vision', - name: 'Computer Vision', - disabled: false, - policy: { - cpu: '16', - memory: '64Gi', - nvidia: 2, - tenstorrent: 4, - concurrent: 4, - queued: 20, - storage_gib: 300, - model_storage_gib: 600, - runtime_seconds: 86400, - registries: ['docker.io', 'ghcr.io', 'quay.io'], - }, - members: [ - { - id: 'usr_ada', - name: 'Ada Lovelace', - email: 'ada@utmist.gitlab.io', - common_writer: true, - }, - { - id: 'usr_alan', - name: 'Alan Turing', - email: 'alan@utmist.gitlab.io', - common_writer: false, - }, - ], - grants: [ - { - id: 'grant-nlp-share', - target_team: 'team-nlp', - scope: 'common', - }, - ], - }, - { - id: 'team-nlp', - name: 'Natural Language Processing', - disabled: false, - policy: { - cpu: '8', - memory: '32Gi', - nvidia: 1, - tenstorrent: 2, - concurrent: 2, - queued: 10, - storage_gib: 200, - model_storage_gib: 400, - runtime_seconds: 43200, - registries: ['docker.io', 'ghcr.io'], - }, - members: [ - { - id: 'usr_ada', - name: 'Ada Lovelace', - email: 'ada@utmist.gitlab.io', - common_writer: true, - }, - { - id: 'usr_grace', - name: 'Grace Hopper', - email: 'grace@utmist.gitlab.io', - common_writer: false, - }, - ], - grants: [], - }, - ], +// --- Initial State Management --- + +let state = { + activeScenario: 'default', + users: [], + currentUserIndex: 0, + machines: [], + pools: [], storageBudgetGiB: 2000, - jobs: [ - { - id: 'job-seed-001', - name: 'ResNet50 Vision Benchmark', - created: new Date(Date.now() - 3600000).toISOString(), - job_state: 'Success', - accelerator: 'nvidia', - device_count: 2, - image: 'ghcr.io/utmist/pytorch-cuda:latest', - cpu: '8', - memory: '32Gi', - node: 'mist-gpu-node-01', - exit_code: 0, - message: 'Job completed successfully in 1420s', - checkpoint_directory: '/outputs/checkpoints/job-seed-001', - output_directory: '/outputs/runs/job-seed-001', - timeout_seconds: 7200, - team_id: 'team-vision', - creator_id: 'usr_ada', - creator_name: 'Ada Lovelace', - storage_scope: 'common', - can_cancel: false, - }, - { - id: 'job-seed-002', - name: 'BERT Fine-tuning Test', - created: new Date(Date.now() - 1800000).toISOString(), - job_state: 'InProgress', - accelerator: 'nvidia', - device_count: 1, - image: 'ghcr.io/utmist/pytorch-cuda:latest', - cpu: '4', - memory: '16Gi', - node: 'mist-gpu-node-01', - checkpoint_directory: '/outputs/checkpoints/job-seed-002', - output_directory: '/outputs/runs/job-seed-002', - timeout_seconds: 3600, - team_id: 'team-nlp', - creator_id: 'usr_grace', - creator_name: 'Grace Hopper', - storage_scope: 'usr_grace', - can_cancel: true, - }, - { - id: 'job-seed-003', - name: 'Tenstorrent Wormhole Matrix Multiplication', - created: new Date(Date.now() - 600000).toISOString(), - job_state: 'Scheduled', - accelerator: 'tenstorrent', - device_count: 2, - image: 'ghcr.io/utmist/tt-metal-runtime:latest', - cpu: '4', - memory: '16Gi', - message: 'Queued; waiting for 2 Tenstorrent n300 board allocations', - checkpoint_directory: '/outputs/checkpoints/job-seed-003', - output_directory: '/outputs/runs/job-seed-003', - timeout_seconds: 1800, - team_id: 'team-vision', - creator_id: 'usr_alan', - creator_name: 'Alan Turing', - storage_scope: 'usr_alan', - can_cancel: true, - }, - ], - datasets: [ - { - id: 'ds-cifar100', - name: 'CIFAR-100 Benchmark Dataset', - filename: 'cifar100.zip', - size: 169000000, - files: 100, - sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', - created: new Date(Date.now() - 86400000).toISOString(), - state: 'ready', - team_id: 'team-vision', - team_name: 'Computer Vision', - scope: 'common', - member_name: 'Ada Lovelace', - shared: false, - }, - { - id: 'ds-wikitext', - name: 'Wikitext-103 Subset', - filename: 'wikitext.tar.gz', - size: 512000000, - files: 1, - sha256: 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', - created: new Date(Date.now() - 43200000).toISOString(), - state: 'ready', - team_id: 'team-nlp', - team_name: 'Natural Language Processing', - scope: 'common', - member_name: 'Grace Hopper', - shared: true, - }, - ], - jobLogs: new Map([ - [ - 'job-seed-001', - `[2026-10-16 20:00:01] [INFO] Initializing PyTorch runtime on NVIDIA node 'mist-gpu-node-01' -[2026-10-16 20:00:03] [INFO] Allocated 2x NVIDIA RTX A6000 GPUs (CUDA devices 0, 1) -[2026-10-16 20:00:05] [INFO] Loading CIFAR-100 dataset from /inputs/cifar100 -[2026-10-16 20:00:12] [INFO] Epoch 1/10: train_loss=2.31, val_acc=68.4% -[2026-10-16 20:10:45] [INFO] Epoch 10/10: train_loss=0.42, val_acc=84.2% -[2026-10-16 20:23:41] [INFO] Checkpoint saved to /outputs/runs/job-seed-001/checkpoint_final.pt -[2026-10-16 20:23:42] [INFO] Training complete. Process exited with code 0.`, - ], - [ - 'job-seed-002', - `[2026-10-16 20:30:00] [INFO] BERT fine-tuning initialized. -[2026-10-16 20:30:02] [INFO] Using GPU 0 (Allocated 1x GPU) -[2026-10-16 20:30:15] [INFO] Warmup steps complete. Training batch 450/2000... -[2026-10-16 20:30:30] [INFO] Current loss: 1.284`, - ], - [ - 'job-seed-003', - `[2026-10-16 20:50:00] [INFO] Job queued in team 'team-vision'. Awaiting Tenstorrent admission controller.`, + storage: {}, + teams: [], + jobs: [], + datasets: [], + images: { + self_service: true, + teams_enabled: true, + registries: ['docker.io', 'ghcr.io', 'quay.io', 'localhost:5000'], + images: [ + { reference: 'docker.io/library/python:3.11-slim', accelerators: ['cpu'] }, + { reference: 'docker.io/library/pytorch-cpu:2.2.0', accelerators: ['cpu'] }, + { reference: 'ghcr.io/utmist/pytorch-cuda:latest', accelerators: ['nvidia'] }, + { reference: 'ghcr.io/utmist/tt-metal-runtime:latest', accelerators: ['tenstorrent'] }, ], - ]), - jobFiles: new Map([ - [ - 'job-seed-001', - [ - { - path: 'checkpoint_final.pt', - size: 104857600, - modified: new Date(Date.now() - 3600000).toISOString(), - }, - { - path: 'training_metrics.json', - size: 4096, - modified: new Date(Date.now() - 3600000).toISOString(), - }, - { - path: 'run_summary.txt', - size: 512, - modified: new Date(Date.now() - 3600000).toISOString(), - }, - ], + profiles: [ + { + accelerator: 'cpu', + default_image: 'docker.io/library/python:3.11-slim', + device_unit: 'core', + max_devices: 16, + note: 'Standard CPU runtime', + }, + { + accelerator: 'nvidia', + default_image: 'ghcr.io/utmist/pytorch-cuda:latest', + device_unit: 'GPU', + max_devices: 4, + note: 'NVIDIA CUDA 12.4 + PyTorch runtime', + }, + { + accelerator: 'tenstorrent', + default_image: 'ghcr.io/utmist/tt-metal-runtime:latest', + device_unit: 'board', + max_devices: 4, + note: 'Tenstorrent Wormhole n300 PCIe runtime', + }, ], - ]), + }, + jobLogs: new Map(), + jobFiles: new Map(), +} + +export function applyConfiguration(config) { + if (config.users) state.users = JSON.parse(JSON.stringify(config.users)) + if (config.currentUserIndex !== undefined) state.currentUserIndex = config.currentUserIndex + if (config.activeUserId) { + const idx = state.users.findIndex( + (u) => u.id === config.activeUserId || u.email.toLowerCase() === config.activeUserId.toLowerCase(), + ) + if (idx >= 0) state.currentUserIndex = idx + } + if (config.machines) state.machines = JSON.parse(JSON.stringify(config.machines)) + if (config.pools) state.pools = JSON.parse(JSON.stringify(config.pools)) + if (config.storage) state.storage = JSON.parse(JSON.stringify(config.storage)) + if (config.storageBudgetGiB !== undefined) state.storageBudgetGiB = config.storageBudgetGiB + if (config.teams) state.teams = JSON.parse(JSON.stringify(config.teams)) + if (config.jobs) { + state.jobs = JSON.parse(JSON.stringify(config.jobs)) + for (const job of state.jobs) { + if (!state.jobLogs.has(job.id)) { + state.jobLogs.set( + job.id, + `[${job.created}] Job ${job.name} initialized in state ${job.job_state}\n`, + ) + } + } + } + if (config.datasets) state.datasets = JSON.parse(JSON.stringify(config.datasets)) + if (config.images) state.images = JSON.parse(JSON.stringify(config.images)) + if (config.name) state.activeScenario = config.name +} + +// Determine initial scenario / config +function initializeState() { + const cliScenario = getArg('--scenario') || process.env.MIST_FAKE_SCENARIO + const cliConfig = getArg('--config') || process.env.MIST_FAKE_CONFIG + + if (cliScenario) { + try { + console.log(`[Config] Loading requested scenario: '${cliScenario}'`) + applyConfiguration(getScenario(cliScenario)) + return + } catch (err) { + console.warn(`[Config] Warning: ${err.message}. Falling back to default.`) + } + } + + if (cliConfig && fs.existsSync(cliConfig)) { + try { + console.log(`[Config] Loading configuration from: ${cliConfig}`) + const raw = fs.readFileSync(cliConfig, 'utf-8') + applyConfiguration(JSON.parse(raw)) + return + } catch (err) { + console.warn(`[Config] Failed to load config from ${cliConfig}: ${err.message}`) + } + } + + if (fs.existsSync(DEFAULT_CONFIG_FILE)) { + try { + console.log(`[Config] Found local ${DEFAULT_CONFIG_FILE}, loading...`) + const raw = fs.readFileSync(DEFAULT_CONFIG_FILE, 'utf-8') + applyConfiguration(JSON.parse(raw)) + return + } catch (err) { + console.warn(`[Config] Failed to parse ${DEFAULT_CONFIG_FILE}: ${err.message}`) + } + } + + // Fallback to default scenario + applyConfiguration(getScenario('default')) } -// Automatically progress queued jobs to running and completed +initializeState() + +// Automatically progress queued jobs over time setInterval(() => { for (const job of state.jobs) { if (job.job_state === 'Scheduled') { - job.job_state = 'InProgress' - job.node = - job.accelerator === 'tenstorrent' - ? 'mist-tt-worker-01' - : 'mist-gpu-node-01' - job.message = 'Pod running on ' + job.node - const logs = - state.jobLogs.get(job.id) || - `[${new Date().toISOString()}] Job submitted.\n` - state.jobLogs.set( - job.id, - logs + - `[${new Date().toISOString()}] Admitted to cluster. Initializing pod on ${job.node}...\n`, + // Find suitable node if available + const pool = state.pools.find( + (p) => p.accelerator === job.accelerator && p.ready && (p.available === null || p.available > 0), ) + if (pool) { + job.job_state = 'InProgress' + job.node = pool.node + job.message = `Pod running on ${job.node}` + const logs = state.jobLogs.get(job.id) || `[${new Date().toISOString()}] Job submitted.\n` + state.jobLogs.set( + job.id, + logs + `[${new Date().toISOString()}] Admitted to cluster. Initializing pod on ${job.node}...\n`, + ) + } } else if (job.job_state === 'InProgress') { const elapsed = Date.now() - new Date(job.created).getTime() - if (elapsed > 20000) { - // Complete job after 20s + if (elapsed > 20000 && !job.id.startsWith('job-hl-run')) { job.job_state = 'Success' job.exit_code = 0 job.message = 'Job completed successfully' @@ -291,21 +190,12 @@ setInterval(() => { const logs = state.jobLogs.get(job.id) || '' state.jobLogs.set( job.id, - logs + - `[${new Date().toISOString()}] Execution finished with code 0.\nOutputs written to /outputs.\n`, + logs + `[${new Date().toISOString()}] Execution finished with code 0.\nOutputs saved.\n`, ) if (!state.jobFiles.has(job.id)) { state.jobFiles.set(job.id, [ - { - path: 'output.log', - size: 2048, - modified: new Date().toISOString(), - }, - { - path: 'model_weights.bin', - size: 52428800, - modified: new Date().toISOString(), - }, + { path: 'output.log', size: 2048, modified: new Date().toISOString() }, + { path: 'model_weights.bin', size: 52428800, modified: new Date().toISOString() }, ]) } } @@ -361,8 +251,7 @@ const server = http.createServer(async (req, res) => { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': - 'Content-Type, X-Mist-Team, Authorization', + 'Access-Control-Allow-Headers': 'Content-Type, X-Mist-Team, Authorization', 'Access-Control-Max-Age': '86400', }) res.end() @@ -374,7 +263,7 @@ const server = http.createServer(async (req, res) => { const query = Object.fromEntries(parsedUrl.searchParams.entries()) const activeTeam = req.headers['x-mist-team'] || query.team_id || '' - // Normalize path by stripping optional '/api' prefix (Vite proxy rewrites /api, direct hits do not) + // Normalize path by stripping optional '/api' prefix if (pathname.startsWith('/api/')) { pathname = pathname.slice(4) } else if (pathname === '/api') { @@ -384,11 +273,77 @@ const server = http.createServer(async (req, res) => { const currentUser = state.users[state.currentUserIndex] || null try { + // ---------------------------------------------------- + // FAKE BACKEND DYNAMIC CONFIGURATION & SCENARIO CONTROL + // ---------------------------------------------------- + + if (pathname === '/_fake/state' && req.method === 'GET') { + sendJSON(res, 200, { + activeScenario: state.activeScenario, + currentUserIndex: state.currentUserIndex, + currentUser, + users: state.users, + machines: state.machines, + pools: state.pools, + storage: state.storage, + storageBudgetGiB: state.storageBudgetGiB, + teams: state.teams, + jobs: state.jobs, + datasets: state.datasets, + }) + return + } + + if ((pathname === '/_fake/state' || pathname === '/_fake/configure') && req.method === 'POST') { + const body = await parseJSONBody(req) + applyConfiguration(body) + sendJSON(res, 200, { + success: true, + message: 'Configuration applied successfully', + activeScenario: state.activeScenario, + }) + return + } + + if (pathname === '/_fake/scenarios' && req.method === 'GET') { + sendJSON(res, 200, { + scenarios: Object.entries(SCENARIO_DESCRIPTIONS).map(([name, description]) => ({ + name, + description, + active: state.activeScenario === name, + })), + }) + return + } + + const scenarioMatch = pathname.match(/^\/_fake\/scenario\/([^/]+)$/) + if (scenarioMatch && req.method === 'POST') { + const name = scenarioMatch[1] + try { + const scenarioData = getScenario(name) + applyConfiguration(scenarioData) + sendJSON(res, 200, { + success: true, + scenario: name, + message: `Switched to hypothetical scenario: '${name}'`, + }) + } catch (err) { + sendError(res, 400, err.message) + } + return + } + + if (pathname === '/_fake/reset' && req.method === 'POST') { + const defaultData = getScenario('default') + applyConfiguration(defaultData) + sendJSON(res, 200, { success: true, message: 'Reset to default scenario' }) + return + } + // ---------------------------------------------------- // AUTH & SESSION ENDPOINTS // ---------------------------------------------------- - // Session check used by AccountGate if (pathname === '/session' && req.method === 'GET') { sendJSON(res, 200, { enabled: true, @@ -397,7 +352,6 @@ const server = http.createServer(async (req, res) => { return } - // Better Auth: Session retrieval if (pathname === '/auth/get-session' && req.method === 'GET') { if (!currentUser) { sendJSON(res, 200, null) @@ -414,7 +368,6 @@ const server = http.createServer(async (req, res) => { return } - // Better Auth: Email Sign-in if (pathname === '/auth/sign-in/email' && req.method === 'POST') { const body = await parseJSONBody(req) const user = state.users.find( @@ -444,24 +397,20 @@ const server = http.createServer(async (req, res) => { return } - // Better Auth: Sign-out if (pathname === '/auth/sign-out' && req.method === 'POST') { state.currentUserIndex = -1 // signed out sendJSON(res, 200, { success: true }) return } - // Better Auth: Change Password if (pathname === '/auth/change-password' && req.method === 'POST') { await parseJSONBody(req) sendJSON(res, 200, { success: true }) return } - // Better Auth Admin: List Users if ( - (pathname === '/auth/admin/list-users' || - pathname === '/auth/admin/listUsers') && + (pathname === '/auth/admin/list-users' || pathname === '/auth/admin/listUsers') && (req.method === 'GET' || req.method === 'POST') ) { sendJSON(res, 200, { @@ -471,7 +420,6 @@ const server = http.createServer(async (req, res) => { return } - // Better Auth Admin: Create User if (pathname === '/auth/admin/create-user' && req.method === 'POST') { const body = await parseJSONBody(req) if (!body.name || !body.email) { @@ -490,24 +438,18 @@ const server = http.createServer(async (req, res) => { return } - // Better Auth Admin: Set User Password if (pathname === '/auth/admin/set-user-password' && req.method === 'POST') { await parseJSONBody(req) sendJSON(res, 200, { success: true }) return } - // Better Auth Admin: Revoke User Sessions - if ( - pathname === '/auth/admin/revoke-user-sessions' && - req.method === 'POST' - ) { + if (pathname === '/auth/admin/revoke-user-sessions' && req.method === 'POST') { await parseJSONBody(req) sendJSON(res, 200, { success: true }) return } - // Better Auth Admin: Ban / Unban User if (pathname === '/auth/admin/ban-user' && req.method === 'POST') { const body = await parseJSONBody(req) const target = state.users.find((u) => u.id === body.userId) @@ -515,6 +457,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, 200, { success: true }) return } + if (pathname === '/auth/admin/unban-user' && req.method === 'POST') { const body = await parseJSONBody(req) const target = state.users.find((u) => u.id === body.userId) @@ -530,127 +473,14 @@ const server = http.createServer(async (req, res) => { if (pathname === '/hardware' && req.method === 'GET') { sendJSON(res, 200, { observed_at: new Date().toISOString(), - machines: [ - { - name: 'mist-gpu-node-01', - address: '10.0.1.10', - ready: true, - schedulable: true, - cpu_allocatable: '32', - memory_allocatable: '128Gi', - }, - { - name: 'mist-tt-worker-01', - address: '10.0.1.11', - ready: true, - schedulable: true, - cpu_allocatable: '16', - memory_allocatable: '64Gi', - }, - ], - pools: [ - { - accelerator: 'nvidia', - node: 'mist-gpu-node-01', - unit: 'GPU', - total: 4, - allocated: state.jobs - .filter( - (j) => - j.accelerator === 'nvidia' && j.job_state === 'InProgress', - ) - .reduce((sum, j) => sum + (j.device_count || 1), 0), - available: Math.max( - 0, - 4 - - state.jobs - .filter( - (j) => - j.accelerator === 'nvidia' && - j.job_state === 'InProgress', - ) - .reduce((sum, j) => sum + (j.device_count || 1), 0), - ), - ready: true, - }, - { - accelerator: 'tenstorrent', - node: 'mist-tt-worker-01', - unit: 'board', - total: 4, - allocated: state.jobs - .filter( - (j) => - j.accelerator === 'tenstorrent' && - j.job_state === 'InProgress', - ) - .reduce((sum, j) => sum + (j.device_count || 1), 0), - available: Math.max( - 0, - 4 - - state.jobs - .filter( - (j) => - j.accelerator === 'tenstorrent' && - j.job_state === 'InProgress', - ) - .reduce((sum, j) => sum + (j.device_count || 1), 0), - ), - ready: true, - chips_per_device: 2, - }, - ], + machines: state.machines, + pools: state.pools, }) return } if (pathname === '/images' && req.method === 'GET') { - sendJSON(res, 200, { - self_service: true, - teams_enabled: true, - registries: ['docker.io', 'ghcr.io', 'quay.io', 'localhost:5000'], - images: [ - { - reference: 'docker.io/library/python:3.11-slim', - accelerators: ['cpu'], - }, - { - reference: 'docker.io/library/pytorch-cpu:2.2.0', - accelerators: ['cpu'], - }, - { - reference: 'ghcr.io/utmist/pytorch-cuda:latest', - accelerators: ['nvidia'], - }, - { - reference: 'ghcr.io/utmist/tt-metal-runtime:latest', - accelerators: ['tenstorrent'], - }, - ], - profiles: [ - { - accelerator: 'cpu', - default_image: 'docker.io/library/python:3.11-slim', - device_unit: 'core', - max_devices: 16, - note: 'Standard CPU runtime', - }, - { - accelerator: 'nvidia', - default_image: 'ghcr.io/utmist/pytorch-cuda:latest', - device_unit: 'GPU', - max_devices: 4, - note: 'NVIDIA CUDA 12.4 + PyTorch runtime', - }, - { - accelerator: 'tenstorrent', - default_image: 'ghcr.io/utmist/tt-metal-runtime:latest', - device_unit: 'board', - max_devices: 4, - note: 'Tenstorrent Wormhole n300 PCIe runtime', - }, - ], - }) + sendJSON(res, 200, state.images) return } @@ -877,16 +707,7 @@ const server = http.createServer(async (req, res) => { // ---------------------------------------------------- if (pathname === '/storage' && req.method === 'GET') { - sendJSON(res, 200, { - enabled: true, - capacity_bytes: 1099511627776, // 1 TiB - available_bytes: 879609302220, // 800 GiB - model_capacity_bytes: 2199023255552, // 2 TiB - model_available_bytes: 1759218604441, // 1.6 TiB - dataset_allocation_gib: 500, - model_allocation_gib: 1000, - upload_limit_bytes: 5368709120, // 5 GiB - }) + sendJSON(res, 200, state.storage) return } @@ -951,12 +772,11 @@ const server = http.createServer(async (req, res) => { } sendJSON(res, 200, { datasets: filtered, - upload_limit_bytes: 5368709120, + upload_limit_bytes: state.storage.upload_limit_bytes || 5368709120, }) return } - // Dataset upload (streaming / raw binary or multipart) if (pathname === '/datasets' && req.method === 'POST') { const filename = query.filename || 'dataset.bin' const name = query.name || filename @@ -1029,13 +849,11 @@ const server = http.createServer(async (req, res) => { return } - // Health check if (pathname === '/healthz' && req.method === 'GET') { sendJSON(res, 200, { status: 'ok', executor: 'fake-backend' }) return } - // 404 Fallback sendError(res, 404, `Endpoint not found: ${req.method} ${pathname}`) } catch (err) { console.error('[Fake Backend Error]', err) @@ -1046,10 +864,12 @@ const server = http.createServer(async (req, res) => { server.listen(PORT, HOST, () => { console.log(`======================================================`) console.log(`Mist Fake Backend running at http://${HOST}:${PORT}`) - console.log(`Ready for web-interface testing!`) - console.log(`- Simulated User: ${state.users[state.currentUserIndex]?.name} (${state.users[state.currentUserIndex]?.role})`) - console.log(`- Teams: ${state.teams.map((t) => t.name).join(', ')}`) - console.log(`- Jobs: ${state.jobs.length} pre-populated`) + console.log(`Active Scenario: '${state.activeScenario}'`) + console.log(`Simulated User: ${state.users[state.currentUserIndex]?.name} (${state.users[state.currentUserIndex]?.role})`) + console.log(`Machines: ${state.machines.map((m) => m.name).join(', ')}`) + console.log(`Teams: ${state.teams.map((t) => t.name).join(', ')}`) + console.log(`Jobs: ${state.jobs.length} in history`) + console.log(`Run 'node scripts/configure-fake-backend.mjs' to modify`) console.log(`======================================================`) }) diff --git a/web-interface/package.json b/web-interface/package.json index 5200d90..0783ac3 100644 --- a/web-interface/package.json +++ b/web-interface/package.json @@ -15,6 +15,7 @@ "test": "vitest run", "lint": "eslint", "fake-backend": "node ../scripts/fake-backend.mjs", + "configure-fake-backend": "node ../scripts/configure-fake-backend.mjs", "format": "prettier --check .", "check": "prettier --write . && eslint --fix" }, From 6f22c8de1172f0932a7f3c53dab4fda124c36bce Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 22:51:10 -0400 Subject: [PATCH 03/10] feat: favicons --- web-interface/public/favicon.ico | Bin 3870 -> 152126 bytes web-interface/public/logo192.png | Bin 5347 -> 13841 bytes web-interface/public/logo512.png | Bin 9664 -> 41025 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/web-interface/public/favicon.ico b/web-interface/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..bd4829290a12df81098f85c3a9027defd44ee06b 100644 GIT binary patch literal 152126 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 literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/web-interface/public/logo192.png b/web-interface/public/logo192.png index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..51ffbf36b4f75a4b32b3f2e619396a3f2a6430c4 100644 GIT binary patch literal 13841 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 F{{iqwvS|PS literal 5347 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

GlY;rU&kOIi~nsbIDAvX(qxtWO8t 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN From 5d3e0702eba961cb30e2ceb46bef5494439f13f4 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 22:54:11 -0400 Subject: [PATCH 04/10] fix(fake-backend): resolve config file relative to project root --- scripts/configure-fake-backend.mjs | 4 +++- scripts/fake-backend.mjs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/configure-fake-backend.mjs b/scripts/configure-fake-backend.mjs index f5a8027..4796596 100644 --- a/scripts/configure-fake-backend.mjs +++ b/scripts/configure-fake-backend.mjs @@ -18,13 +18,15 @@ import path from 'node:path' import http from 'node:http' import readline from 'node:readline/promises' import { stdin as input, stdout as output } from 'node:process' +import { fileURLToPath } from 'node:url' import { getScenario, SCENARIO_DESCRIPTIONS, SCENARIO_FACTORIES, } from './fake-backend-scenarios.mjs' -const CONFIG_FILE = path.resolve(process.cwd(), '.fake-backend-config.json') +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const CONFIG_FILE = path.resolve(ROOT_DIR, '.fake-backend-config.json') // Parse CLI flags const argv = process.argv.slice(2) diff --git a/scripts/fake-backend.mjs b/scripts/fake-backend.mjs index 00d33f5..d267623 100644 --- a/scripts/fake-backend.mjs +++ b/scripts/fake-backend.mjs @@ -17,7 +17,7 @@ import http from 'node:http' import fs from 'node:fs' import path from 'node:path' -import { URL } from 'node:url' +import { URL, fileURLToPath } from 'node:url' import crypto from 'node:crypto' import { getScenario, @@ -36,7 +36,8 @@ const PORT = Number( getArg('--port') || process.env.PORT || process.env.MIST_FAKE_PORT || 3000, ) const HOST = process.env.HOST || '127.0.0.1' -const DEFAULT_CONFIG_FILE = path.resolve(process.cwd(), '.fake-backend-config.json') +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const DEFAULT_CONFIG_FILE = path.resolve(ROOT_DIR, '.fake-backend-config.json') // --- Initial State Management --- From 573a32b2400dfcab2909fd3901ad97346deae9d2 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 22:59:03 -0400 Subject: [PATCH 05/10] feat: svg for favicon --- web-interface/public/favicon.svg | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 web-interface/public/favicon.svg diff --git a/web-interface/public/favicon.svg b/web-interface/public/favicon.svg new file mode 100644 index 0000000..a526ef2 --- /dev/null +++ b/web-interface/public/favicon.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M + + From 3ab09cc17e6b09823a6d1817c0cc8fb5bf0491d0 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Fri, 16 Oct 2026 23:04:21 -0400 Subject: [PATCH 06/10] feat: readme --- README.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9d0f2f8..468d0e6 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,17 @@ UTMIST's research compute platform. Submit Python scripts or tagged container images, choose CPU, NVIDIA GPUs or Tenstorrent boards, upload datasets, and collect persistent results through the same website or CLI. -**Private website:** http://100.73.139.66:8088, accessible through authorized +**Private website:** [http://100.73.139.66:8088](http://100.73.139.66:8088), accessible through authorized Tailscale connectivity. A Mist account and team membership are also required. ## Current installation -| Machine | Role | LAN address | Tailscale address | -|---|---|---|---| -| `utmist-z1opa08` | k3s server, API/auth, website, CPU and two RTX A4000 GPUs | `10.0.0.175` | `100.73.139.66` | -| `utmist-tt` | k3s worker, four n300 boards/eight chips, shared storage | `10.0.0.112` | `100.95.175.37` | + +| Machine | Role | LAN address | Tailscale address | +| ---------------- | --------------------------------------------------------- | ------------ | ----------------- | +| `utmist-z1opa08` | k3s server, API/auth, website, CPU and two RTX A4000 GPUs | `10.0.0.175` | `100.73.139.66` | +| `utmist-tt` | k3s worker, four n300 boards/eight chips, shared storage | `10.0.0.112` | `100.95.175.37` | + The cluster and NFS use the existing LAN. Tailscale provides private portal and administrator SSH access. Kubernetes/containerd run workload containers. @@ -60,3 +62,21 @@ chaining, browser image archives/builds, private pull-credential management and distributed multi-machine training are outside this release. Automatic backups, HTTPS and monitoring/cleanup products were explicitly excluded. This is a private HTTP installation with one control plane, one auth instance and one storage host. + +## Frontend Testing + +how to use fake backend for testing frontend. + +(temporary, functionality, to be removed in future commit.) + +```bash +# Terminal 1 +npm --prefix web-interface run dev -- --host 127.0.0.1 --port 3001 --strictPort + +# Terminal 2 +npm --prefix web-interface run fake-backend +``` + + + +  \ No newline at end of file From 474c787605b5a92545e143dbde920479d5a465db Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Tue, 20 Oct 2026 09:45:49 -0400 Subject: [PATCH 07/10] docs: agents.md and instructions --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 +++ AGENTS.md | 9 +++++++++ README.md | 6 ++++++ 4 files changed, 18 insertions(+) delete mode 100644 .DS_Store create mode 100644 AGENTS.md diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 6df9d253a2aae9e0f803c35c0ecf0c9a0108d468..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK-AcnS6i(c98AIrW!Y%{e4&09nhc~6p7qFriDzl|Si?tbR=Pt&e*ZM*}iO=IX zNeT{oE#l6BM`ap#soA(j!K1~yE3$6lMy+NkCjK!D(;Zk*iU^+mR=O z;dRP7Z8q*|%82J1BNFul_tuRQd>>GP}$oBnF59Vt^RfZU*eR zAX?i^1FfDIAO6T?p*=uD!Mp+$5YRU+0bt-h($+x}m#9OYYq2nhqo7}x Q1JXr66G9y^@Cyul0r>VwYybcN diff --git a/.gitignore b/.gitignore index f456dba..f130b04 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__/ data/ .direnv/ .fake-backend-config.json + +# OS files +.DS_Store \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6baa25e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +This is a project designing the compute platform for UTMIST (University of Toronto Machine Intelligence Student Team). The target cluster is two computers, one with tenstorrent gpu and one with nvidia gpus. + +## Documentation + +Agents are allowed to edit the README and top level markdown files as well as docs/agents, but shouldn't touch docs/humans as those are by humans for humans (though agents may notify humans if they believe human docs are severely outdated). + +## Temporary Decisions + +Make the frontend lightweight and move logic to backend. We aren't short on bandwidth, so it is fine to send a new qpi request and refresh UI every once in a while. \ No newline at end of file diff --git a/README.md b/README.md index 468d0e6..ad6502a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ distributed multi-machine training are outside this release. Automatic backups, HTTPS and monitoring/cleanup products were explicitly excluded. This is a private HTTP installation with one control plane, one auth instance and one storage host. +### Documentation + +Humans are responsible for editing the human facing docs. This is because the human facing docs will be read by humans, and it is rude to give them untested instructions to follow. + +Documentation should include explanations of design decisions for later platform maintainers. + ## Frontend Testing how to use fake backend for testing frontend. From 73f73053dbc2df177e3812adbdfee7ee17da6505 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Tue, 20 Oct 2026 11:05:29 -0400 Subject: [PATCH 08/10] feat: better backend unreachable page; hugo hextra docs --- .gitignore | 1 + README.md | 16 ++-- cli/docs/setup.md | 2 +- deploy/k3s/README.md | 6 +- .../evidence/department-2026-10-09/README.md | 2 +- deploy/k3s/setup-runbook.md | 6 +- deploy/private/README.md | 8 +- docs/README.md | 71 ++++++++------- docs/{ => content}/architecture.md | 5 ++ .../README.md => content/archive/_index.md} | 7 +- .../archive/complete-foundation.md | 6 +- .../archive/foundation-execution-plan.md | 2 +- .../archive/k3s-deployment-history.md | 22 ++--- .../archive/k3s-setup-history.md | 6 +- .../{ => content}/archive/kubernetes-pilot.md | 6 +- .../archive/local-job-foundation.md | 6 +- .../archive/shared-storage-deployment-plan.md | 2 +- docs/{ => content}/complete-foundation.md | 7 +- .../department-rollout-execution.md | 9 +- docs/{ => content}/department-rollout.md | 15 ++-- docs/{ => content}/design-system.md | 5 ++ docs/{ => content}/development.md | 7 +- .../foundation-execution-plan.md | 7 +- docs/{ => content}/frontend-architecture.md | 7 +- docs/{ => content}/k3s-setup.md | 11 ++- docs/{ => content}/kubernetes-pilot.md | 7 +- docs/{ => content}/local-job-foundation.md | 7 +- docs/{ => content}/repository-cleanup.md | 11 ++- .../shared-storage-deployment-plan.md | 7 +- docs/{ => content}/storage.md | 7 +- docs/{ => content}/tailscale-ssh.md | 7 +- docs/{ => content}/testing.md | 9 +- pre_compute_platform.md | 6 +- scripts/check-docs.py | 2 +- src/jobs.md | 4 +- web-interface/.prettierignore | 6 +- web-interface/README.md | 8 +- web-interface/eslint.config.js | 2 +- web-interface/package.json | 1 + web-interface/src/auth.tsx | 10 ++- .../src/components/JobsPage.test.tsx | 4 +- web-interface/src/components/Navbar.tsx | 15 +++- .../src/components/jobs/JobSubmissionForm.tsx | 2 +- web-interface/src/format.ts | 7 +- web-interface/src/routes/__root.tsx | 7 ++ web-interface/vite.config.ts | 86 ++++++++++++++++++- 46 files changed, 328 insertions(+), 129 deletions(-) rename docs/{ => content}/architecture.md (98%) rename docs/{archive/README.md => content/archive/_index.md} (91%) rename docs/{ => content}/archive/complete-foundation.md (98%) rename docs/{ => content}/archive/foundation-execution-plan.md (99%) rename docs/{ => content}/archive/k3s-deployment-history.md (95%) rename docs/{ => content}/archive/k3s-setup-history.md (98%) rename docs/{ => content}/archive/kubernetes-pilot.md (97%) rename docs/{ => content}/archive/local-job-foundation.md (98%) rename docs/{ => content}/archive/shared-storage-deployment-plan.md (99%) rename docs/{ => content}/complete-foundation.md (66%) rename docs/{ => content}/department-rollout-execution.md (97%) rename docs/{ => content}/department-rollout.md (97%) rename docs/{ => content}/design-system.md (98%) rename docs/{ => content}/development.md (95%) rename docs/{ => content}/foundation-execution-plan.md (66%) rename docs/{ => content}/frontend-architecture.md (97%) rename docs/{ => content}/k3s-setup.md (96%) rename docs/{ => content}/kubernetes-pilot.md (67%) rename docs/{ => content}/local-job-foundation.md (66%) rename docs/{ => content}/repository-cleanup.md (93%) rename docs/{ => content}/shared-storage-deployment-plan.md (67%) rename docs/{ => content}/storage.md (97%) rename docs/{ => content}/tailscale-ssh.md (97%) rename docs/{ => content}/testing.md (94%) diff --git a/.gitignore b/.gitignore index f130b04..ed675b2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ data/ .direnv/ .fake-backend-config.json +.hugo_build.lock # OS files .DS_Store \ No newline at end of file diff --git a/README.md b/README.md index ad6502a..b34c67a 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,17 @@ administrator SSH access. Kubernetes/containerd run workload containers. Start with the [documentation index](docs/README.md). -- [Using the portal, teams and jobs](docs/department-rollout.md) -- [k3s setup, accelerator allocation and diagnostics](docs/k3s-setup.md) -- [Tailscale and SSH setup](docs/tailscale-ssh.md) -- [Architecture and code organization](docs/architecture.md) -- [Storage and upload limits](docs/storage.md) +- [Using the portal, teams and jobs](docs/content/department-rollout.md) +- [k3s setup, accelerator allocation and diagnostics](docs/content/k3s-setup.md) +- [Tailscale and SSH setup](docs/content/tailscale-ssh.md) +- [Architecture and code organization](docs/content/architecture.md) +- [Storage and upload limits](docs/content/storage.md) - [Deployment and operations](deploy/private/README.md) -- [Developer setup](docs/development.md), [CLI](cli/docs/setup.md), [testing](docs/testing.md) +- [Developer setup](docs/content/development.md), [CLI](cli/docs/setup.md), [testing](docs/content/testing.md) Main contains the department release merged through [PR #110](https://github.com/UTMIST/Mist/pull/110). The earlier main and operating -snapshots are preserved in the [historical archive](docs/archive/README.md). +snapshots are preserved in the [historical archive](docs/content/archive/_index.md). ## Development and checks @@ -49,7 +49,7 @@ npm --prefix web-interface run build ``` For a local UI against the authenticated deployed API, follow -[developer setup](docs/development.md). For the existing installation, deploy with +[developer setup](docs/content/development.md). For the existing installation, deploy with `bash deploy/private/build-and-deploy.sh`; read its [prerequisites](deploy/private/README.md) before running it. Fresh-host provisioning is covered separately by the k3s guide. diff --git a/cli/docs/setup.md b/cli/docs/setup.md index 2377fda..789a050 100644 --- a/cli/docs/setup.md +++ b/cli/docs/setup.md @@ -65,4 +65,4 @@ To cancel that job while waiting/running, use `bin/mist-cli job cancel "$JOB_ID" read historical jobs; new work requires a team. `--scope common` requires common write permission. Custom tagged public images are allowed by team registry policy; TT custom images use `--tt-runtime container`. See the -[department guide](../../docs/department-rollout.md) for limits and storage. +[department guide](../../docs/content/department-rollout.md) for limits and storage. diff --git a/deploy/k3s/README.md b/deploy/k3s/README.md index cb60407..55ceeb1 100644 --- a/deploy/k3s/README.md +++ b/deploy/k3s/README.md @@ -1,7 +1,7 @@ # Kubernetes deployment assets Current installation: two k3s nodes, authenticated team jobs, scoped NFS storage -and private Tailscale portal. Start with the [k3s setup/handoff](../../docs/k3s-setup.md) +and private Tailscale portal. Start with the [k3s setup/handoff](../../docs/content/k3s-setup.md) and [deployment operations](../private/README.md). ## Manifest and tooling map @@ -29,6 +29,6 @@ The application image build uses [Dockerfile.api](Dockerfile.api). API/auth imag are imported into main's k3s containerd, not scheduled by a Docker supervisor. Use `bash deploy/private/build-and-deploy.sh` for the existing installation. -[Old deployment history](../../docs/archive/k3s-deployment-history.md) and -[original node setup](../../docs/archive/k3s-setup-history.md) preserve earlier +[Old deployment history](../../docs/content/archive/k3s-deployment-history.md) and +[original node setup](../../docs/content/archive/k3s-setup-history.md) preserve earlier observations; their unauthenticated/node-local instructions are superseded. diff --git a/deploy/k3s/evidence/department-2026-10-09/README.md b/deploy/k3s/evidence/department-2026-10-09/README.md index 9766550..a9596b2 100644 --- a/deploy/k3s/evidence/department-2026-10-09/README.md +++ b/deploy/k3s/evidence/department-2026-10-09/README.md @@ -2,7 +2,7 @@ Evidence from the actual two-node private pilot before repository cleanup. The original Go/Docker/logger counts below belong to that earlier release; -[current cleanup verification](../../../../docs/repository-cleanup.md) records +[current cleanup verification](../../../../docs/content/repository-cleanup.md) records the maintained suites after their obsolete implementations were removed. Reports contain synthetic verification data and job/device IDs; no passwords, cookies or auth secrets. diff --git a/deploy/k3s/setup-runbook.md b/deploy/k3s/setup-runbook.md index 5540d11..643eb3a 100644 --- a/deploy/k3s/setup-runbook.md +++ b/deploy/k3s/setup-runbook.md @@ -1,9 +1,9 @@ # Current k3s setup runbook -The maintained two-node runbook is [k3s setup and accelerator handoff](../../docs/k3s-setup.md). +The maintained two-node runbook is [k3s setup and accelerator handoff](../../docs/content/k3s-setup.md). It covers current versions, LAN/Tailscale roles, fresh server/worker boundaries, NVIDIA/TT operators, diagnostics and troubleshooting. -- [Tailscale and administrator SSH](../../docs/tailscale-ssh.md) +- [Tailscale and administrator SSH](../../docs/content/tailscale-ssh.md) - [Application deployment and service operations](../private/README.md) -- [Original single-node installation and worker-join history](../../docs/archive/k3s-setup-history.md) +- [Original single-node installation and worker-join history](../../docs/content/archive/k3s-setup-history.md) diff --git a/deploy/private/README.md b/deploy/private/README.md index 4d9ff64..5a8fe9e 100644 --- a/deploy/private/README.md +++ b/deploy/private/README.md @@ -1,9 +1,9 @@ # Private deployment operations -Current user/admin and architecture guide: [department rollout](../../docs/department-rollout.md). -Execution/evidence: [rollout checklist](../../docs/department-rollout-execution.md). -Setup: [k3s](../../docs/k3s-setup.md), [Tailscale/SSH](../../docs/tailscale-ssh.md), -[storage](../../docs/storage.md), and [developer setup](../../docs/development.md). +Current user/admin and architecture guide: [department rollout](../../docs/content/department-rollout.md). +Execution/evidence: [rollout checklist](../../docs/content/department-rollout-execution.md). +Setup: [k3s](../../docs/content/k3s-setup.md), [Tailscale/SSH](../../docs/content/tailscale-ssh.md), +[storage](../../docs/content/storage.md), and [developer setup](../../docs/content/development.md). ## Services and persistent state diff --git a/docs/README.md b/docs/README.md index ce6536a..2cad2d2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,43 +1,40 @@ -# Documentation +# Mist Documentation -Current main-branch documentation, verified against the two running machines on -**October 9, 2026**. Addresses and versions describe this installation; adjust -host-specific settings when provisioning another environment. +This directory contains the documentation site for Mist, powered by **Hugo** and the **Hextra** documentation theme. -## Find the right guide +## Viewing Documentation Locally -| Task | Guide | -|---|---| -| Sign in, manage teams, upload data, submit/download jobs | [Department operating guide](department-rollout.md) | -| Understand services and source layout | [Architecture](architecture.md) | -| Install/inspect k3s, join a worker, verify accelerator allocation | [k3s setup](k3s-setup.md) | -| Connect privately and administer either host | [Tailscale and SSH](tailscale-ssh.md) | -| Understand dataset/model pools and physical capacity | [Storage](storage.md) | -| Build, deploy, restart or troubleshoot services | [Deployment operations](../deploy/private/README.md) | -| Develop locally with the existing authenticated API | [Developer setup](development.md) | -| Authenticate and submit from a terminal | [CLI](../cli/docs/setup.md) | -| Run automated checks and read recorded live evidence | [Testing](testing.md) | -| Maintain UI components and interactions | [Design system](design-system.md) | -| Understand frontend architecture and backend offloading | [Frontend architecture](frontend-architecture.md) | -| Read completed scope, exclusions and release verification | [Execution record](department-rollout-execution.md) | -| See what repository cleanup removed and verified | [Cleanup record](repository-cleanup.md) | - -## Access and execution - -```text -Browser / CLI → Tailscale portal :8088 → Nginx → Go API → k3s - | | - Better Auth + team policy - | - CPU/NVIDIA or Tenstorrent Job - | - scoped dataset/model NFS mounts on QuietBox +To preview the documentation locally with live-reload: + +```bash +cd docs +hugo server +``` + +The site will be available at `http://localhost:1313/docs/`. + +To build the static site (outputting to `web-interface/public/docs` for the web portal): + +```bash +cd docs +hugo ``` -Mist login is separate from Tailscale membership and Linux SSH login. Researchers -use the portal; administrator SSH is restricted by the installed host rules. -The worker reaches the control plane and storage through LAN addresses. +## Documentation Guides -[Earlier plans and single-owner guides](archive/README.md) are historical only. -Current guidance does not require Redis, an SSH session to launch a job, a public -cloud load balancer, or permission to approve every tagged image. +Documentation source files are located in `content/`: + +| Guide | Description | +|---|---| +| [Department operating guide](content/department-rollout.md) | Sign in, manage teams, upload data, submit/download jobs | +| [Architecture](content/architecture.md) | Service topologies, boundaries, and codebase layout | +| [k3s setup](content/k3s-setup.md) | Cluster setup, worker nodes, and hardware acceleration | +| [Tailscale and SSH](content/tailscale-ssh.md) | Remote secure networking and administrator SSH | +| [Storage](content/storage.md) | Dataset and model shared storage pools | +| [Developer setup](content/development.md) | Local frontend and backend development | +| [Testing](content/testing.md) | Automated tests and evidence runs | +| [Design system](content/design-system.md) | Portal design principles and components | +| [Frontend architecture](content/frontend-architecture.md) | UI layout and backend offloading plan | +| [Execution record](content/department-rollout-execution.md) | Department rollout status and validation | +| [Cleanup record](content/repository-cleanup.md) | Repository maintenance history | +| [Historical archive](content/archive/_index.md) | Archived previous-generation guides and plans | diff --git a/docs/architecture.md b/docs/content/architecture.md similarity index 98% rename from docs/architecture.md rename to docs/content/architecture.md index 7ce1f2d..8a3a755 100644 --- a/docs/architecture.md +++ b/docs/content/architecture.md @@ -1,3 +1,8 @@ +--- +title: "Architecture & Services" +weight: 20 +--- + # Architecture Current private department installation, verified October 9, 2026. diff --git a/docs/archive/README.md b/docs/content/archive/_index.md similarity index 91% rename from docs/archive/README.md rename to docs/content/archive/_index.md index 8f98a75..af96a83 100644 --- a/docs/archive/README.md +++ b/docs/content/archive/_index.md @@ -1,3 +1,8 @@ +--- +title: "Historical Records" +weight: 200 +--- + # Historical records These files preserve earlier design decisions and verification phases. They are @@ -18,4 +23,4 @@ The old main is preserved on [`archive/main-before-k3s-20261009`](https://github.com/UTMIST/Mist/tree/archive/main-before-k3s-20261009). The pre-cleanup department release is commit [`3431ffd`](https://github.com/UTMIST/Mist/tree/3431ffdb713fac72881fc7f61f2750a82fe3f916). -Use the [current documentation](../README.md) for this installation. +Use the [current documentation](../_index.md) for this installation. diff --git a/docs/archive/complete-foundation.md b/docs/content/archive/complete-foundation.md similarity index 98% rename from docs/archive/complete-foundation.md rename to docs/content/archive/complete-foundation.md index f1d8875..fb0aa37 100644 --- a/docs/archive/complete-foundation.md +++ b/docs/content/archive/complete-foundation.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). > Current department storage and portal settings: [department rollout](../department-rollout.md). @@ -11,7 +11,7 @@ > **Historical account-only snapshot.** The [department rollout](../department-rollout.md) > supersedes these instructions: team membership, scoped folders, enforced quotas, > self-service images and the new portal are implemented. Use that guide and -> [current deployment operations](../../deploy/private/README.md) for current behavior. +> [current deployment operations](../../../deploy/private/README.md) for current behavior. > The remaining sections record the preceding foundation, including its limitations. Implemented on `feat/local-job-foundation`, October 8–9, 2026. This continues the @@ -196,7 +196,7 @@ inside `/outputs` if you want them to be listed/downloaded. ## Deployment, restart and rollback -See [deployment scripts](../../deploy/private/README.md). k3s server/agent, NFS and +See [deployment scripts](../../../deploy/private/README.md). k3s server/agent, NFS and Nginx are system services. Kubernetes restores API/auth deployments. The frontend is a static release, with `/srv/mist-web/current` pointing at the active build. Deployment configuration, RBAC, claim templates and image policies are checked diff --git a/docs/archive/foundation-execution-plan.md b/docs/content/archive/foundation-execution-plan.md similarity index 99% rename from docs/archive/foundation-execution-plan.md rename to docs/content/archive/foundation-execution-plan.md index aeceef5..48eef71 100644 --- a/docs/archive/foundation-execution-plan.md +++ b/docs/content/archive/foundation-execution-plan.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Mist foundation execution plan diff --git a/docs/archive/k3s-deployment-history.md b/docs/content/archive/k3s-deployment-history.md similarity index 95% rename from docs/archive/k3s-deployment-history.md rename to docs/content/archive/k3s-deployment-history.md index 4256a27..9f6cd55 100644 --- a/docs/archive/k3s-deployment-history.md +++ b/docs/content/archive/k3s-deployment-history.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Accelerator jobs on k3s @@ -16,7 +16,7 @@ join on 2026-10-03. The NVIDIA server runs Ubuntu 22.04, k3s 1.36, and two RTX A4000 GPUs. The host driver is pre-installed. NVIDIA GPU Operator v26.7.1 is configured with -[`nvidia-gpu-operator-values.yaml`](../../deploy/k3s/nvidia-gpu-operator-values.yaml). Pin the +[`nvidia-gpu-operator-values.yaml`](../../../deploy/k3s/nvidia-gpu-operator-values.yaml). Pin the chart version when installing or upgrading: ```bash @@ -64,7 +64,7 @@ Wormhole chips. The NVIDIA smoke Job remains specific to `utmist-z1opa08`. TT-Operator **0.3.0** installs Fabric Manager **0.2.30** and the DRA driver **0.0.59**, configured by -[`tenstorrent-operator-values.yaml`](../../deploy/k3s/tenstorrent-operator-values.yaml). +[`tenstorrent-operator-values.yaml`](../../../deploy/k3s/tenstorrent-operator-values.yaml). The existing host KMD/firmware and the existing NFD installation are retained; driver management, duplicate NFD, telemetry, JobSet, and PMIx are disabled. The manually verified `tenstorrent.com/has-tt=true` node label enables discovery. @@ -96,14 +96,14 @@ a ResourceClaimTemplate, and only its allocated device nodes are injected through CDI. Boards are released when the pod finishes, even while its completed Job and checkpoints are retained. -[`training-tenstorrent-job.yaml`](../../deploy/k3s/training-tenstorrent-job.yaml) now requests +[`training-tenstorrent-job.yaml`](../../../deploy/k3s/training-tenstorrent-job.yaml) now requests one board, runs without privileged access, and reserves 2 GiB of 1 GiB hugepages. The host still has 16 GiB of such hugepages. It mounts the existing TT-Metal checkout and Python environment read-only, so it is specific to this QuietBox rather than a portable training image. For uniquely named training checks and separate checkpoint directories, -use [`submit_tenstorrent_smoke.py`](../../deploy/k3s/submit_tenstorrent_smoke.py). It needs +use [`submit_tenstorrent_smoke.py`](../../../deploy/k3s/submit_tenstorrent_smoke.py). It needs Python PyYAML, available on the server. For example: ```bash @@ -161,7 +161,7 @@ the temporary PVC reader is deleted. The barrier times out after five minutes if interrupted. The checkpoint PVC is retained. Operator restart recovery and an NVIDIA CUDA regression check also passed. -See [`tenstorrent-allocation-results.json`](../../deploy/k3s/tenstorrent-allocation-results.json) +See [`tenstorrent-allocation-results.json`](../../../deploy/k3s/tenstorrent-allocation-results.json) for pinned image IDs and results. Logs, claim/queue snapshots, admission checks, and checkpoint archives are in `/home/utmist/k3s-allocation-results-2026-10-03` on the NVIDIA server. @@ -171,7 +171,7 @@ capacity for placement. Verify/correct it before adding memory-based selectors. ## Training smoke jobs -[`training_smoke.py`](../../deploy/k3s/training_smoke.py) learns a 32-input/32-output linear +[`training_smoke.py`](../../../deploy/k3s/training_smoke.py) learns a 32-input/32-output linear mapping from 128 synthetic samples and evaluates 64 held-out samples. Each device must complete 100 gradient updates, reduce training MSE by at least 98%, achieve held-out MSE below `0.0005`, change its weights, and reload its @@ -220,7 +220,7 @@ checkpoints. The archives were also checked after the Pods completed. Precision and implementations differ; these figures establish convergence and are not a performance comparison. Exact image IDs, the TT-Metal host commit, Job timestamps, artifact locations, and acceptance thresholds are in -[`training-smoke-results.json`](../../deploy/k3s/training-smoke-results.json). Local logs and +[`training-smoke-results.json`](../../../deploy/k3s/training-smoke-results.json). Local logs and checkpoint archives are retained under `/home/utmist/k3s-training-results-2026-10-03` on the NVIDIA server. The completed Kubernetes Jobs and their checkpoint PVCs are retained too. @@ -292,7 +292,7 @@ entrypoint/arguments, or choose **Small training check** for an accelerator. The page displays actual placement, queue messages, exit status and logs. Hardware availability is live; the Machines route uses the same inventory. Cancel stops a waiting/running workload and retains cancellation history. -CLI commands are in [the CLI guide](../../cli/docs/setup.md). +CLI commands are in [the CLI guide](../../../cli/docs/setup.md). The installed user units keep both local services running during the user's systemd session. Their paths target this server and need adjustment on another @@ -382,7 +382,7 @@ read by fresh containers on both nodes. Temporary read-only output probes are removed; test Jobs and result directories remain. The final markers are `FOUNDATION_BROWSER_PASSED` and `FOUNDATION_ALLOCATION_OUTPUTS_PASSED`. October 8 evidence is under `/home/utmist/mist-foundation-results-2026-10-08`. -See [the checked-in summary](../../deploy/k3s/foundation-results.json) for actual job IDs, +See [the checked-in summary](../../../deploy/k3s/foundation-results.json) for actual job IDs, deployed image ID, and completed checks. ### API acceptance test @@ -402,7 +402,7 @@ Successful output ends with `MIST_API_PASSED`. Jobs and checkpoint directories are retained. A barrier times out after five minutes if interrupted. The verification script uses the operator's administrative context to inspect claims/devices and release test barriers; the Mist API itself cannot exec. -Results from 2026-10-03 are in [mist-api-results.json](../../deploy/k3s/mist-api-results.json), +Results from 2026-10-03 are in [mist-api-results.json](../../../deploy/k3s/mist-api-results.json), with raw evidence and checkpoint archives under `/home/utmist/mist-api-results-2026-10-03`. These checks establish small model training and allocation, not large/distributed-model support or performance. diff --git a/docs/archive/k3s-setup-history.md b/docs/content/archive/k3s-setup-history.md similarity index 98% rename from docs/archive/k3s-setup-history.md rename to docs/content/archive/k3s-setup-history.md index 7b8b3af..d32e55d 100644 --- a/docs/archive/k3s-setup-history.md +++ b/docs/content/archive/k3s-setup-history.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Single-node k3s and NVIDIA GPU pilot: setup and handoff @@ -28,7 +28,7 @@ The NVIDIA GPU Operator added the Kubernetes integration: The operator was configured **not** to replace the existing host driver. Those values are in -[`nvidia-gpu-operator-values.yaml`](../../deploy/k3s/nvidia-gpu-operator-values.yaml). NVIDIA +[`nvidia-gpu-operator-values.yaml`](../../../deploy/k3s/nvidia-gpu-operator-values.yaml). NVIDIA states that its NRI plugin is not yet generally available; this is a pilot configuration. See the [GPU Operator installation guide](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/getting-started.html) and [CDI/NRI guide](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/cdi.html). @@ -125,7 +125,7 @@ healthy. ### 4. Execute CUDA through k3s -The tracked [`gpu-smoke-job.yaml`](../../deploy/k3s/gpu-smoke-job.yaml) requests and limits one +The tracked [`gpu-smoke-job.yaml`](../../../deploy/k3s/gpu-smoke-job.yaml) requests and limits one `nvidia.com/gpu` device. Create the namespace if it does not already exist: ```bash diff --git a/docs/archive/kubernetes-pilot.md b/docs/content/archive/kubernetes-pilot.md similarity index 97% rename from docs/archive/kubernetes-pilot.md rename to docs/content/archive/kubernetes-pilot.md index eab19dd..8e5230a 100644 --- a/docs/archive/kubernetes-pilot.md +++ b/docs/content/archive/kubernetes-pilot.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Kubernetes execution pilot @@ -38,7 +38,7 @@ flowchart LR ``` See [installation, commands and acceptance tests](k3s-deployment-history.md#mist-api-cli-and-jobs-page) -and [job lifecycle and API endpoints](../../src/jobs.md). +and [job lifecycle and API endpoints](../../../src/jobs.md). ## Hardware and scheduling @@ -88,7 +88,7 @@ its allocated board while device cgroups denied the other three, and that allocation recovered after restarting the DRA driver. CLI and actual browser checks exercise submission, status, logs and cancellation; backend/CLI/web tests and a frontend production build cover the application changes. -See [recorded results](../../deploy/k3s/mist-api-results.json) for evidence paths. +See [recorded results](../../../deploy/k3s/mist-api-results.json) for evidence paths. ## Access and retention diff --git a/docs/archive/local-job-foundation.md b/docs/content/archive/local-job-foundation.md similarity index 98% rename from docs/archive/local-job-foundation.md rename to docs/content/archive/local-job-foundation.md index 1e09e53..12e48d7 100644 --- a/docs/archive/local-job-foundation.md +++ b/docs/content/archive/local-job-foundation.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Local job foundation @@ -87,7 +87,7 @@ autograd compatibility. Cross-machine/distributed training is separate work. ## API contracts -The [job lifecycle reference](../../src/jobs.md) describes state and retention. +The [job lifecycle reference](../../../src/jobs.md) describes state and retention. The browser uses `/api`; Vite proxies it to the API. The API itself has these unprefixed routes: @@ -306,7 +306,7 @@ Their final markers are `FOUNDATION_BROWSER_PASSED` and Today's detailed evidence is saved under `/home/utmist/mist-foundation-results-2026-10-08`. The checked-in -[verification summary](../../deploy/k3s/foundation-results.json) records actual +[verification summary](../../../deploy/k3s/foundation-results.json) records actual job IDs, the deployed image ID, and completed checks. Browser and allocation/ persistence checks passed on October 8 EDT (October 9 UTC). A separate live check verified image-pull error reporting, deadline failure, and retained diff --git a/docs/archive/shared-storage-deployment-plan.md b/docs/content/archive/shared-storage-deployment-plan.md similarity index 99% rename from docs/archive/shared-storage-deployment-plan.md rename to docs/content/archive/shared-storage-deployment-plan.md index 2233be7..385621c 100644 --- a/docs/archive/shared-storage-deployment-plan.md +++ b/docs/content/archive/shared-storage-deployment-plan.md @@ -1,6 +1,6 @@ > **Historical record — superseded.** This text describes an earlier phase. > Commands and claims below are retained as history, not current setup instructions. -> Start with the [current documentation index](../README.md) and +> Start with the [current documentation index](../_index.md) and > [department operating guide](../department-rollout.md). # Complete foundation: parts 4 and 5 diff --git a/docs/complete-foundation.md b/docs/content/complete-foundation.md similarity index 66% rename from docs/complete-foundation.md rename to docs/content/complete-foundation.md index d2ad790..f8009cf 100644 --- a/docs/complete-foundation.md +++ b/docs/content/complete-foundation.md @@ -1,7 +1,12 @@ +--- +title: "Complete Foundation (Historical)" +weight: 301 +--- + # Historical guide This phase has been superseded by the department platform on main. -- [Current documentation](README.md) +- [Current documentation](_index.md) - [Current operating guide](department-rollout.md) - [Archived original record](archive/complete-foundation.md) diff --git a/docs/department-rollout-execution.md b/docs/content/department-rollout-execution.md similarity index 97% rename from docs/department-rollout-execution.md rename to docs/content/department-rollout-execution.md index 7e532b0..acfec62 100644 --- a/docs/department-rollout-execution.md +++ b/docs/content/department-rollout-execution.md @@ -1,3 +1,8 @@ +--- +title: "Rollout Execution Record" +weight: 100 +--- + # Department rollout execution Authorized October 9, 2026: implement all seven agreed remaining parts on @@ -62,7 +67,7 @@ behavior; they do not add a separate monitoring service. Recorded October 9 on the private production portal. Reports and selected logs are checked in under -[department evidence](../deploy/k3s/evidence/department-2026-10-09/README.md). +[department evidence](../../deploy/k3s/evidence/department-2026-10-09/README.md). - 14 API/cluster/storage checks: membership, write denial, scoped read-only grants, running-job revocation, FIFO/fair turns, API restart, cancellation, limits, @@ -170,5 +175,5 @@ requires an independent reviewer for subsequent pull requests. The live private portal already runs the verified implementation. Git publication does not reset accounts, jobs or files, or switch the live services again. -See [the final release record](../deploy/k3s/evidence/department-2026-10-09/release-validation.json) +See [the final release record](../../deploy/k3s/evidence/department-2026-10-09/release-validation.json) and [testing instructions](testing.md). diff --git a/docs/department-rollout.md b/docs/content/department-rollout.md similarity index 97% rename from docs/department-rollout.md rename to docs/content/department-rollout.md index d20899e..7d13333 100644 --- a/docs/department-rollout.md +++ b/docs/content/department-rollout.md @@ -1,9 +1,14 @@ +--- +title: "Department Operating Guide" +weight: 10 +--- + # Mist department rollout Merged into **main** through [PR #110](https://github.com/UTMIST/Mist/pull/110) on October 9, 2026. This is the current operating guide. Original accounts, jobs, datasets and checkpoint PVCs are preserved; earlier instructions are in the -[historical archive](archive/README.md). +[historical archive](archive/_index.md). ## Use the portal @@ -255,7 +260,7 @@ A regular-member UI demonstration submitted a custom image containing `/app/train.py` and `CMD ["python", "/app/train.py"]`. No script or command override was injected. Its two-layer PyTorch network trained for 200 steps on one RTX A4000 using `cuda:0`, reduced loss from 14.088 to 0.004936, saved weights/metrics and -verified checkpoint reload. [Container mode evidence](../deploy/k3s/evidence/department-2026-10-09/container-mode-result.json). +verified checkpoint reload. [Container mode evidence](../../deploy/k3s/evidence/department-2026-10-09/container-mode-result.json). The demonstration image was preloaded on the main node; it was not pushed to a public registry. Normal team workflows build/push to an allowed registry. @@ -366,7 +371,7 @@ errors for rejected requests. The main-node build command is `bash deploy/private/build-and-deploy.sh`. It preserves bootstrap/internal auth credentials, builds the API/CLI and imports API/auth images, applies policy/RBAC manifests, waits for rollouts, type-checks and builds the SPA, -and installs an atomic Nginx release. See [private operations](../deploy/private/README.md) +and installs an atomic Nginx release. See [private operations](../../deploy/private/README.md) for host services and recovery of deployment changes. API/auth image tags are `mist-api:departments-20261009` and `mist-auth:departments-20261009`. @@ -374,8 +379,8 @@ Build the CLI with `go -C cli build -o ../bin/mist-cli .`. New developer tasks should retain the identity → team policy → queued Job → scoped storage boundary. Never accept owner/roles from a submission or mount an entire store into a job. -Tests: [department verifier](../deploy/k3s/verify_department.mjs), -[browser verifier](../deploy/k3s/verify_department_browser.mjs), [large-dataset verifier](../deploy/k3s/verify_large_dataset.py), Go team tests, +Tests: [department verifier](../../deploy/k3s/verify_department.mjs), +[browser verifier](../../deploy/k3s/verify_department_browser.mjs), [large-dataset verifier](../../deploy/k3s/verify_large_dataset.py), Go team tests, CLI tests and React job/control tests. Checked-in evidence is linked from the execution plan. Protected verification state contains credentials and is never checked in. diff --git a/docs/design-system.md b/docs/content/design-system.md similarity index 98% rename from docs/design-system.md rename to docs/content/design-system.md index a09ee98..2885488 100644 --- a/docs/design-system.md +++ b/docs/content/design-system.md @@ -1,3 +1,8 @@ +--- +title: "Portal Design System" +weight: 80 +--- + # Portal design October 9, 2026. The visual direction takes inspiration from Cursor's restrained diff --git a/docs/development.md b/docs/content/development.md similarity index 95% rename from docs/development.md rename to docs/content/development.md index e92e469..83c0345 100644 --- a/docs/development.md +++ b/docs/content/development.md @@ -1,3 +1,8 @@ +--- +title: "Developer Setup" +weight: 60 +--- + # Developer setup Use Go **1.25.1** and Node.js **22.16.0 or newer**. Run repository commands from @@ -72,5 +77,5 @@ bash deploy/private/build-and-deploy.sh This requires existing administrative kubeconfig, Docker/root access, auth Secrets, operators and mounted storage. It is not a fresh-machine installer. -Read [operations](../deploy/private/README.md) and [testing](testing.md) first. +Read [operations](../../deploy/private/README.md) and [testing](testing.md) first. Current user/admin behavior is in [the operating guide](department-rollout.md). diff --git a/docs/foundation-execution-plan.md b/docs/content/foundation-execution-plan.md similarity index 66% rename from docs/foundation-execution-plan.md rename to docs/content/foundation-execution-plan.md index 130c7c5..53c606b 100644 --- a/docs/foundation-execution-plan.md +++ b/docs/content/foundation-execution-plan.md @@ -1,7 +1,12 @@ +--- +title: "Foundation Execution Plan (Historical)" +weight: 302 +--- + # Historical guide This phase has been superseded by the department platform on main. -- [Current documentation](README.md) +- [Current documentation](_index.md) - [Current operating guide](department-rollout.md) - [Archived original record](archive/foundation-execution-plan.md) diff --git a/docs/frontend-architecture.md b/docs/content/frontend-architecture.md similarity index 97% rename from docs/frontend-architecture.md rename to docs/content/frontend-architecture.md index f27ed6a..b1a81bf 100644 --- a/docs/frontend-architecture.md +++ b/docs/content/frontend-architecture.md @@ -1,6 +1,11 @@ +--- +title: "Frontend Architecture" +weight: 90 +--- + # Frontend Architecture & Backend Offloading -Documentation of the current Mist frontend ([`web-interface`](../web-interface)), its structural organization, and a roadmap of client-side processing to offload to the backend. +Documentation of the current Mist frontend ([`web-interface`](../../web-interface)), its structural organization, and a roadmap of client-side processing to offload to the backend. ## Architecture overview diff --git a/docs/k3s-setup.md b/docs/content/k3s-setup.md similarity index 96% rename from docs/k3s-setup.md rename to docs/content/k3s-setup.md index 2c728f0..fb80ea3 100644 --- a/docs/k3s-setup.md +++ b/docs/content/k3s-setup.md @@ -1,8 +1,13 @@ +--- +title: "k3s Cluster & Accelerators" +weight: 30 +--- + # k3s setup and accelerator handoff Verified on **October 9, 2026**. This guide describes the existing two-node installation, fresh-install boundaries and diagnostic commands. Routine -application deployment is in [operations](../deploy/private/README.md). +application deployment is in [operations](../../deploy/private/README.md). ## Installed cluster @@ -182,7 +187,7 @@ PyTorch autograd/model compatibility. Use **Jobs → New job → NVIDIA/Tenstorrent → Training check** after signing in and selecting your team. This exercises authentication, quotas, queue, allocator, -training and persistent output. Use the [CLI](../cli/docs/setup.md) from either +training and persistent output. Use the [CLI](../../cli/docs/setup.md) from either host to submit a real script through the same API. The direct manifest below is an administrator infrastructure diagnostic in the @@ -215,7 +220,7 @@ retain needed evidence before deleting only that completed diagnostic Job. - **Storage unavailable:** verify QuietBox's NFS/team-volume services and both pool mounts. Do not delete PVCs or overwrite mounted backing files. -Current live evidence is in [the verification record](../deploy/k3s/evidence/department-2026-10-09/README.md). +Current live evidence is in [the verification record](../../deploy/k3s/evidence/department-2026-10-09/README.md). No new reboot, operator upgrade or rejoin was performed for this documentation audit. References: [K3s quick start](https://docs.k3s.io/quick-start), diff --git a/docs/kubernetes-pilot.md b/docs/content/kubernetes-pilot.md similarity index 67% rename from docs/kubernetes-pilot.md rename to docs/content/kubernetes-pilot.md index b778a4e..a91ce37 100644 --- a/docs/kubernetes-pilot.md +++ b/docs/content/kubernetes-pilot.md @@ -1,7 +1,12 @@ +--- +title: "Kubernetes Pilot (Historical)" +weight: 303 +--- + # Historical guide This phase has been superseded by the department platform on main. -- [Current documentation](README.md) +- [Current documentation](_index.md) - [Current operating guide](department-rollout.md) - [Archived original record](archive/kubernetes-pilot.md) diff --git a/docs/local-job-foundation.md b/docs/content/local-job-foundation.md similarity index 66% rename from docs/local-job-foundation.md rename to docs/content/local-job-foundation.md index a188e01..5cf0eda 100644 --- a/docs/local-job-foundation.md +++ b/docs/content/local-job-foundation.md @@ -1,7 +1,12 @@ +--- +title: "Local Job Foundation (Historical)" +weight: 304 +--- + # Historical guide This phase has been superseded by the department platform on main. -- [Current documentation](README.md) +- [Current documentation](_index.md) - [Current operating guide](department-rollout.md) - [Archived original record](archive/local-job-foundation.md) diff --git a/docs/repository-cleanup.md b/docs/content/repository-cleanup.md similarity index 93% rename from docs/repository-cleanup.md rename to docs/content/repository-cleanup.md index bc619bd..332e596 100644 --- a/docs/repository-cleanup.md +++ b/docs/content/repository-cleanup.md @@ -1,3 +1,8 @@ +--- +title: "Repository Cleanup" +weight: 110 +--- + # Repository cleanup — October 9, 2026 Based on main commit `3431ffd`, the department release merged through PR #110. @@ -38,7 +43,7 @@ from deploying services. ## Verification -- [Sanitized validation report](../deploy/k3s/evidence/department-2026-10-09/cleanup-validation.json). +- [Sanitized validation report](../../deploy/k3s/evidence/department-2026-10-09/cleanup-validation.json). - Full current Go workspace tests with race detection: **29 API tests + 6 CLI tests**, no skipped tests; CLI entrypoint has no test files. Vet passes. - API and CLI static builds, CLI/root/job help and retired-executor rejection pass. @@ -60,7 +65,7 @@ from deploying services. The existing portal continues to run the verified department release. These source changes do not redeploy it. Previous live CUDA/TT/browser/isolation -[release evidence](../deploy/k3s/evidence/department-2026-10-09/README.md) remains +[release evidence](../../deploy/k3s/evidence/department-2026-10-09/README.md) remains valid for that release; it is not claimed as a fresh training/browser run of the cleanup candidate. No large storage tests, production restarts, account/job/file mutations or full-disk cleanup were performed. @@ -73,7 +78,7 @@ legacy workspace. No blanket prune, formatting or history rewrite is used. ## Reproduce current source checks See [testing](testing.md), [developer setup](development.md) and -[documentation index](README.md). Raw local validation output is kept outside Git +[documentation index](_index.md). Raw local validation output is kept outside Git under `/home/utmist/mist-cleanup-validation-20261009`; publish only sanitized summaries. Credentials, session state, kubeconfig and SSH private keys stay out of committed documentation/evidence. diff --git a/docs/shared-storage-deployment-plan.md b/docs/content/shared-storage-deployment-plan.md similarity index 67% rename from docs/shared-storage-deployment-plan.md rename to docs/content/shared-storage-deployment-plan.md index 86916d7..f5252e5 100644 --- a/docs/shared-storage-deployment-plan.md +++ b/docs/content/shared-storage-deployment-plan.md @@ -1,7 +1,12 @@ +--- +title: "Shared Storage Plan (Historical)" +weight: 305 +--- + # Historical guide This phase has been superseded by the department platform on main. -- [Current documentation](README.md) +- [Current documentation](_index.md) - [Current operating guide](department-rollout.md) - [Archived original record](archive/shared-storage-deployment-plan.md) diff --git a/docs/storage.md b/docs/content/storage.md similarity index 97% rename from docs/storage.md rename to docs/content/storage.md index a116de1..2672e8e 100644 --- a/docs/storage.md +++ b/docs/content/storage.md @@ -1,3 +1,8 @@ +--- +title: "Storage & Upload Limits" +weight: 50 +--- + # Storage and upload limits Current department storage on **QuietBox**, verified October 9, 2026. Both @@ -91,7 +96,7 @@ findmnt -t ext4,nfs,nfs4 With root access, inspect `exportfs -v`, per-team mounts and the storage-agent journal when readiness fails. Migration/expansion and service installation are -in [deployment operations](../deploy/private/README.md). Never truncate or unlink +in [deployment operations](../../deploy/private/README.md). Never truncate or unlink mounted image files, recreate populated filesystems or treat team removal as cleanup. Small-file migration, quota bounds and real accelerator input/output paths passed. diff --git a/docs/tailscale-ssh.md b/docs/content/tailscale-ssh.md similarity index 97% rename from docs/tailscale-ssh.md rename to docs/content/tailscale-ssh.md index 611a5cf..19bd196 100644 --- a/docs/tailscale-ssh.md +++ b/docs/content/tailscale-ssh.md @@ -1,3 +1,8 @@ +--- +title: "Tailscale & Admin SSH" +weight: 40 +--- + # Tailscale and SSH setup Verified against both hosts on **October 9, 2026**. Tailscale **1.102.4** is @@ -112,7 +117,7 @@ The installed `mist-portal-firewall.service` permits ordinary Tailscale sources to main's TCP **8088**, denying direct SSH/backend/cluster access. Infrastructure IPs and the recorded administrator device **100.99.194.52** retain broader access. The script contains matching IPv6 entries. Adding an administrator device needs -an explicit update to [the host allowlist](../deploy/private/portal-firewall.sh), +an explicit update to [the host allowlist](../../deploy/private/portal-firewall.sh), plus any applicable tailnet policy. LAN rules are separate. A plain SSH public key does not bypass this firewall. Access from an external diff --git a/docs/testing.md b/docs/content/testing.md similarity index 94% rename from docs/testing.md rename to docs/content/testing.md index 275c3c3..2060c3d 100644 --- a/docs/testing.md +++ b/docs/content/testing.md @@ -1,3 +1,8 @@ +--- +title: "Automated Testing" +weight: 70 +--- + # Testing Use the pinned Go/Node runtimes in [developer setup](development.md). The root @@ -38,7 +43,7 @@ Current automated suites do not require Redis or Docker. The old supervisor, Docker manager/logger tests and fake CLI-config tests were removed with their retired implementations. This is not a test skip: those products are no longer part of the application. The earlier full legacy suite results remain as dated -[pre-cleanup evidence](../deploy/k3s/evidence/department-2026-10-09/release-validation.json). +[pre-cleanup evidence](../../deploy/k3s/evidence/department-2026-10-09/release-validation.json). ## Real installation checks @@ -66,7 +71,7 @@ verifiers have been retired because they no longer reflect current auth/storage. ## Evidence and limits -The [October 9 evidence](../deploy/k3s/evidence/department-2026-10-09/README.md) +The [October 9 evidence](../../deploy/k3s/evidence/department-2026-10-09/README.md) records real CUDA/TT training, separate board/GPU reservations, input/output mounts, checkpoint reload/download, grants/revocation, queue restart and browser flows. The regular-member Container demonstration preserved its own image CMD; diff --git a/pre_compute_platform.md b/pre_compute_platform.md index a23531c..6f67bdb 100644 --- a/pre_compute_platform.md +++ b/pre_compute_platform.md @@ -3,9 +3,9 @@ The earlier GCP jump-host, shared-password and manual reservation guide is superseded by the current private Mist installation. -- [Tailscale and SSH setup](docs/tailscale-ssh.md) -- [Research portal and team job submission](docs/department-rollout.md) -- [k3s cluster setup](docs/k3s-setup.md) +- [Tailscale and SSH setup](docs/content/tailscale-ssh.md) +- [Research portal and team job submission](docs/content/department-rollout.md) +- [k3s cluster setup](docs/content/k3s-setup.md) Passwords are not published in current documentation. The previous password remains in Git history; retire it if still valid. Researchers use Mist accounts diff --git a/scripts/check-docs.py b/scripts/check-docs.py index 531acb9..fcc3090 100644 --- a/scripts/check-docs.py +++ b/scripts/check-docs.py @@ -46,7 +46,7 @@ def slugs(path): fence = line[3:].strip() block = [] else: - if fence in ("bash", "sh") and "docs/archive/" not in name: + if fence in ("bash", "sh") and "/archive/" not in name: check = subprocess.run(["bash", "-n"], input="\n".join(block), text=True, capture_output=True) shells += 1 if check.returncode: diff --git a/src/jobs.md b/src/jobs.md index 8ad28da..df373c2 100644 --- a/src/jobs.md +++ b/src/jobs.md @@ -1,7 +1,7 @@ # Job lifecycle and API -Current authenticated team execution. See [the operating guide](../docs/department-rollout.md#developer-api-contract) -for the complete API and [architecture](../docs/architecture.md) for source layout. +Current authenticated team execution. See [the operating guide](../docs/content/department-rollout.md#developer-api-contract) +for the complete API and [architecture](../docs/content/architecture.md) for source layout. ## Lifecycle diff --git a/web-interface/.prettierignore b/web-interface/.prettierignore index 5322d7f..e820c72 100644 --- a/web-interface/.prettierignore +++ b/web-interface/.prettierignore @@ -1,3 +1,7 @@ package-lock.json pnpm-lock.yaml -yarn.lock \ No newline at end of file +yarn.lock +public/docs +dist +dist-ssr +.tanstack diff --git a/web-interface/README.md b/web-interface/README.md index 160d903..d965fb4 100644 --- a/web-interface/README.md +++ b/web-interface/README.md @@ -3,7 +3,7 @@ The authenticated React portal serves real team Jobs, hardware inventory, dataset uploads, common/member folder browsing, account administration and team policy/sharing controls. Production: **http://100.73.139.66:8088** through Tailscale. -See the [operating guide](../docs/department-rollout.md). +See the [operating guide](../docs/content/department-rollout.md). ## Start locally @@ -24,7 +24,7 @@ npm run dev -- --host 127.0.0.1 --port 3001 --strictPort Vite forwards `/api` and `/auth` to the API on port 3000. `MIST_API_PROXY` changes that target. Authenticate with a real Mist account and select your team; this -preview reaches real jobs/data. See [development](../docs/development.md) for +preview reaches real jobs/data. See [development](../docs/content/development.md) for other-host origins and backend work. Production uses a static Nginx release, not Vite or these per-user services. @@ -51,7 +51,7 @@ preserves image startup with blank overrides. CPU/RAM inventory shows allocation not live utilization. Inputs are read-only; save results in `/outputs`. The old fake account helpers/chart and unused `use-immer` dependency are retired. -Current behavior and components follow [the design system](../docs/design-system.md). +Current behavior and components follow [the design system](../docs/content/design-system.md). ## Verify @@ -62,4 +62,4 @@ npm run lint npm run build ``` -See [testing](../docs/testing.md) for browser-to-cluster checks and their scope. +See [testing](../docs/content/testing.md) for browser-to-cluster checks and their scope. diff --git a/web-interface/eslint.config.js b/web-interface/eslint.config.js index 3f272c0..7cdf373 100644 --- a/web-interface/eslint.config.js +++ b/web-interface/eslint.config.js @@ -15,6 +15,6 @@ export default [ }, }, { - ignores: ['eslint.config.js', 'prettier.config.js'], + ignores: ['eslint.config.js', 'prettier.config.js', 'public/**', 'dist/**'], }, ] diff --git a/web-interface/package.json b/web-interface/package.json index 0783ac3..2475954 100644 --- a/web-interface/package.json +++ b/web-interface/package.json @@ -11,6 +11,7 @@ "scripts": { "dev": "vite dev --port 3000", "build": "vite build", + "build:docs": "hugo -s ../docs", "preview": "vite preview", "test": "vitest run", "lint": "eslint", diff --git a/web-interface/src/auth.tsx b/web-interface/src/auth.tsx index 7f82822..546b417 100644 --- a/web-interface/src/auth.tsx +++ b/web-interface/src/auth.tsx @@ -4,6 +4,7 @@ import { createAuthClient } from 'better-auth/react' import { adminClient } from 'better-auth/client/plugins' import { jobsAPI } from '#/api.ts' import { errorMessage, usePolling } from '#/hooks/usePolling.ts' +import { BackendConnectionError } from '#/components/BackendConnectionError.tsx' export const authClient = createAuthClient({ basePath: '/auth', @@ -107,10 +108,11 @@ export function AccountGate({ children }: { children: ReactNode }) { ) if (session.error) return ( -

-

Account service unavailable: {session.error}

- -
+ ) if (session.data?.enabled && !session.data.user) return diff --git a/web-interface/src/components/JobsPage.test.tsx b/web-interface/src/components/JobsPage.test.tsx index ca397a0..3ace18f 100644 --- a/web-interface/src/components/JobsPage.test.tsx +++ b/web-interface/src/components/JobsPage.test.tsx @@ -270,7 +270,9 @@ test('keeps a cleared image blank while editing and uses the image startup defau const image = screen.getByLabelText('Container image') fireEvent.change(image, { target: { value: '' } }) expect(image.value).toBe('') - expect(screen.getByRole('button', { name: 'Submit job' }).hasAttribute('disabled')).toBe(true) + expect( + screen.getByRole('button', { name: 'Submit job' }).hasAttribute('disabled'), + ).toBe(true) fireEvent.change(image, { target: { value: 'cpu' } }) expect(image.value).toBe('cpu') fireEvent.change(image, { target: { value: 'cpu-runtime' } }) diff --git a/web-interface/src/components/Navbar.tsx b/web-interface/src/components/Navbar.tsx index 6814752..3b50311 100644 --- a/web-interface/src/components/Navbar.tsx +++ b/web-interface/src/components/Navbar.tsx @@ -1,7 +1,14 @@ import { Link } from '@tanstack/react-router' import { authClient, useAccount } from '#/auth.tsx' import { useState } from 'react' -import { LayoutDashboard, Play, Database, Server, Users } from 'lucide-react' +import { + LayoutDashboard, + Play, + Database, + Server, + Users, + BookOpen, +} from 'lucide-react' import { WorkspaceSelector } from '#/teams.tsx' export default function Navbar() { @@ -60,6 +67,12 @@ export default function Navbar() { {' '} Teams +
+ {' '} + Docs +
diff --git a/web-interface/src/components/jobs/JobSubmissionForm.tsx b/web-interface/src/components/jobs/JobSubmissionForm.tsx index 7bd51f3..21958ba 100644 --- a/web-interface/src/components/jobs/JobSubmissionForm.tsx +++ b/web-interface/src/components/jobs/JobSubmissionForm.tsx @@ -86,7 +86,7 @@ export function JobSubmissionForm({ const image = (mode === 'training-smoke' ? profile?.default_image - : imageOverride ?? profile?.default_image) || '' + : (imageOverride ?? profile?.default_image)) || '' const images = catalog?.images.filter((option) => option.accelerators.includes(compute)) ?? [] diff --git a/web-interface/src/format.ts b/web-interface/src/format.ts index e5e74c1..109eb12 100644 --- a/web-interface/src/format.ts +++ b/web-interface/src/format.ts @@ -1,6 +1,9 @@ export function formatBytes(bytes: number) { if (bytes === 0) return '0 B' const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'] - const power = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1) - return `${(bytes / 1024 ** power).toLocaleString(undefined, {maximumFractionDigits: power ? 1 : 0})} ${units[power]}` + const power = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ) + return `${(bytes / 1024 ** power).toLocaleString(undefined, { maximumFractionDigits: power ? 1 : 0 })} ${units[power]}` } diff --git a/web-interface/src/routes/__root.tsx b/web-interface/src/routes/__root.tsx index ccda042..6df912b 100644 --- a/web-interface/src/routes/__root.tsx +++ b/web-interface/src/routes/__root.tsx @@ -6,9 +6,16 @@ import '../styles.css' import Navbar from '#/components/Navbar.tsx' import { AccountGate } from '#/auth.tsx' import { TeamGate } from '#/teams.tsx' +import { BackendConnectionError } from '#/components/BackendConnectionError.tsx' export const Route = createRootRoute({ component: RootComponent, + errorComponent: ({ error, reset }) => ( + + ), }) function RootComponent() { diff --git a/web-interface/vite.config.ts b/web-interface/vite.config.ts index dc69775..206d212 100644 --- a/web-interface/vite.config.ts +++ b/web-interface/vite.config.ts @@ -1,12 +1,95 @@ import { defineConfig } from 'vite' +import type { Plugin } from 'vite' import { devtools } from '@tanstack/devtools-vite' import tsconfigPaths from 'vite-tsconfig-paths' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { execSync } from 'node:child_process' import { tanstackRouter } from '@tanstack/router-plugin/vite' import viteReact from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +function docsStaticPlugin(): Plugin { + return { + name: 'mist-docs-static', + buildStart() { + const docsIndex = path.resolve(__dirname, 'public/docs/index.html') + if (!fs.existsSync(docsIndex)) { + try { + execSync('hugo -s ../docs -d ../web-interface/public/docs', { + stdio: 'ignore', + }) + } catch { + // Hugo may not be installed; ignore build error + } + } + }, + configureServer(server) { + server.middlewares.use((req, res, next) => { + if (!req.url) return next() + const [pathname, search] = req.url.split('?') + const query = search ? `?${search}` : '' + if (pathname === '/docs') { + res.writeHead(301, { Location: `/docs/${query}` }) + res.end() + return + } + if (pathname === '/docs/') { + req.url = `/docs/index.html${query}` + return next() + } + if (pathname.startsWith('/docs/')) { + const relative = pathname.replace(/^\/docs\//, '').replace(/\/$/, '') + const potentialIndex = path.resolve( + __dirname, + 'public/docs', + relative, + 'index.html', + ) + if (fs.existsSync(potentialIndex)) { + req.url = `/docs/${relative}/index.html${query}` + } + } + next() + }) + }, + configurePreviewServer(server) { + server.middlewares.use((req, res, next) => { + if (!req.url) return next() + const [pathname, search] = req.url.split('?') + const query = search ? `?${search}` : '' + if (pathname === '/docs') { + res.writeHead(301, { Location: `/docs/${query}` }) + res.end() + return + } + if (pathname === '/docs/') { + req.url = `/docs/index.html${query}` + return next() + } + if (pathname.startsWith('/docs/')) { + const relative = pathname.replace(/^\/docs\//, '').replace(/\/$/, '') + const potentialIndex = path.resolve( + __dirname, + 'dist/docs', + relative, + 'index.html', + ) + if (fs.existsSync(potentialIndex)) { + req.url = `/docs/${relative}/index.html${query}` + } + } + next() + }) + }, + } +} + const config = defineConfig({ server: { proxy: { @@ -15,7 +98,7 @@ const config = defineConfig({ }, '/api': { target: process.env.MIST_API_PROXY ?? 'http://127.0.0.1:3000', - rewrite: (path) => path.replace(/^\/api/, ''), + rewrite: (p) => p.replace(/^\/api/, ''), }, }, }, @@ -23,6 +106,7 @@ const config = defineConfig({ devtools(), tsconfigPaths({ projects: ['./tsconfig.json'] }), tailwindcss(), + docsStaticPlugin(), tanstackRouter({ target: 'react', autoCodeSplitting: true }), viteReact(), ], From 953e2f05ae9256097172dac1547c9f9f70dcf76a Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Tue, 20 Oct 2026 11:13:11 -0400 Subject: [PATCH 09/10] feat: restructure docs build system --- .gitignore | 23 +- docs/content/_index.md | 48 ++++ docs/go.mod | 5 + docs/go.sum | 2 + docs/hugo.yaml | 47 ++++ .../BackendConnectionError.test.tsx | 76 ++++++ .../src/components/BackendConnectionError.tsx | 241 ++++++++++++++++++ web-interface/vite.config.ts | 165 +++++++----- 8 files changed, 537 insertions(+), 70 deletions(-) create mode 100644 docs/content/_index.md create mode 100644 docs/go.mod create mode 100644 docs/go.sum create mode 100644 docs/hugo.yaml create mode 100644 web-interface/src/components/BackendConnectionError.test.tsx create mode 100644 web-interface/src/components/BackendConnectionError.tsx diff --git a/.gitignore b/.gitignore index ed675b2..6bd8076 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,24 @@ +# Build outputs bin/ -/logs/ -__pycache__/ -*.py[cod] +dist/ +web-interface/dist/ +docs/public/ +docs/resources/ +*.hugo_build.lock + +# Dependencies +node_modules/ + +# Runtime data & local environment data/ +/logs/ .direnv/ .fake-backend-config.json -.hugo_build.lock +*.local + +# Python caches +__pycache__/ +*.py[cod] # OS files -.DS_Store \ No newline at end of file +.DS_Store diff --git a/docs/content/_index.md b/docs/content/_index.md new file mode 100644 index 0000000..fb686e8 --- /dev/null +++ b/docs/content/_index.md @@ -0,0 +1,48 @@ +--- +title: "Documentation Overview" +weight: 1 +--- + +# UTMIST Compute Platform Documentation + +Current main-branch documentation, verified against the two running machines on +**October 9, 2026**. Addresses and versions describe this installation; adjust +host-specific settings when provisioning another environment. + +## Find the right guide + +| Task | Guide | +|---|---| +| Sign in, manage teams, upload data, submit/download jobs | [Department operating guide](department-rollout.md) | +| Understand services and source layout | [Architecture](architecture.md) | +| Install/inspect k3s, join a worker, verify accelerator allocation | [k3s setup](k3s-setup.md) | +| Connect privately and administer either host | [Tailscale and SSH](tailscale-ssh.md) | +| Understand dataset/model pools and physical capacity | [Storage](storage.md) | +| Build, deploy, restart or troubleshoot services | [Deployment operations](../../deploy/private/README.md) | +| Develop locally with the existing authenticated API | [Developer setup](development.md) | +| Authenticate and submit from a terminal | [CLI](../../cli/docs/setup.md) | +| Run automated checks and read recorded live evidence | [Testing](testing.md) | +| Maintain UI components and interactions | [Design system](design-system.md) | +| Understand frontend architecture and backend offloading | [Frontend architecture](frontend-architecture.md) | +| Read completed scope, exclusions and release verification | [Execution record](department-rollout-execution.md) | +| See what repository cleanup removed and verified | [Cleanup record](repository-cleanup.md) | + +## Access and execution + +```text +Browser / CLI → Tailscale portal :8088 → Nginx → Go API → k3s + | | + Better Auth + team policy + | + CPU/NVIDIA or Tenstorrent Job + | + scoped dataset/model NFS mounts on QuietBox +``` + +Mist login is separate from Tailscale membership and Linux SSH login. Researchers +use the portal; administrator SSH is restricted by the installed host rules. +The worker reaches the control plane and storage through LAN addresses. + +[Earlier plans and single-owner guides](archive/_index.md) are historical only. +Current guidance does not require Redis, an SSH session to launch a job, a public +cloud load balancer, or permission to approve every tagged image. diff --git a/docs/go.mod b/docs/go.mod new file mode 100644 index 0000000..1e7e5e9 --- /dev/null +++ b/docs/go.mod @@ -0,0 +1,5 @@ +module github.com/UTMIST/Mist/docs + +go 1.25.3 + +require github.com/imfing/hextra v0.13.0 // indirect diff --git a/docs/go.sum b/docs/go.sum new file mode 100644 index 0000000..6557292 --- /dev/null +++ b/docs/go.sum @@ -0,0 +1,2 @@ +github.com/imfing/hextra v0.13.0 h1:1zhCYy0tA9ZRErFBlU6NJEgR1HjK69bgd52EmpwnG00= +github.com/imfing/hextra v0.13.0/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo= diff --git a/docs/hugo.yaml b/docs/hugo.yaml new file mode 100644 index 0000000..f959ee7 --- /dev/null +++ b/docs/hugo.yaml @@ -0,0 +1,47 @@ +baseURL: "/docs/" +title: "Mist Documentation" +defaultContentLanguage: "en" + +module: + imports: + - path: "github.com/imfing/hextra" + +markup: + highlight: + noClasses: false + guessSyntax: true + goldmark: + renderer: + unsafe: true + +menu: + main: + - name: "Overview" + pageRef: "/" + weight: 1 + - name: "Portal" + url: "/" + weight: 2 + - name: "GitHub" + url: "https://github.com/UTMIST/Mist" + weight: 3 + params: + icon: "github" + +params: + description: "Documentation and operational guides for UTMIST's research compute platform." + navbar: + displayLogo: true + logo: + text: "Mist Docs" + width: "max-w-7xl" + footer: + displayCopyright: true + displayPoweredBy: false + editURL: + enable: false + search: + enable: true + theme: + default: "system" + displayToggle: true diff --git a/web-interface/src/components/BackendConnectionError.test.tsx b/web-interface/src/components/BackendConnectionError.test.tsx new file mode 100644 index 0000000..d074456 --- /dev/null +++ b/web-interface/src/components/BackendConnectionError.test.tsx @@ -0,0 +1,76 @@ +// @vitest-environment jsdom +import { afterEach, expect, test, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { BackendConnectionError } from './BackendConnectionError' + +afterEach(cleanup) + +test('renders connection error message and status badge', () => { + render( + , + ) + + const heading = screen.getByRole('heading', { level: 1 }) + expect(heading.textContent).toContain('Unable to reach the compute backend') + expect( + screen.getByText('Failed to fetch /api/session: connection refused'), + ).toBeTruthy() + expect(screen.getByText('Backend Offline')).toBeTruthy() +}) + +test('triggers onRetry when Try again button is clicked', () => { + const onRetry = vi.fn() + render( + , + ) + + const retryButton = screen.getByRole('button', { + name: 'Try again to connect to the backend', + }) + expect(retryButton).toBeTruthy() + expect(retryButton.textContent).toContain('Try again') + + fireEvent.click(retryButton) + expect(onRetry).toHaveBeenCalledTimes(1) +}) + +test('disables button and displays loading state when isRetrying is true', () => { + render( + {}} + isRetrying={true} + />, + ) + + const retryButton = screen.getByRole('button', { + name: 'Try again to connect to the backend', + }) + expect(retryButton.hasAttribute('disabled')).toBe(true) + expect(retryButton.textContent).toContain('Checking connection…') +}) + +test('provides links to static documentation and GitHub issues', () => { + render( + {}} + />, + ) + + const docsLink = screen.getByRole('link', { + name: /Documentation.*View documentation/s, + }) + expect(docsLink.getAttribute('href')).toBe('/docs/') + + const githubLink = screen.getByRole('link', { + name: /Report an Issue.*Open GitHub issues/s, + }) + expect(githubLink.getAttribute('href')).toBe( + 'https://github.com/UTMIST/Mist/issues', + ) + expect(githubLink.getAttribute('target')).toBe('_blank') +}) diff --git a/web-interface/src/components/BackendConnectionError.tsx b/web-interface/src/components/BackendConnectionError.tsx new file mode 100644 index 0000000..347cec4 --- /dev/null +++ b/web-interface/src/components/BackendConnectionError.tsx @@ -0,0 +1,241 @@ +import { useState } from 'react' +import { + AlertCircle, + BookOpen, + ExternalLink, + Github, + RefreshCw, +} from 'lucide-react' + +export interface BackendConnectionErrorProps { + error?: string + onRetry?: () => void | Promise + isRetrying?: boolean +} + +export function BackendConnectionError({ + error = 'Unable to establish connection to the Mist API service.', + onRetry, + isRetrying: externalRetrying, +}: BackendConnectionErrorProps) { + const [internalRetrying, setInternalRetrying] = useState(false) + const isRetrying = externalRetrying ?? internalRetrying + + async function handleRetry() { + if (isRetrying || !onRetry) return + setInternalRetrying(true) + try { + await onRetry() + } finally { + setInternalRetrying(false) + } + } + + return ( +
+
+ {/* Brand header & status badge */} +
+
+ +
+ + Mist + + Research Compute +
+
+ +
+
+
+ + {/* Primary Message */} +
+

SYSTEM STATUS / API DISCONNECTED

+

+ Unable to reach the compute backend +

+

+ The web portal is running, but cannot communicate with the Mist API + or account service. The backend server may be restarting, undergoing + maintenance, or temporarily unreachable over the private network. +

+
+ + {/* Error diagnostics banner */} +
+
+
+
+ + {/* Action button */} +
+ + + Automatic retries happen in the background. + +
+ + {/* Informational Cards: Docs & GitHub */} +
+ {/* Static Documentation link */} + +
+
+ + + Static & Active + +
+

+ Documentation + +

+

+ The static documentation site remains unaffected and fully + accessible even when the compute backend is down. Browse + operating guides, architecture, and setup manuals. +

+
+
+ View documentation + +
+
+ + {/* GitHub Repository Issue link */} + +
+
+ + + GitHub + +
+

+ Report an Issue +

+

+ Notice an unexpected service outage or API bug? Submit a report + or check active incidents on the UTMIST GitHub repository. +

+
+
+ Open GitHub issues +
+
+
+ + {/* Troubleshooting checklist */} +
+

+ Troubleshooting checklist +

+
    +
  • + Private mesh: Verify that your Tailscale client + is connected to the UTMIST tailnet. +
  • +
  • + Local dev: Ensure backend server is active at{' '} + + http://127.0.0.1:3000 + + . +
  • +
  • + Cluster admin: Check service logs with{' '} + + kubectl -n mist-system get pods + + . +
  • +
+
+
+
+ ) +} diff --git a/web-interface/vite.config.ts b/web-interface/vite.config.ts index 206d212..a52cb6c 100644 --- a/web-interface/vite.config.ts +++ b/web-interface/vite.config.ts @@ -14,78 +14,113 @@ import tailwindcss from '@tailwindcss/vite' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const MIME_TYPES: Record = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.xml': 'application/xml; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.txt': 'text/plain; charset=utf-8', +} + function docsStaticPlugin(): Plugin { + const docsDir = path.resolve(__dirname, '../docs/public') + + function ensureDocsBuilt() { + if (!fs.existsSync(path.resolve(docsDir, 'index.html'))) { + try { + execSync('hugo -s ../docs', { stdio: 'ignore' }) + } catch { + // Hugo may not be installed in minimal test environments + } + } + } + + function serveFromDocs( + req: { url?: string }, + res: { + writeHead: (status: number, headers?: Record) => void + end: () => void + }, + next: () => void, + ) { + if (!req.url) return next() + const [pathname, search] = req.url.split('?') + const query = search ? `?${search}` : '' + + if (pathname === '/docs') { + res.writeHead(301, { Location: `/docs/${query}` }) + res.end() + return + } + + if (pathname.startsWith('/docs/')) { + const relPath = pathname.replace(/^\/docs\//, '') + let candidate = path.resolve(docsDir, relPath) + + // Prevent directory traversal outside docsDir + if (!candidate.startsWith(docsDir)) { + res.writeHead(403) + res.end() + return + } + + if (fs.existsSync(candidate)) { + const stat = fs.statSync(candidate) + if (stat.isDirectory()) { + if (!pathname.endsWith('/')) { + res.writeHead(301, { Location: `${pathname}/${query}` }) + res.end() + return + } + candidate = path.resolve(candidate, 'index.html') + } + } else { + const asHtml = `${candidate}.html` + if (fs.existsSync(asHtml)) candidate = asHtml + } + + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { + const ext = path.extname(candidate).toLowerCase() + const mime = MIME_TYPES[ext] ?? 'application/octet-stream' + res.writeHead(200, { + 'Content-Type': mime, + 'Cache-Control': 'no-cache', + }) + fs.createReadStream(candidate).pipe(res as any) + return + } + } + + next() + } + return { name: 'mist-docs-static', buildStart() { - const docsIndex = path.resolve(__dirname, 'public/docs/index.html') - if (!fs.existsSync(docsIndex)) { - try { - execSync('hugo -s ../docs -d ../web-interface/public/docs', { - stdio: 'ignore', - }) - } catch { - // Hugo may not be installed; ignore build error - } + ensureDocsBuilt() + }, + closeBundle() { + ensureDocsBuilt() + const distDocs = path.resolve(__dirname, 'dist/docs') + if (fs.existsSync(docsDir)) { + fs.cpSync(docsDir, distDocs, { recursive: true }) } }, configureServer(server) { - server.middlewares.use((req, res, next) => { - if (!req.url) return next() - const [pathname, search] = req.url.split('?') - const query = search ? `?${search}` : '' - if (pathname === '/docs') { - res.writeHead(301, { Location: `/docs/${query}` }) - res.end() - return - } - if (pathname === '/docs/') { - req.url = `/docs/index.html${query}` - return next() - } - if (pathname.startsWith('/docs/')) { - const relative = pathname.replace(/^\/docs\//, '').replace(/\/$/, '') - const potentialIndex = path.resolve( - __dirname, - 'public/docs', - relative, - 'index.html', - ) - if (fs.existsSync(potentialIndex)) { - req.url = `/docs/${relative}/index.html${query}` - } - } - next() - }) - }, - configurePreviewServer(server) { - server.middlewares.use((req, res, next) => { - if (!req.url) return next() - const [pathname, search] = req.url.split('?') - const query = search ? `?${search}` : '' - if (pathname === '/docs') { - res.writeHead(301, { Location: `/docs/${query}` }) - res.end() - return - } - if (pathname === '/docs/') { - req.url = `/docs/index.html${query}` - return next() - } - if (pathname.startsWith('/docs/')) { - const relative = pathname.replace(/^\/docs\//, '').replace(/\/$/, '') - const potentialIndex = path.resolve( - __dirname, - 'dist/docs', - relative, - 'index.html', - ) - if (fs.existsSync(potentialIndex)) { - req.url = `/docs/${relative}/index.html${query}` - } - } - next() - }) + ensureDocsBuilt() + server.middlewares.use(serveFromDocs) }, } } From 4e64d703f611c559d795eae285b3896195c54361 Mon Sep 17 00:00:00 2001 From: andrei-akopian Date: Tue, 20 Oct 2026 13:35:43 -0400 Subject: [PATCH 10/10] feat: hugo quality of life features --- docs/content/_index.md | 3 ++ docs/content/archive/_index.md | 2 ++ docs/hugo.yaml | 61 +++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index fb686e8..c11a6df 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -1,4 +1,7 @@ --- +type: docs +cascade: + type: docs title: "Documentation Overview" weight: 1 --- diff --git a/docs/content/archive/_index.md b/docs/content/archive/_index.md index af96a83..7c8bcdf 100644 --- a/docs/content/archive/_index.md +++ b/docs/content/archive/_index.md @@ -1,6 +1,8 @@ --- title: "Historical Records" weight: 200 +sidebar: + open: false --- # Historical records diff --git a/docs/hugo.yaml b/docs/hugo.yaml index f959ee7..94f90f1 100644 --- a/docs/hugo.yaml +++ b/docs/hugo.yaml @@ -1,6 +1,7 @@ baseURL: "/docs/" title: "Mist Documentation" defaultContentLanguage: "en" +enableRobotsTXT: true module: imports: @@ -13,35 +14,79 @@ markup: goldmark: renderer: unsafe: true + extensions: + passthrough: + delimiters: + block: [['\[', '\]'], ['$$', '$$']] + inline: [['\(', '\)']] + enable: true menu: main: - name: "Overview" pageRef: "/" weight: 1 - - name: "Portal" + - name: "Web Portal" url: "/" weight: 2 + - name: "Search" + weight: 5 + params: + type: search - name: "GitHub" url: "https://github.com/UTMIST/Mist" - weight: 3 + weight: 10 params: icon: "github" params: description: "Documentation and operational guides for UTMIST's research compute platform." + + externalLinkDecoration: true + navbar: + displayTitle: true displayLogo: true logo: text: "Mist Docs" - width: "max-w-7xl" + width: wide + + theme: + default: "system" + displayToggle: true + footer: + enable: true displayCopyright: true displayPoweredBy: false - editURL: - enable: false + width: normal + + displayUpdatedDate: true + dateFormat: "January 2, 2006" + search: enable: true - theme: - default: "system" - displayToggle: true + type: flexsearch + flexsearch: + index: content + tokenize: forward + + editURL: + enable: true + base: "https://github.com/UTMIST/Mist/edit/main/docs/content" + + highlight: + copy: + enable: true + display: hover + + toc: + enable: true + displayTags: true + + page: + width: normal + displayPagination: true + + imageZoom: + enable: true