From 023cf83cfa6d1a18f45268b2099ea3ca06455509 Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Fri, 14 Aug 2026 14:25:37 +0700 Subject: [PATCH] feat: enhance project structure with server and workflow management - Introduced a new server package with Fastify for handling API requests and workflows. - Implemented user authentication and authorization with JWT and cookie management. - Added endpoints for managing workflows, runs, and user accounts. - Established a dashboard for monitoring workflow status and statistics. - Included a script sandbox for executing user-defined scripts securely. - Updated README with setup instructions and API documentation. - Configured database migrations for user management. - Enhanced logging capabilities for better traceability. --- .gitignore | 1 + README.md | 53 + package.json | 30 +- packages/server/docs/mt.http | 44 +- packages/server/fs-store.js | 128 + packages/server/knexfile.js | 5 +- packages/server/logger.js | 5 +- .../server/migrations/20260814120000_users.js | 20 + packages/server/package.json | 28 + packages/server/paths.js | 9 + packages/server/registry.js | 288 ++ packages/server/runner.js | 438 +-- packages/server/script-sandbox.js | 5 +- packages/server/scripts/jsonata.js | 10 +- packages/server/scripts/ntfy.js | 11 +- packages/server/src/api/auth.js | 103 + packages/server/src/api/dashboard.js | 86 + packages/server/src/api/runs.js | 26 + packages/server/src/api/scripts.js | 64 + packages/server/src/api/users.js | 85 + packages/server/src/api/workflows.js | 240 ++ packages/server/store.js | 99 + packages/server/workflow-parse.js | 15 + .../workflows/default/manual-trigger.yaml | 1 + packages/server/workflows/default/test.yaml | 2 +- packages/web/index.html | 12 + packages/web/package.json | 29 + packages/web/src/App.jsx | 60 + packages/web/src/api/client.js | 22 + packages/web/src/api/hooks.js | 256 ++ packages/web/src/components/CodeEditor.jsx | 30 + packages/web/src/components/Layout.jsx | 93 + .../web/src/components/MermaidDiagram.jsx | 75 + packages/web/src/index.css | 14 + packages/web/src/lib/format.jsx | 51 + packages/web/src/lib/workflow-mermaid.js | 67 + packages/web/src/main.jsx | 28 + packages/web/src/pages/AuthPage.jsx | 64 + packages/web/src/pages/EventDetailPage.jsx | 129 + packages/web/src/pages/EventsPage.jsx | 82 + packages/web/src/pages/HomePage.jsx | 124 + packages/web/src/pages/ScriptsPage.jsx | 198 ++ packages/web/src/pages/UsersPage.jsx | 194 ++ packages/web/src/pages/WorkflowsPage.jsx | 384 +++ packages/web/src/theme.jsx | 27 + packages/web/vite.config.js | 14 + pnpm-lock.yaml | 2735 ++++++++++++++++- pnpm-workspace.yaml | 2 + 48 files changed, 6127 insertions(+), 359 deletions(-) create mode 100644 README.md create mode 100644 packages/server/fs-store.js create mode 100644 packages/server/migrations/20260814120000_users.js create mode 100644 packages/server/package.json create mode 100644 packages/server/paths.js create mode 100644 packages/server/registry.js create mode 100644 packages/server/src/api/auth.js create mode 100644 packages/server/src/api/dashboard.js create mode 100644 packages/server/src/api/runs.js create mode 100644 packages/server/src/api/scripts.js create mode 100644 packages/server/src/api/users.js create mode 100644 packages/server/src/api/workflows.js create mode 100644 packages/server/workflow-parse.js create mode 100644 packages/web/index.html create mode 100644 packages/web/package.json create mode 100644 packages/web/src/App.jsx create mode 100644 packages/web/src/api/client.js create mode 100644 packages/web/src/api/hooks.js create mode 100644 packages/web/src/components/CodeEditor.jsx create mode 100644 packages/web/src/components/Layout.jsx create mode 100644 packages/web/src/components/MermaidDiagram.jsx create mode 100644 packages/web/src/index.css create mode 100644 packages/web/src/lib/format.jsx create mode 100644 packages/web/src/lib/workflow-mermaid.js create mode 100644 packages/web/src/main.jsx create mode 100644 packages/web/src/pages/AuthPage.jsx create mode 100644 packages/web/src/pages/EventDetailPage.jsx create mode 100644 packages/web/src/pages/EventsPage.jsx create mode 100644 packages/web/src/pages/HomePage.jsx create mode 100644 packages/web/src/pages/ScriptsPage.jsx create mode 100644 packages/web/src/pages/UsersPage.jsx create mode 100644 packages/web/src/pages/WorkflowsPage.jsx create mode 100644 packages/web/src/theme.jsx create mode 100644 packages/web/vite.config.js create mode 100644 pnpm-workspace.yaml diff --git a/.gitignore b/.gitignore index 921cc58..2e4d176 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ logs/ *.db *.db-* .DS_Store +packages/web/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d9605d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# scrunner + +Workflow runner with a sandboxed script engine, SQLite run history, and an admin UI. + +## Packages + +- `packages/server` — Fastify runner, HTTP/cron triggers, admin REST API +- `packages/web` — React admin UI (Vite, DaisyUI, React Query) + +## Setup + +```bash +pnpm install +pnpm dev +``` + +- API: http://localhost:9000 +- UI (dev): http://localhost:5173 + +The first account created becomes **admin**. Later accounts are created from Users. + +## Scripts + +| Command | Description | +|---|---| +| `pnpm dev` | Server + Vite together | +| `pnpm dev:server` | API/runner only | +| `pnpm dev:web` | UI only (proxies `/api` to :9000) | +| `pnpm build` | Production UI build | +| `pnpm start` | Serve API and built UI from :9000 | +| `pnpm migrate` | Apply SQLite migrations | + +## Environment + +| Variable | Default | Notes | +|---|---|---| +| `SCRUNNER_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production** | +| `SCRUNNER_DB_PATH` | `packages/server/data/scrunner.db` | SQLite file | +| `SCRUNNER_LOG_LEVEL` | `debug` | Pino level | +| `SCRUNNER_RETENTION_DAYS` | `30` | Run history prune | +| `SCRUNNER_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev | +| `PORT` | `9000` | HTTP port | +| `NODE_ENV` | — | Set `production` for secure cookies | + +## Production + +```bash +pnpm install +pnpm build +SCRUNNER_JWT_SECRET=... NODE_ENV=production pnpm start +``` + +The server serves `packages/web/dist` when that folder exists. diff --git a/package.json b/package.json index b9e7b28..dacf15f 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,22 @@ { "name": "scrunner", "version": "1.0.0", - "description": "", - "main": "index.js", + "private": true, + "description": "Script/workflow runner with admin UI", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "pnpm --filter @scrunner/server --filter @scrunner/web --parallel dev", + "dev:server": "pnpm --filter @scrunner/server dev", + "dev:web": "pnpm --filter @scrunner/web dev", + "build": "pnpm --filter @scrunner/web build", + "start": "pnpm --filter @scrunner/server start", + "migrate": "pnpm --filter @scrunner/server migrate" }, - "keywords": [], - "author": "", - "license": "ISC", "packageManager": "pnpm@10.25.0", "pnpm": { "onlyBuiltDependencies": [ - "better-sqlite3" + "better-sqlite3", + "esbuild" ] - }, - "dependencies": { - "axios": "^1.19.0", - "better-sqlite3": "^13.0.3", - "fastify": "^5.12.0", - "jsonata": "^2.2.2", - "knex": "^3.3.0", - "node-cron": "^4.6.0", - "pino": "^10.3.1", - "pino-roll": "^4.0.0", - "yaml": "^2.9.0" } -} \ No newline at end of file +} diff --git a/packages/server/docs/mt.http b/packages/server/docs/mt.http index eed9934..92f846c 100644 --- a/packages/server/docs/mt.http +++ b/packages/server/docs/mt.http @@ -4,32 +4,38 @@ Content-Type: application/json 0 -HTTP/1.1 200 - OK -content-type: application/json; charset=utf-8 -content-length: 73 -date: Fri, 14 Aug 2026 04:34:56 GMT -connection: close ### POST http://localhost:9000/u/default/time-to-ntfy Content-Type: application/json {} -HTTP/1.1 500 - Internal Server Error -content-type: application/json; charset=utf-8 -content-length: 97 -date: Fri, 14 Aug 2026 04:11:52 GMT -connection: close -### -GET http://localhost:9000/admin/runs?owner=default&limit=20 -### -POST http://localhost:9000/admin/workflows/reregister +### Auth bootstrap +GET http://localhost:9000/api/auth/bootstrap + +### Login +POST http://localhost:9000/api/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "changeme1" +} + +### Dashboard +GET http://localhost:9000/api/dashboard + +### Runs +GET http://localhost:9000/api/runs?owner=default&limit=20 + +### Reregister +POST http://localhost:9000/api/workflows/reregister Content-Type: application/json {} -HTTP/1.1 200 - OK -content-type: application/json; charset=utf-8 -content-length: 33 -date: Fri, 14 Aug 2026 04:34:22 GMT -connection: close \ No newline at end of file +### Run workflow manually +POST http://localhost:9000/api/workflows/default/manual-trigger.yaml/run +Content-Type: application/json + +{} diff --git a/packages/server/fs-store.js b/packages/server/fs-store.js new file mode 100644 index 0000000..23061d1 --- /dev/null +++ b/packages/server/fs-store.js @@ -0,0 +1,128 @@ +import fs from "fs"; +import path from "path"; +import yaml from "yaml"; +import { SCRIPTS_DIR, WORKFLOWS_DIR } from "./paths.js"; + +export function assertScriptName(name) { + if (typeof name !== "string" || !/^[A-Za-z0-9._-]+\.js$/.test(name)) { + const err = new Error("invalid script name"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function assertOwner(name) { + if (typeof name !== "string" || !/^[A-Za-z0-9_-]+$/.test(name)) { + const err = new Error("invalid owner"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function assertWorkflowFile(name) { + if ( + typeof name !== "string" || + !/^[A-Za-z0-9._-]+\.ya?ml$/.test(name) || + name === "registers.yaml" + ) { + const err = new Error("invalid workflow file"); + err.statusCode = 400; + throw err; + } + return name; +} + +export function listScriptFiles() { + if (!fs.existsSync(SCRIPTS_DIR)) return []; + return fs + .readdirSync(SCRIPTS_DIR) + .filter((f) => f.endsWith(".js")) + .sort(); +} + +export function readScript(name) { + assertScriptName(name); + const filePath = path.join(SCRIPTS_DIR, name); + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); +} + +export function writeScript(name, content) { + assertScriptName(name); + fs.mkdirSync(SCRIPTS_DIR, { recursive: true }); + fs.writeFileSync(path.join(SCRIPTS_DIR, name), content, "utf8"); +} + +export function deleteScript(name) { + assertScriptName(name); + const filePath = path.join(SCRIPTS_DIR, name); + if (!fs.existsSync(filePath)) return false; + fs.unlinkSync(filePath); + return true; +} + +export function listOwners() { + if (!fs.existsSync(WORKFLOWS_DIR)) return []; + return fs + .readdirSync(WORKFLOWS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); +} + +function registersPath(owner) { + return path.join(WORKFLOWS_DIR, owner, "registers.yaml"); +} + +export function readRegisters(owner) { + const filePath = registersPath(owner); + if (!fs.existsSync(filePath)) return []; + const parsed = yaml.parse(fs.readFileSync(filePath, "utf8")) ?? {}; + return Array.isArray(parsed.scripts) ? parsed.scripts : []; +} + +export function writeRegisters(owner, files) { + const ownerDir = path.join(WORKFLOWS_DIR, owner); + fs.mkdirSync(ownerDir, { recursive: true }); + fs.writeFileSync( + registersPath(owner), + yaml.stringify({ scripts: files }), + "utf8", + ); +} + +export function readWorkflowYaml(owner, file) { + assertOwner(owner); + assertWorkflowFile(file); + const filePath = path.join(WORKFLOWS_DIR, owner, file); + if (!fs.existsSync(filePath)) return null; + return fs.readFileSync(filePath, "utf8"); +} + +export function writeWorkflowYaml(owner, file, content) { + assertOwner(owner); + assertWorkflowFile(file); + const ownerDir = path.join(WORKFLOWS_DIR, owner); + fs.mkdirSync(ownerDir, { recursive: true }); + fs.writeFileSync(path.join(ownerDir, file), content, "utf8"); +} + +export function deleteWorkflowYaml(owner, file) { + assertOwner(owner); + assertWorkflowFile(file); + const filePath = path.join(WORKFLOWS_DIR, owner, file); + if (!fs.existsSync(filePath)) return false; + fs.unlinkSync(filePath); + return true; +} + +export function listOwnerYamlFiles(owner) { + const ownerDir = path.join(WORKFLOWS_DIR, owner); + if (!fs.existsSync(ownerDir)) return []; + return fs + .readdirSync(ownerDir) + .filter((f) => f.endsWith(".yaml") && f !== "registers.yaml") + .sort(); +} diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js index 6016e81..eed0814 100644 --- a/packages/server/knexfile.js +++ b/packages/server/knexfile.js @@ -1,7 +1,8 @@ import fs from "fs"; import path from "path"; +import { DATA_DIR, SERVER_ROOT } from "./paths.js"; -const dbPath = process.env.SCRUNNER_DB_PATH ?? path.resolve("data/scrunner.db"); +const dbPath = process.env.SCRUNNER_DB_PATH ?? path.join(DATA_DIR, "scrunner.db"); fs.mkdirSync(path.dirname(dbPath), { recursive: true }); /** @type {import("knex").Knex.Config} */ @@ -12,7 +13,7 @@ const config = { }, useNullAsDefault: true, migrations: { - directory: "./migrations", + directory: path.join(SERVER_ROOT, "migrations"), extension: "js", loadExtensions: [".js"], }, diff --git a/packages/server/logger.js b/packages/server/logger.js index 5c79427..01a4ee5 100644 --- a/packages/server/logger.js +++ b/packages/server/logger.js @@ -2,6 +2,7 @@ import fs from "fs"; import path from "path"; import pino from "pino"; import * as store from "./store.js"; +import { LOGS_DIR } from "./paths.js"; const LEVEL_TO_NUM = { trace: 10, @@ -87,12 +88,12 @@ const sqliteStream = { }, }; -fs.mkdirSync("logs", { recursive: true }); +fs.mkdirSync(LOGS_DIR, { recursive: true }); const rollingFile = pino.transport({ target: "pino-roll", options: { - file: path.resolve("logs/scrunner.log"), + file: path.join(LOGS_DIR, "scrunner.log"), size: "10m", mkdir: true, limit: { count: 5 }, diff --git a/packages/server/migrations/20260814120000_users.js b/packages/server/migrations/20260814120000_users.js new file mode 100644 index 0000000..c30e307 --- /dev/null +++ b/packages/server/migrations/20260814120000_users.js @@ -0,0 +1,20 @@ +/** + * @param {import("knex").Knex} knex + */ +export async function up(knex) { + await knex.schema.createTable("users", (t) => { + t.text("id").primary(); + t.text("username").notNullable().unique(); + t.text("password_hash").notNullable(); + t.text("role").notNullable(); + t.text("created_at").notNullable(); + t.text("updated_at").notNullable(); + }); +} + +/** + * @param {import("knex").Knex} knex + */ +export async function down(knex) { + await knex.schema.dropTableIfExists("users"); +} diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..1ac21fa --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,28 @@ +{ + "name": "@scrunner/server", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "runner.js", + "scripts": { + "dev": "node --watch runner.js", + "start": "node runner.js", + "migrate": "node -e \"import('./db.js').then((m) => m.migrate().then(() => process.exit(0)))\"" + }, + "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.1.0", + "@fastify/jwt": "^9.1.0", + "@fastify/static": "^8.2.0", + "axios": "^1.19.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^13.0.3", + "fastify": "^5.12.0", + "jsonata": "^2.2.2", + "knex": "^3.3.0", + "node-cron": "^4.6.0", + "pino": "^10.3.1", + "pino-roll": "^4.0.0", + "yaml": "^2.9.0" + } +} diff --git a/packages/server/paths.js b/packages/server/paths.js new file mode 100644 index 0000000..f84f6a5 --- /dev/null +++ b/packages/server/paths.js @@ -0,0 +1,9 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +export const SERVER_ROOT = path.dirname(fileURLToPath(import.meta.url)); +export const SCRIPTS_DIR = path.join(SERVER_ROOT, "scripts"); +export const WORKFLOWS_DIR = path.join(SERVER_ROOT, "workflows"); +export const DATA_DIR = path.join(SERVER_ROOT, "data"); +export const LOGS_DIR = path.join(SERVER_ROOT, "logs"); +export const WEB_DIST = path.resolve(SERVER_ROOT, "../web/dist"); diff --git a/packages/server/registry.js b/packages/server/registry.js new file mode 100644 index 0000000..11e972b --- /dev/null +++ b/packages/server/registry.js @@ -0,0 +1,288 @@ +import fs from "fs"; +import path from "path"; +import yaml from "yaml"; +import cron from "node-cron"; +import { WORKFLOWS_DIR } from "./paths.js"; +import { log } from "./logger.js"; +import * as store from "./store.js"; +import { clearScriptCache, runScript } from "./script-sandbox.js"; +import { namespacedPath, parseScriptStep } from "./workflow-parse.js"; +import * as fsStore from "./fs-store.js"; + +/** + * @typedef {{ owner: string, file: string, workflow: any }} WorkflowEntry + */ + +/** + * @param {import("fastify").FastifyInstance} server + */ +export function createRegistry(server) { + /** @type {Map} */ + const workflows = new Map(); + /** @type {Map} */ + const loadErrors = new Map(); + /** @type {import("node-cron").ScheduledTask[]} */ + const cronTasks = []; + /** @type {import("node-cron").ScheduledTask | null} */ + let pruneTask = null; + /** @type {Set} */ + const registeredHttpRoutes = new Set(); + + function registerWorkflows() { + workflows.clear(); + loadErrors.clear(); + clearScriptCache(); + + if (!fs.existsSync(WORKFLOWS_DIR)) { + log.warn("workflows directory missing"); + return; + } + + const owners = fsStore.listOwners(); + + for (const owner of owners) { + const registersPath = path.join(WORKFLOWS_DIR, owner, "registers.yaml"); + if (!fs.existsSync(registersPath)) { + log.warn(`Skipping owner "${owner}": no registers.yaml`); + continue; + } + + let workflowFiles = []; + try { + workflowFiles = fsStore.readRegisters(owner); + } catch (err) { + log.error({ err, owner }, "failed to parse registers.yaml"); + continue; + } + + const onDisk = fsStore.listOwnerYamlFiles(owner); + for (const file of onDisk) { + if (!workflowFiles.includes(file)) { + log.warn(`Workflow file not in registers.yaml: ${owner}/${file}`); + } + } + + for (const file of workflowFiles) { + const key = `${owner}/${file}`; + const filePath = path.join(WORKFLOWS_DIR, owner, file); + try { + const workflowData = fs.readFileSync(filePath, "utf8"); + const workflow = yaml.parse(workflowData); + workflows.set(key, { owner, file, workflow }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + loadErrors.set(key, message); + log.error({ err, workflow: key }, "failed to load workflow; skipping"); + } + } + } + + log.debug({ count: workflows.size }, "workflows loaded"); + } + + function registerHttpTriggers() { + const seen = new Set(); + + for (const [key, { owner, workflow }] of workflows) { + if (workflow.enabled === false) { + log.debug(`Skipping disabled workflow HTTP triggers (${key})`); + continue; + } + + for (const trigger of workflow.triggers ?? []) { + if (trigger.type !== "HTTP") continue; + + const method = String(trigger.method ?? "POST").toUpperCase(); + const url = namespacedPath(owner, trigger.path); + const routeKey = `${method} ${url}`; + + if (seen.has(routeKey)) { + log.warn(`Skipping duplicate HTTP trigger ${routeKey} (${key})`); + continue; + } + seen.add(routeKey); + + if (registeredHttpRoutes.has(routeKey)) { + continue; + } + registeredHttpRoutes.add(routeKey); + + server.route({ + method, + url, + handler: async (req, reply) => { + const result = await runWorkflow( + key, + { data: req.body }, + { type: "http", detail: `${method} ${url}` }, + ); + if (result.status === "failed") { + return reply.code(500).send({ + runId: result.runId, + error: result.error, + }); + } + return reply.send({ + runId: result.runId, + result: result.result, + }); + }, + }); + log.debug(`Registered HTTP trigger ${routeKey} (${key})`); + } + } + } + + function registerCronTriggers() { + for (const task of cronTasks) { + task.destroy(); + } + cronTasks.length = 0; + + for (const [key, { workflow }] of workflows) { + if (workflow.enabled === false) { + log.debug(`Skipping disabled workflow cron triggers (${key})`); + continue; + } + + for (const trigger of workflow.triggers ?? []) { + if (trigger.type !== "cron") continue; + + const schedule = trigger.schedule; + if (!schedule || !cron.validate(schedule)) { + log.warn(`Skipping invalid cron schedule "${schedule}" (${key})`); + continue; + } + + const task = cron.schedule( + schedule, + () => { + log.debug(`cron firing ${key} (${schedule})`); + return runWorkflow( + key, + { data: workflow.data ?? null }, + { type: "cron", detail: schedule }, + ); + }, + { name: `${key}:${schedule}`, noOverlap: true }, + ); + cronTasks.push(task); + log.debug(`Registered cron trigger ${schedule} (${key})`); + } + } + } + + function registerPruneJob() { + if (pruneTask) { + pruneTask.destroy(); + pruneTask = null; + } + const days = Number(process.env.SCRUNNER_RETENTION_DAYS ?? 30); + pruneTask = cron.schedule( + "0 0 * * *", + async () => { + try { + const deleted = await store.pruneOlderThan(days); + log.info({ deleted, days }, "pruned old workflow runs"); + } catch (err) { + log.error({ err }, "failed to prune old workflow runs"); + } + }, + { name: "prune-runs" }, + ); + } + + /** + * @param {string} key + * @param {{ data?: unknown }} context + * @param {{ type: string, detail?: string | null }} trigger + */ + async function runWorkflow(key, context, trigger) { + const entry = workflows.get(key); + if (!entry) { + log.error({ workflow: key }, "workflow not found"); + return { runId: null, status: "failed", error: "workflow not found" }; + } + + const { owner, workflow } = entry; + const run = await store.startRun({ + owner, + workflow: key, + workflowName: workflow?.name, + trigger, + input: context.data, + }); + const runLog = log.child({ runId: run.id, owner, workflow: key }); + runLog.debug("running workflow"); + + let ctx = { + ...context, + data: context.data ?? workflow.data ?? null, + }; + + try { + for (const [index, rawStep] of (workflow.scripts ?? []).entries()) { + const { script, config } = parseScriptStep(rawStep); + const step = await store.startStep({ + runId: run.id, + index, + script, + config, + }); + const stepLog = runLog.child({ stepId: step.id, script }); + try { + ctx = await runScript(script, { ...ctx, config }, { + log: stepLog, + workflowName: key, + }); + await store.finishStep(step.id, "success", ctx); + } catch (err) { + await store.finishStep(step.id, "failed", null, err); + throw err; + } + } + await store.finishRun(run.id, "success", ctx); + return { runId: run.id, status: "success", result: ctx }; + } catch (err) { + runLog.error({ err }, "workflow failed"); + await store.finishRun(run.id, "failed", null, err); + return { + runId: run.id, + status: "failed", + error: err instanceof Error ? err.message : String(err), + }; + } + } + + function reregister() { + registerWorkflows(); + registerHttpTriggers(); + registerCronTriggers(); + } + + function referencedScripts() { + const refs = new Set(); + for (const { workflow } of workflows.values()) { + for (const raw of workflow.scripts ?? []) { + try { + refs.add(parseScriptStep(raw).script); + } catch { + // skip invalid steps + } + } + } + return refs; + } + + return { + workflows, + loadErrors, + registerWorkflows, + registerHttpTriggers, + registerCronTriggers, + registerPruneJob, + reregister, + runWorkflow, + referencedScripts, + }; +} diff --git a/packages/server/runner.js b/packages/server/runner.js index eab615f..541d89e 100644 --- a/packages/server/runner.js +++ b/packages/server/runner.js @@ -1,328 +1,148 @@ import fs from "fs"; -import path from "path"; -import yaml from "yaml"; import fastify from "fastify"; -import cron from "node-cron"; +import cookie from "@fastify/cookie"; +import cors from "@fastify/cors"; +import jwt from "@fastify/jwt"; +import fastifyStatic from "@fastify/static"; import { migrate, db } from "./db.js"; import { log, enableLogPersistence, flushLogs } from "./logger.js"; import * as store from "./store.js"; -import { clearScriptCache, runScript } from "./script-sandbox.js"; +import { createRegistry } from "./registry.js"; +import { COOKIE, OPEN_API_ROUTES } from "./src/api/auth.js"; +import authPlugin from "./src/api/auth.js"; +import usersPlugin from "./src/api/users.js"; +import scriptsPluginFactory from "./src/api/scripts.js"; +import workflowsPluginFactory from "./src/api/workflows.js"; +import runsPlugin from "./src/api/runs.js"; +import dashboardPluginFactory from "./src/api/dashboard.js"; +import { WEB_DIST } from "./paths.js"; await migrate(); enableLogPersistence(); -/** - * @typedef {{ owner: string, file: string, workflow: any }} WorkflowEntry - */ +const jwtSecret = + process.env.SCRUNNER_JWT_SECRET ?? + (process.env.NODE_ENV === "production" ? "" : "scrunner-dev-secret"); -/** - * @type {Map} - */ -const workflows = new Map(); - -/** - * @type {import("node-cron").ScheduledTask[]} - */ -const cronTasks = []; - -/** - * @type {import("node-cron").ScheduledTask | null} - */ -let pruneTask = null; - -/** - * @type {Set} - */ -const registeredHttpRoutes = new Set(); - -function parseScriptStep(step) { - if (typeof step === "string") return { script: step, config: null }; - if (step?.script) return { script: step.script, config: step.config ?? null }; - throw new Error(`Invalid script step: ${JSON.stringify(step)}`); +if (!jwtSecret) { + log.error("SCRUNNER_JWT_SECRET is required in production"); + process.exit(1); } -/** - * Resolve an HTTP path under the owner namespace: /notify -> /u/alice/notify - * @param {string} owner - * @param {string} triggerPath - */ -function namespacedPath(owner, triggerPath) { - const cleaned = String(triggerPath).replace(/^\/+/, ""); - return `/u/${owner}/${cleaned}`; -} - -function registerWorkflows() { - workflows.clear(); - clearScriptCache(); - - const workflowsRoot = "workflows"; - if (!fs.existsSync(workflowsRoot)) { - log.warn("workflows directory missing"); - return; - } - - const owners = fs - .readdirSync(workflowsRoot, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name); - - for (const owner of owners) { - const registersPath = path.join(workflowsRoot, owner, "registers.yaml"); - if (!fs.existsSync(registersPath)) { - log.warn(`Skipping owner "${owner}": no registers.yaml`); - continue; - } - - let workflowFiles = []; - try { - const registerData = fs.readFileSync(registersPath, "utf8"); - const parsed = yaml.parse(registerData) ?? {}; - workflowFiles = parsed.scripts ?? []; - } catch (err) { - log.error({ err, owner }, "failed to parse registers.yaml"); - continue; - } - - const ownerDir = path.join(workflowsRoot, owner); - const onDisk = fs - .readdirSync(ownerDir) - .filter((f) => f.endsWith(".yaml") && f !== "registers.yaml"); - for (const file of onDisk) { - if (!workflowFiles.includes(file)) { - log.warn( - `Workflow file not in registers.yaml: ${owner}/${file}`, - ); - } - } - - for (const file of workflowFiles) { - const key = `${owner}/${file}`; - const filePath = path.join(ownerDir, file); - try { - const workflowData = fs.readFileSync(filePath, "utf8"); - const workflow = yaml.parse(workflowData); - workflows.set(key, { owner, file, workflow }); - } catch (err) { - log.error({ err, workflow: key }, "failed to load workflow; skipping"); - } - } - } - - log.debug({ count: workflows.size }, "workflows loaded"); -} - -function registerHttpTriggers() { - const seen = new Set(); - - for (const [key, { owner, workflow }] of workflows) { - if (workflow.enabled === false) { - log.debug(`Skipping disabled workflow HTTP triggers (${key})`); - continue; - } - - for (const trigger of workflow.triggers ?? []) { - if (trigger.type !== "HTTP") continue; - - const method = String(trigger.method ?? "POST").toUpperCase(); - const url = namespacedPath(owner, trigger.path); - const routeKey = `${method} ${url}`; - - if (seen.has(routeKey)) { - log.warn(`Skipping duplicate HTTP trigger ${routeKey} (${key})`); - continue; - } - seen.add(routeKey); - - if (registeredHttpRoutes.has(routeKey)) { - continue; - } - registeredHttpRoutes.add(routeKey); - - server.route({ - method, - url, - handler: async (req, reply) => { - const result = await runWorkflow( - key, - { data: req.body }, - { type: "http", detail: `${method} ${url}` }, - ); - if (result.status === "failed") { - return reply.code(500).send({ - runId: result.runId, - error: result.error, - }); - } - return reply.send({ - runId: result.runId, - result: result.result, - }); - }, - }); - log.debug(`Registered HTTP trigger ${routeKey} (${key})`); - } - } -} - -function registerCronTriggers() { - for (const task of cronTasks) { - task.destroy(); - } - cronTasks.length = 0; - - for (const [key, { workflow }] of workflows) { - if (workflow.enabled === false) { - log.debug(`Skipping disabled workflow cron triggers (${key})`); - continue; - } - - for (const trigger of workflow.triggers ?? []) { - if (trigger.type !== "cron") continue; - - const schedule = trigger.schedule; - if (!schedule || !cron.validate(schedule)) { - log.warn(`Skipping invalid cron schedule "${schedule}" (${key})`); - continue; - } - - const task = cron.schedule( - schedule, - () => { - log.debug(`cron firing ${key} (${schedule})`); - return runWorkflow( - key, - { data: workflow.data ?? null }, - { type: "cron", detail: schedule }, - ); - }, - { name: `${key}:${schedule}`, noOverlap: true }, - ); - cronTasks.push(task); - log.debug(`Registered cron trigger ${schedule} (${key})`); - } - } -} - -function registerPruneJob() { - if (pruneTask) { - pruneTask.destroy(); - pruneTask = null; - } - const days = Number(process.env.SCRUNNER_RETENTION_DAYS ?? 30); - pruneTask = cron.schedule( - "0 0 * * *", - async () => { - try { - const deleted = await store.pruneOlderThan(days); - log.info({ deleted, days }, "pruned old workflow runs"); - } catch (err) { - log.error({ err }, "failed to prune old workflow runs"); - } - }, - { name: "prune-runs" }, - ); -} - -/** - * @param {string} key - * @param {{ data?: unknown }} context - * @param {{ type: string, detail?: string | null }} trigger - */ -async function runWorkflow(key, context, trigger) { - const entry = workflows.get(key); - if (!entry) { - log.error({ workflow: key }, "workflow not found"); - return { runId: null, status: "failed", error: "workflow not found" }; - } - - const { owner, workflow } = entry; - const run = await store.startRun({ - owner, - workflow: key, - workflowName: workflow?.name, - trigger, - input: context.data, - }); - const runLog = log.child({ runId: run.id, owner, workflow: key }); - runLog.debug("running workflow"); - - let ctx = { - ...context, - data: context.data ?? workflow.data ?? null, - }; - - try { - for (const [index, rawStep] of (workflow.scripts ?? []).entries()) { - const { script, config } = parseScriptStep(rawStep); - const step = await store.startStep({ - runId: run.id, - index, - script, - config, - }); - const stepLog = runLog.child({ stepId: step.id, script }); - try { - ctx = await runScript(script, { ...ctx, config }, { - log: stepLog, - workflowName: key, - }); - await store.finishStep(step.id, "success", ctx); - } catch (err) { - await store.finishStep(step.id, "failed", null, err); - throw err; - } - } - await store.finishRun(run.id, "success", ctx); - return { runId: run.id, status: "success", result: ctx }; - } catch (err) { - runLog.error({ err }, "workflow failed"); - await store.finishRun(run.id, "failed", null, err); - return { - runId: run.id, - status: "failed", - error: err instanceof Error ? err.message : String(err), - }; - } -} - -registerWorkflows(); - const server = fastify({ loggerInstance: log }); -registerHttpTriggers(); -registerCronTriggers(); -registerPruneJob(); - -server.post("/admin/workflows/reregister", async (_req, reply) => { - registerWorkflows(); - // Fastify cannot remove routes; new routes are added, cron is rebuilt. - registerHttpTriggers(); - registerCronTriggers(); - return reply.send({ message: "Workflows refreshed" }); +await server.register(cookie); +await server.register(jwt, { + secret: jwtSecret, + cookie: { + cookieName: COOKIE, + signed: false, + }, +}); +await server.register(cors, { + origin: process.env.SCRUNNER_CORS_ORIGIN ?? "http://localhost:5173", + credentials: true, }); -server.get("/admin/runs", async (req, reply) => { - const q = /** @type {Record} */ (req.query); - const limit = q.limit ? Number(q.limit) : undefined; - const runs = await store.listRuns({ - owner: q.owner, - workflow: q.workflow, - status: q.status, - limit: Number.isFinite(limit) ? limit : undefined, - before: q.before, - }); - return reply.send({ runs }); -}); - -server.get("/admin/runs/:id", async (req, reply) => { - const { id } = /** @type {{ id: string }} */ (req.params); - const run = await store.getRun(id); - if (!run) { - return reply.code(404).send({ error: "run not found" }); +server.decorate("authenticate", async function authenticate(req, reply) { + try { + await req.jwtVerify(); + } catch { + return reply.code(401).send({ error: "unauthorized" }); } - return reply.send(run); }); +server.decorate("requireAdmin", async function requireAdmin(req, reply) { + if (req.user?.role !== "admin") { + return reply.code(403).send({ error: "forbidden" }); + } +}); + +const registry = createRegistry(server); +registry.registerWorkflows(); +registry.registerHttpTriggers(); +registry.registerCronTriggers(); +registry.registerPruneJob(); + +await server.register( + async (api) => { + api.addHook("onRequest", async (req, reply) => { + const raw = (req.url || "").split("?")[0]; + const stripped = raw.replace(/^\/api/, "") || "/"; + const routeUrl = req.routeOptions?.url || stripped; + const open = + OPEN_API_ROUTES.has(`${req.method} ${routeUrl}`) || + OPEN_API_ROUTES.has(`${req.method} ${stripped}`); + if (open) return; + await server.authenticate(req, reply); + }); + await api.register(authPlugin); + await api.register(usersPlugin); + await api.register(scriptsPluginFactory(registry)); + await api.register(workflowsPluginFactory(registry)); + await api.register(runsPlugin); + await api.register(dashboardPluginFactory(registry)); + }, + { prefix: "/api" }, +); + +server.post( + "/admin/workflows/reregister", + { onRequest: [server.authenticate] }, + async (_req, reply) => { + registry.reregister(); + return reply.send({ message: "Workflows refreshed" }); + }, +); + +server.get( + "/admin/runs", + { onRequest: [server.authenticate] }, + async (req, reply) => { + const q = /** @type {Record} */ (req.query); + const limit = q.limit ? Number(q.limit) : undefined; + const runs = await store.listRuns({ + owner: q.owner, + workflow: q.workflow, + status: q.status, + limit: Number.isFinite(limit) ? limit : undefined, + before: q.before, + }); + return reply.send({ runs }); + }, +); + +server.get( + "/admin/runs/:id", + { onRequest: [server.authenticate] }, + async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const run = await store.getRun(id); + if (!run) { + return reply.code(404).send({ error: "run not found" }); + } + return reply.send(run); + }, +); + +if (fs.existsSync(WEB_DIST)) { + await server.register(fastifyStatic, { + root: WEB_DIST, + wildcard: false, + }); + server.setNotFoundHandler((req, reply) => { + const url = req.raw.url ?? ""; + if ( + url.startsWith("/api") || + url.startsWith("/u/") || + url.startsWith("/admin") + ) { + return reply.code(404).send({ error: "not found" }); + } + return reply.sendFile("index.html"); + }); +} + async function shutdown() { try { await flushLogs(); @@ -336,13 +156,15 @@ async function shutdown() { process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); +const port = Number(process.env.PORT ?? 9000); + server .listen({ host: "0.0.0.0", - port: 9000, + port, }) .then(() => { - log.info("Server is running on port 9000"); + log.info(`Server is running on port ${port}`); }) .catch((err) => { log.error({ err }, "failed to start server"); diff --git a/packages/server/script-sandbox.js b/packages/server/script-sandbox.js index e565967..9573bbc 100644 --- a/packages/server/script-sandbox.js +++ b/packages/server/script-sandbox.js @@ -1,8 +1,9 @@ import fs from "fs"; +import path from "path"; import vm from "node:vm"; -import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import axios from "axios"; +import { SCRIPTS_DIR } from "./paths.js"; const hostRequire = createRequire(import.meta.url); @@ -291,7 +292,7 @@ function createScriptSandbox({ log, script, workflowName }) { } function loadCompiledScript(script) { - const filePath = fileURLToPath(new URL(`./scripts/${script}`, import.meta.url)); + const filePath = path.join(SCRIPTS_DIR, script); const { mtimeMs } = fs.statSync(filePath); const cached = scriptCache.get(script); if (cached && cached.mtimeMs === mtimeMs) { diff --git a/packages/server/scripts/jsonata.js b/packages/server/scripts/jsonata.js index b0177e5..0f6506e 100644 --- a/packages/server/scripts/jsonata.js +++ b/packages/server/scripts/jsonata.js @@ -1,6 +1,10 @@ import jsonata from "jsonata"; -export default function jsonataFn(context) { - const expression = jsonata(context.config.expression); - return expression.evaluate(context.data) +export default function jsonataFn(ctx) { + log.info({ctx}, "jsonata: context") + log.info("jsonata: evaluating expression %s", ctx.config.expression); + const expression = jsonata(ctx.config.expression); + const result = expression.evaluate(ctx.data); + log.info({result}, "jsonata: expression result"); + return result; } \ No newline at end of file diff --git a/packages/server/scripts/ntfy.js b/packages/server/scripts/ntfy.js index 5e50d57..065c4fe 100644 --- a/packages/server/scripts/ntfy.js +++ b/packages/server/scripts/ntfy.js @@ -1,12 +1,19 @@ export default async function ntfy(ctx) { - log.info({ ctx }, "ntfy"); + log.info({ ctx }, "ntfy incoming context"); const headers = {} if(ctx.data?.title) { + log.info("ntfy: setting title %s", ctx.data.title); headers.Title = ctx.data.title } - await $axios.post(ctx.config?.url || "https://ntfy.sh/scrunner", ctx.data?.message || "Hello from scrunner", { + const ntfyUrl = ctx.config?.url || "https://ntfy.sh/scrunner"; + + log.info("ntfy sending message to %s", ntfyUrl); + const truncatedMessage = ctx.data?.message?.substring(0, 100); + log.info("ntfy messsage: %s", truncatedMessage); + + await $axios.post(ntfyUrl, ctx.data?.message || "Hello from scrunner", { headers: headers }) return {sent: "true"} diff --git a/packages/server/src/api/auth.js b/packages/server/src/api/auth.js new file mode 100644 index 0000000..d877c3d --- /dev/null +++ b/packages/server/src/api/auth.js @@ -0,0 +1,103 @@ +import bcrypt from "bcryptjs"; +import * as store from "../../store.js"; + +export const COOKIE = "scrunner_token"; +export const OPEN_API_ROUTES = new Set([ + "GET /auth/bootstrap", + "POST /auth/register", + "POST /auth/login", +]); + +export function cookieOpts() { + return { + httpOnly: true, + path: "/", + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + maxAge: 7 * 24 * 60 * 60, + }; +} + +export function validateCredentials(username, password) { + if (!/^[A-Za-z0-9_]{3,32}$/.test(username)) { + return "username must be 3-32 letters, numbers, or underscore"; + } + if (password.length < 8) { + return "password must be at least 8 characters"; + } + return null; +} + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function authPlugin(fastify) { + fastify.get("/auth/bootstrap", async () => { + const count = await store.countUsers(); + return { needsSetup: count === 0 }; + }); + + fastify.post("/auth/register", async (req, reply) => { + const count = await store.countUsers(); + if (count > 0) { + return reply.code(403).send({ error: "setup already complete" }); + } + const body = /** @type {{ username?: string, password?: string }} */ (req.body ?? {}); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const err = validateCredentials(username, password); + if (err) return reply.code(400).send({ error: err }); + + const passwordHash = await bcrypt.hash(password, 10); + const user = await store.createUser({ + username, + passwordHash, + role: "admin", + }); + const token = await reply.jwtSign({ + id: user.id, + username: user.username, + role: user.role, + }); + reply.setCookie(COOKIE, token, cookieOpts()); + return { user }; + }); + + fastify.post("/auth/login", async (req, reply) => { + const body = /** @type {{ username?: string, password?: string }} */ (req.body ?? {}); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const row = await store.getUserAuthByUsername(username); + if (!row || !(await bcrypt.compare(password, row.password_hash))) { + return reply.code(401).send({ error: "invalid credentials" }); + } + const token = await reply.jwtSign({ + id: row.id, + username: row.username, + role: row.role, + }); + reply.setCookie(COOKIE, token, cookieOpts()); + return { + user: { + id: row.id, + username: row.username, + role: row.role, + created_at: row.created_at, + updated_at: row.updated_at, + }, + }; + }); + + fastify.post("/auth/logout", async (_req, reply) => { + reply.clearCookie(COOKIE, { path: "/" }); + return { ok: true }; + }); + + fastify.get("/auth/me", async (req, reply) => { + const user = await store.getUserById(req.user.id); + if (!user) { + return reply.code(401).send({ error: "unauthorized" }); + } + return { user }; + }); +} diff --git a/packages/server/src/api/dashboard.js b/packages/server/src/api/dashboard.js new file mode 100644 index 0000000..fe3831e --- /dev/null +++ b/packages/server/src/api/dashboard.js @@ -0,0 +1,86 @@ +import yaml from "yaml"; +import * as store from "../../store.js"; +import * as fsStore from "../../fs-store.js"; + +/** + * @param {{ workflows: Map, loadErrors: Map }} registry + */ +export default function dashboardPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function dashboardPlugin(fastify) { + fastify.get("/dashboard", async () => { + const owners = fsStore.listOwners(); + let workflowCount = 0; + let enabledCount = 0; + let brokenCount = registry.loadErrors.size; + const brokenWorkflows = []; + + for (const [key, message] of registry.loadErrors) { + const [owner, ...rest] = key.split("/"); + brokenWorkflows.push({ + key, + owner, + file: rest.join("/"), + loadError: message, + }); + } + + for (const owner of owners) { + let registered = []; + try { + registered = fsStore.readRegisters(owner); + } catch { + registered = []; + } + const files = [ + ...new Set([...registered, ...fsStore.listOwnerYamlFiles(owner)]), + ]; + for (const file of files) { + workflowCount += 1; + const key = `${owner}/${file}`; + const loaded = registry.workflows.get(key); + if (loaded?.workflow && loaded.workflow.enabled !== false) { + enabledCount += 1; + } else if (!loaded && !registry.loadErrors.has(key)) { + const raw = fsStore.readWorkflowYaml(owner, file); + if (raw) { + try { + const parsed = yaml.parse(raw); + if (parsed?.enabled !== false) enabledCount += 1; + } catch (err) { + brokenCount += 1; + brokenWorkflows.push({ + key, + owner, + file, + loadError: err instanceof Error ? err.message : String(err), + }); + } + } + } + } + } + + const [running, failed, recent] = await Promise.all([ + store.listRuns({ status: "running", limit: 10 }), + store.listRuns({ status: "failed", limit: 20 }), + store.listRuns({ limit: 10 }), + ]); + + return { + workflowCount, + scriptCount: fsStore.listScriptFiles().length, + enabledCount, + brokenCount, + running, + needsAttention: { + failed, + brokenWorkflows, + }, + recent, + }; + }); + }; +} diff --git a/packages/server/src/api/runs.js b/packages/server/src/api/runs.js new file mode 100644 index 0000000..570e707 --- /dev/null +++ b/packages/server/src/api/runs.js @@ -0,0 +1,26 @@ +import * as store from "../../store.js"; + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function runsPlugin(fastify) { + fastify.get("/runs", async (req) => { + const q = /** @type {Record} */ (req.query ?? {}); + const limit = q.limit ? Number(q.limit) : undefined; + const runs = await store.listRuns({ + owner: q.owner, + workflow: q.workflow, + status: q.status, + limit: Number.isFinite(limit) ? limit : undefined, + before: q.before, + }); + return { runs }; + }); + + fastify.get("/runs/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const run = await store.getRun(id); + if (!run) return reply.code(404).send({ error: "run not found" }); + return run; + }); +} diff --git a/packages/server/src/api/scripts.js b/packages/server/src/api/scripts.js new file mode 100644 index 0000000..4c06684 --- /dev/null +++ b/packages/server/src/api/scripts.js @@ -0,0 +1,64 @@ +import { clearScriptCache } from "../../script-sandbox.js"; +import * as fsStore from "../../fs-store.js"; + +/** + * @param {{ referencedScripts: () => Set }} registry + */ +export default function scriptsPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function scriptsPlugin(fastify) { + fastify.get("/scripts", async () => { + return { scripts: fsStore.listScriptFiles() }; + }); + + fastify.get("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const content = fsStore.readScript(name); + if (content == null) return reply.code(404).send({ error: "script not found" }); + return { name, content }; + }); + + fastify.put("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const body = /** @type {{ content?: string }} */ (req.body ?? {}); + if (typeof body.content !== "string") { + return reply.code(400).send({ error: "content is required" }); + } + const existed = fsStore.readScript(name) != null; + fsStore.writeScript(name, body.content); + clearScriptCache(); + return reply.code(existed ? 200 : 201).send({ name }); + }); + + fastify.delete("/scripts/:name", async (req, reply) => { + const { name } = /** @type {{ name: string }} */ (req.params); + try { + fsStore.assertScriptName(name); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + if (registry.referencedScripts().has(name)) { + return reply + .code(409) + .send({ error: "script is referenced by a workflow" }); + } + if (!fsStore.deleteScript(name)) { + return reply.code(404).send({ error: "script not found" }); + } + clearScriptCache(); + return { ok: true }; + }); + }; +} diff --git a/packages/server/src/api/users.js b/packages/server/src/api/users.js new file mode 100644 index 0000000..1255d48 --- /dev/null +++ b/packages/server/src/api/users.js @@ -0,0 +1,85 @@ +import bcrypt from "bcryptjs"; +import * as store from "../../store.js"; +import { validateCredentials } from "./auth.js"; + +/** + * @param {import("fastify").FastifyInstance} fastify + */ +export default async function usersPlugin(fastify) { + fastify.addHook("onRequest", fastify.requireAdmin); + + fastify.get("/users", async () => { + const users = await store.listUsers(); + return { users }; + }); + + fastify.post("/users", async (req, reply) => { + const body = /** @type {{ username?: string, password?: string, role?: string }} */ ( + req.body ?? {} + ); + const username = String(body.username ?? "").trim(); + const password = String(body.password ?? ""); + const role = body.role === "admin" ? "admin" : "operator"; + const err = validateCredentials(username, password); + if (err) return reply.code(400).send({ error: err }); + + const existing = await store.getUserAuthByUsername(username); + if (existing) { + return reply.code(409).send({ error: "username taken" }); + } + + const user = await store.createUser({ + username, + passwordHash: await bcrypt.hash(password, 10), + role, + }); + return reply.code(201).send({ user }); + }); + + fastify.patch("/users/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const existing = await store.getUserAuthById(id); + if (!existing) return reply.code(404).send({ error: "user not found" }); + + const body = /** @type {{ password?: string, role?: string }} */ (req.body ?? {}); + /** @type {{ passwordHash?: string, role?: string }} */ + const patch = {}; + + if (body.role) { + if (body.role !== "admin" && body.role !== "operator") { + return reply.code(400).send({ error: "invalid role" }); + } + if (existing.role === "admin" && body.role !== "admin") { + const admins = await store.countAdmins(); + if (admins <= 1) { + return reply.code(400).send({ error: "cannot demote last admin" }); + } + } + patch.role = body.role; + } + + if (body.password) { + if (body.password.length < 8) { + return reply.code(400).send({ error: "password must be at least 8 characters" }); + } + patch.passwordHash = await bcrypt.hash(body.password, 10); + } + + const user = await store.updateUser(id, patch); + return { user }; + }); + + fastify.delete("/users/:id", async (req, reply) => { + const { id } = /** @type {{ id: string }} */ (req.params); + const existing = await store.getUserAuthById(id); + if (!existing) return reply.code(404).send({ error: "user not found" }); + if (existing.role === "admin") { + const admins = await store.countAdmins(); + if (admins <= 1) { + return reply.code(400).send({ error: "cannot delete last admin" }); + } + } + await store.deleteUser(id); + return { ok: true }; + }); +} diff --git a/packages/server/src/api/workflows.js b/packages/server/src/api/workflows.js new file mode 100644 index 0000000..adc66ab --- /dev/null +++ b/packages/server/src/api/workflows.js @@ -0,0 +1,240 @@ +import yaml from "yaml"; +import * as store from "../../store.js"; +import * as fsStore from "../../fs-store.js"; +import { namespacedPath, parseScriptStep } from "../../workflow-parse.js"; + +function triggerSummary(owner, workflow) { + if (!workflow || typeof workflow !== "object") return []; + return (workflow.triggers ?? []).map((t) => { + const type = t?.type ?? "unknown"; + const isHttp = String(type).toLowerCase() === "http"; + return { + type, + method: isHttp ? String(t?.method ?? "POST").toUpperCase() : t?.method ?? null, + path: isHttp && t?.path != null ? namespacedPath(owner, t.path) : t?.path ?? null, + schedule: t?.schedule ?? null, + }; + }); +} + +function scriptNames(workflow) { + if (!workflow || typeof workflow !== "object") return []; + const names = []; + for (const raw of workflow.scripts ?? []) { + try { + names.push(parseScriptStep(raw).script); + } catch { + names.push(null); + } + } + return names; +} + +/** + * @param {{ workflows: Map, loadErrors: Map, reregister: () => void }} registry + */ +export default function workflowsPluginFactory(registry) { + /** + * @param {import("fastify").FastifyInstance} fastify + */ + return async function workflowsPlugin(fastify) { + fastify.get("/owners", async () => { + return { owners: fsStore.listOwners() }; + }); + + fastify.get("/workflows", async (req) => { + const q = /** @type {{ owner?: string }} */ (req.query ?? {}); + const stats = await store.workflowStats(); + const owners = q.owner + ? [fsStore.assertOwner(q.owner)] + : fsStore.listOwners(); + + const items = []; + for (const owner of owners) { + let registered = []; + try { + registered = fsStore.readRegisters(owner); + } catch { + registered = []; + } + const onDisk = fsStore.listOwnerYamlFiles(owner); + const files = [...new Set([...registered, ...onDisk])]; + + for (const file of files) { + const key = `${owner}/${file}`; + const loaded = registry.workflows.get(key); + const loadError = registry.loadErrors.get(key) ?? null; + let parsed = loaded?.workflow ?? null; + if (!parsed) { + const raw = fsStore.readWorkflowYaml(owner, file); + if (raw != null) { + try { + parsed = yaml.parse(raw); + } catch (err) { + // keep loadError + if (!loadError) { + // file on disk but unparseable and not in registers + } + } + } + } + const st = stats[key] ?? { + invocationCount: 0, + lastInvokedAt: null, + lastStatus: null, + }; + items.push({ + owner, + file, + key, + name: parsed?.name ?? file, + description: parsed?.description ?? null, + enabled: parsed ? parsed.enabled !== false : false, + registered: registered.includes(file), + loadError: + loadError ?? + (parsed ? null : "unreadable"), + lastInvokedAt: st.lastInvokedAt, + lastStatus: st.lastStatus ?? null, + invocationCount: st.invocationCount, + triggers: triggerSummary(owner, parsed), + scripts: scriptNames(parsed), + }); + } + } + return { workflows: items }; + }); + + fastify.get("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const content = fsStore.readWorkflowYaml(owner, file); + if (content == null) { + return reply.code(404).send({ error: "workflow not found" }); + } + const key = `${owner}/${file}`; + let parsed = null; + let parseError = null; + try { + parsed = yaml.parse(content); + } catch (err) { + parseError = err instanceof Error ? err.message : String(err); + } + const stats = (await store.workflowStats())[key] ?? { + invocationCount: 0, + lastInvokedAt: null, + lastStatus: null, + }; + return { + owner, + file, + key, + content, + parsed, + parseError, + loadError: registry.loadErrors.get(key) ?? parseError, + lastInvokedAt: stats.lastInvokedAt, + lastStatus: stats.lastStatus ?? null, + invocationCount: stats.invocationCount, + }; + }); + + fastify.put("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const body = /** @type {{ content?: string }} */ (req.body ?? {}); + if (typeof body.content !== "string") { + return reply.code(400).send({ error: "content is required" }); + } + try { + yaml.parse(body.content); + } catch (err) { + return reply.code(400).send({ + error: `invalid yaml: ${err instanceof Error ? err.message : String(err)}`, + }); + } + const existed = fsStore.readWorkflowYaml(owner, file) != null; + fsStore.writeWorkflowYaml(owner, file, body.content); + const registered = fsStore.readRegisters(owner); + if (!registered.includes(file)) { + registered.push(file); + fsStore.writeRegisters(owner, registered); + } + registry.reregister(); + return reply.code(existed ? 200 : 201).send({ owner, file }); + }); + + fastify.delete("/workflows/:owner/:file", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + if (!fsStore.deleteWorkflowYaml(owner, file)) { + return reply.code(404).send({ error: "workflow not found" }); + } + const registered = fsStore.readRegisters(owner).filter((f) => f !== file); + fsStore.writeRegisters(owner, registered); + registry.reregister(); + return { ok: true }; + }); + + fastify.post("/workflows/:owner/:file/run", async (req, reply) => { + const { owner, file } = /** @type {{ owner: string, file: string }} */ ( + req.params + ); + try { + fsStore.assertOwner(owner); + fsStore.assertWorkflowFile(file); + } catch (err) { + return reply.code(err.statusCode ?? 400).send({ error: err.message }); + } + const key = `${owner}/${file}`; + if (!registry.workflows.has(key)) { + return reply.code(404).send({ + error: registry.loadErrors.get(key) ?? "workflow not loaded", + }); + } + const body = /** @type {{ data?: unknown }} */ (req.body ?? {}); + const result = await registry.runWorkflow( + key, + { data: body.data ?? null }, + { type: "manual", detail: "ui" }, + ); + if (result.status === "failed") { + return reply.code(result.runId ? 500 : 404).send({ + runId: result.runId, + error: result.error, + }); + } + return { + runId: result.runId, + status: result.status, + result: result.result, + }; + }); + + fastify.post("/workflows/reregister", async () => { + registry.reregister(); + return { message: "Workflows refreshed" }; + }); + }; +} diff --git a/packages/server/store.js b/packages/server/store.js index 38a7bea..6c76017 100644 --- a/packages/server/store.js +++ b/packages/server/store.js @@ -235,3 +235,102 @@ export async function pruneOlderThan(days) { const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); return db("workflow_runs").where("started_at", "<", cutoff).del(); } + +/** + * @returns {Promise>} + */ +export async function workflowStats() { + const rows = await db("workflow_runs") + .select("workflow", "status", "started_at") + .orderBy("started_at", "desc"); + + /** @type {Record} */ + const out = {}; + for (const row of rows) { + const existing = out[row.workflow]; + if (!existing) { + out[row.workflow] = { + invocationCount: 1, + lastInvokedAt: row.started_at ?? null, + lastStatus: row.status ?? null, + }; + } else { + existing.invocationCount += 1; + } + } + return out; +} + +export async function countUsers() { + const row = await db("users").count({ n: "*" }).first(); + return Number(row?.n ?? 0); +} + +export async function countAdmins() { + const row = await db("users").where({ role: "admin" }).count({ n: "*" }).first(); + return Number(row?.n ?? 0); +} + +/** + * @param {{ username: string, passwordHash: string, role: string }} opts + */ +export async function createUser({ username, passwordHash, role }) { + const id = randomUUID(); + const now = nowIso(); + await db("users").insert({ + id, + username, + password_hash: passwordHash, + role, + created_at: now, + updated_at: now, + }); + return getUserById(id); +} + +export async function getUserById(id) { + const row = await db("users").where({ id }).first(); + return row ? publicUser(row) : null; +} + +export async function getUserAuthByUsername(username) { + return db("users").where({ username }).first(); +} + +export async function getUserAuthById(id) { + return db("users").where({ id }).first(); +} + +export async function listUsers() { + const rows = await db("users") + .select("id", "username", "role", "created_at", "updated_at") + .orderBy("username", "asc"); + return rows; +} + +/** + * @param {string} id + * @param {{ passwordHash?: string, role?: string }} patch + */ +export async function updateUser(id, patch) { + const update = { updated_at: nowIso() }; + if (patch.passwordHash) update.password_hash = patch.passwordHash; + if (patch.role) update.role = patch.role; + await db("users").where({ id }).update(update); + return getUserById(id); +} + +export async function deleteUser(id) { + return db("users").where({ id }).del(); +} + +function publicUser(row) { + return { + id: row.id, + username: row.username, + role: row.role, + created_at: row.created_at, + updated_at: row.updated_at, + }; +} + diff --git a/packages/server/workflow-parse.js b/packages/server/workflow-parse.js new file mode 100644 index 0000000..b144c62 --- /dev/null +++ b/packages/server/workflow-parse.js @@ -0,0 +1,15 @@ +export function parseScriptStep(step) { + if (typeof step === "string") return { script: step, config: null }; + if (step?.script) return { script: step.script, config: step.config ?? null }; + throw new Error(`Invalid script step: ${JSON.stringify(step)}`); +} + +/** + * Resolve an HTTP path under the owner namespace: /notify -> /u/alice/notify + * @param {string} owner + * @param {string} triggerPath + */ +export function namespacedPath(owner, triggerPath) { + const cleaned = String(triggerPath).replace(/^\/+/, ""); + return `/u/${owner}/${cleaned}`; +} diff --git a/packages/server/workflows/default/manual-trigger.yaml b/packages/server/workflows/default/manual-trigger.yaml index 781cdbc..fd5307d 100644 --- a/packages/server/workflows/default/manual-trigger.yaml +++ b/packages/server/workflows/default/manual-trigger.yaml @@ -6,4 +6,5 @@ triggers: - type: HTTP method: POST path: /mt + - type: manual diff --git a/packages/server/workflows/default/test.yaml b/packages/server/workflows/default/test.yaml index b7b273e..871e1b7 100644 --- a/packages/server/workflows/default/test.yaml +++ b/packages/server/workflows/default/test.yaml @@ -1,4 +1,4 @@ -name: manual trigger +name: jsonata scripts: - get-current-time.js - script: jsonata.js diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000..41012af --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,12 @@ + + + + + + scrunner + + +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..b22974e --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@scrunner/web", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tanstack/react-query": "^5.84.0", + "axios": "^1.11.0", + "mermaid": "^11.9.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-icons": "^5.5.0", + "react-router-dom": "^7.8.0", + "yaml": "^2.8.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.11", + "@vitejs/plugin-react": "^5.0.0", + "daisyui": "^5.0.50", + "tailwindcss": "^4.1.11", + "vite": "^7.1.2" + } +} diff --git a/packages/web/src/App.jsx b/packages/web/src/App.jsx new file mode 100644 index 0000000..1ac8ed1 --- /dev/null +++ b/packages/web/src/App.jsx @@ -0,0 +1,60 @@ +import { useEffect } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; +import { useBootstrap, useMe } from "./api/hooks.js"; +import { Layout } from "./components/Layout.jsx"; +import { AuthPage } from "./pages/AuthPage.jsx"; +import { HomePage } from "./pages/HomePage.jsx"; +import { ScriptsPage } from "./pages/ScriptsPage.jsx"; +import { WorkflowsPage } from "./pages/WorkflowsPage.jsx"; +import { EventsPage } from "./pages/EventsPage.jsx"; +import { EventDetailPage } from "./pages/EventDetailPage.jsx"; +import { UsersPage } from "./pages/UsersPage.jsx"; + +export function App() { + const qc = useQueryClient(); + const bootstrap = useBootstrap(); + const me = useMe(); + + useEffect(() => { + function onUnauthorized() { + qc.setQueryData(["me"], null); + } + window.addEventListener("scrunner:unauthorized", onUnauthorized); + return () => window.removeEventListener("scrunner:unauthorized", onUnauthorized); + }, [qc]); + + const user = me.data?.user; + const loading = + bootstrap.isLoading || (me.isLoading && !me.isError && me.data === undefined); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ; + } + + return ( + + + } /> + } /> + } /> + } /> + } /> + {user.role === "admin" ? ( + } /> + ) : ( + } /> + )} + } /> + + + ); +} diff --git a/packages/web/src/api/client.js b/packages/web/src/api/client.js new file mode 100644 index 0000000..14ab21e --- /dev/null +++ b/packages/web/src/api/client.js @@ -0,0 +1,22 @@ +import axios from "axios"; + +export const api = axios.create({ + baseURL: "/api", + withCredentials: true, +}); + +api.interceptors.response.use( + (res) => res, + (err) => { + const url = err.config?.url ?? ""; + const isAuthCall = url.includes("/auth/login") || url.includes("/auth/register") || url.includes("/auth/me") || url.includes("/auth/bootstrap"); + if (err.response?.status === 401 && !isAuthCall) { + window.dispatchEvent(new Event("scrunner:unauthorized")); + } + return Promise.reject(err); + }, +); + +export function errorMessage(err, fallback = "request failed") { + return err?.response?.data?.error ?? err?.message ?? fallback; +} diff --git a/packages/web/src/api/hooks.js b/packages/web/src/api/hooks.js new file mode 100644 index 0000000..44517cd --- /dev/null +++ b/packages/web/src/api/hooks.js @@ -0,0 +1,256 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "./client.js"; + +export function useBootstrap() { + return useQuery({ + queryKey: ["bootstrap"], + queryFn: async () => (await api.get("/auth/bootstrap")).data, + }); +} + +export function useMe() { + return useQuery({ + queryKey: ["me"], + queryFn: async () => { + try { + return (await api.get("/auth/me")).data; + } catch (err) { + if (err.response?.status === 401) return { user: null }; + throw err; + } + }, + retry: false, + }); +} + +export function useLogin() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/auth/login", body)).data, + onSuccess: (data) => { + qc.setQueryData(["me"], data); + qc.invalidateQueries({ queryKey: ["bootstrap"] }); + }, + }); +} + +export function useRegister() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/auth/register", body)).data, + onSuccess: (data) => { + qc.setQueryData(["me"], data); + qc.invalidateQueries({ queryKey: ["bootstrap"] }); + }, + }); +} + +export function useLogout() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async () => (await api.post("/auth/logout")).data, + onSuccess: () => { + qc.setQueryData(["me"], null); + qc.clear(); + }, + }); +} + +export function useDashboard() { + return useQuery({ + queryKey: ["dashboard"], + queryFn: async () => (await api.get("/dashboard")).data, + refetchInterval: (query) => + query.state.data?.running?.length ? 4000 : 15000, + }); +} + +export function useScripts() { + return useQuery({ + queryKey: ["scripts"], + queryFn: async () => (await api.get("/scripts")).data.scripts, + }); +} + +export function useScript(name, enabled = true) { + return useQuery({ + queryKey: ["scripts", name], + queryFn: async () => (await api.get(`/scripts/${encodeURIComponent(name)}`)).data, + enabled: Boolean(name) && enabled, + }); +} + +export function useSaveScript() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ name, content }) => + (await api.put(`/scripts/${encodeURIComponent(name)}`, { content })).data, + onSuccess: (_data, vars) => { + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["scripts", vars.name] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useDeleteScript() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (name) => + (await api.delete(`/scripts/${encodeURIComponent(name)}`)).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useWorkflows(owner) { + return useQuery({ + queryKey: ["workflows", owner ?? "all"], + queryFn: async () => { + const params = owner ? { owner } : {}; + return (await api.get("/workflows", { params })).data.workflows; + }, + }); +} + +export function useWorkflow(owner, file, enabled = true) { + return useQuery({ + queryKey: ["workflows", owner, file], + queryFn: async () => + (await api.get(`/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`)) + .data, + enabled: Boolean(owner && file) && enabled, + }); +} + +export function useOwners() { + return useQuery({ + queryKey: ["owners"], + queryFn: async () => (await api.get("/owners")).data.owners, + }); +} + +export function useSaveWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file, content }) => + ( + await api.put( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`, + { content }, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["owners"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useDeleteWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file }) => + ( + await api.delete( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}`, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useRunWorkflow() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ owner, file, data }) => + ( + await api.post( + `/workflows/${encodeURIComponent(owner)}/${encodeURIComponent(file)}/run`, + data !== undefined ? { data } : {}, + ) + ).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["runs"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useReregisterWorkflows() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async () => (await api.post("/workflows/reregister")).data, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["workflows"] }); + qc.invalidateQueries({ queryKey: ["scripts"] }); + qc.invalidateQueries({ queryKey: ["owners"] }); + qc.invalidateQueries({ queryKey: ["dashboard"] }); + }, + }); +} + +export function useRuns(filters = {}) { + const { owner, workflow, status, limit } = filters; + return useQuery({ + queryKey: ["runs", { owner, workflow, status, limit }], + queryFn: async () => { + const params = {}; + if (owner) params.owner = owner; + if (workflow) params.workflow = workflow; + if (status) params.status = status; + if (limit) params.limit = limit; + return (await api.get("/runs", { params })).data.runs; + }, + }); +} + +export function useRun(id) { + return useQuery({ + queryKey: ["runs", id], + queryFn: async () => (await api.get(`/runs/${encodeURIComponent(id)}`)).data, + enabled: Boolean(id), + refetchInterval: (query) => + query.state.data?.status === "running" ? 2000 : false, + }); +} + +export function useUsers() { + return useQuery({ + queryKey: ["users"], + queryFn: async () => (await api.get("/users")).data.users, + }); +} + +export function useCreateUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body) => (await api.post("/users", body)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} + +export function useUpdateUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, ...body }) => + (await api.patch(`/users/${encodeURIComponent(id)}`, body)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} + +export function useDeleteUser() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id) => + (await api.delete(`/users/${encodeURIComponent(id)}`)).data, + onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }), + }); +} diff --git a/packages/web/src/components/CodeEditor.jsx b/packages/web/src/components/CodeEditor.jsx new file mode 100644 index 0000000..234e771 --- /dev/null +++ b/packages/web/src/components/CodeEditor.jsx @@ -0,0 +1,30 @@ +import Editor from "@monaco-editor/react"; +import { useTheme } from "../theme.jsx"; + +export function CodeEditor({ language, value, onChange, height = "50vh" }) { + const { theme } = useTheme(); + const isMobile = typeof window !== "undefined" && window.innerWidth < 768; + + return ( +
+ onChange(v ?? "")} + theme={theme === "dark" ? "vs-dark" : "light"} + options={{ + minimap: { enabled: !isMobile }, + lineNumbers: "on", + tabSize: 2, + insertSpaces: true, + autoIndent: "full", + automaticLayout: true, + wordWrap: "on", + scrollBeyondLastLine: false, + fontSize: 13, + }} + /> +
+ ); +} diff --git a/packages/web/src/components/Layout.jsx b/packages/web/src/components/Layout.jsx new file mode 100644 index 0000000..b77e334 --- /dev/null +++ b/packages/web/src/components/Layout.jsx @@ -0,0 +1,93 @@ +import { NavLink, useNavigate } from "react-router-dom"; +import { + LuActivity, + LuCode, + LuGitBranch, + LuHouse, + LuLogOut, + LuMenu, + LuMoon, + LuSun, + LuUsers, +} from "react-icons/lu"; +import { useLogout } from "../api/hooks.js"; +import { useTheme } from "../theme.jsx"; + +const links = [ + { to: "/", label: "Home", icon: LuHouse, end: true }, + { to: "/scripts", label: "Scripts", icon: LuCode }, + { to: "/workflows", label: "Workflows", icon: LuGitBranch }, + { to: "/events", label: "Events", icon: LuActivity }, +]; + +export function Layout({ user, children }) { + const { theme, toggle } = useTheme(); + const logout = useLogout(); + const navigate = useNavigate(); + + const navItems = [ + ...links, + ...(user.role === "admin" + ? [{ to: "/users", label: "Users", icon: LuUsers }] + : []), + ]; + + function closeDrawer() { + const el = document.getElementById("nav-drawer"); + if (el) el.checked = false; + } + + return ( +
+ +
+
+
+ +
+
scrunner
+
+ + {user.username} + +
+
+
{children}
+
+
+
+
+ ); +} diff --git a/packages/web/src/components/MermaidDiagram.jsx b/packages/web/src/components/MermaidDiagram.jsx new file mode 100644 index 0000000..93f748f --- /dev/null +++ b/packages/web/src/components/MermaidDiagram.jsx @@ -0,0 +1,75 @@ +import { useEffect, useId, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import mermaid from "mermaid"; +import { useTheme } from "../theme.jsx"; + +function findNode(root, id) { + return ( + root.querySelector(`#${CSS.escape(id)}`) || + root.querySelector(`[id="${CSS.escape(id)}"]`) || + root.querySelector(`[id*="${CSS.escape(id)}"]`) + ); +} + +export function MermaidDiagram({ chart, scriptIds = {} }) { + const { theme } = useTheme(); + const navigate = useNavigate(); + const ref = useRef(null); + const reactId = useId().replace(/:/g, ""); + + const scriptKey = JSON.stringify(scriptIds); + + useEffect(() => { + if (!chart || !ref.current) { + if (ref.current) ref.current.innerHTML = ""; + return; + } + + let cancelled = false; + mermaid.initialize({ + startOnLoad: false, + securityLevel: "loose", + suppressErrorRendering: true, + theme: theme === "dark" ? "dark" : "default", + }); + + const ids = JSON.parse(scriptKey); + const renderId = `mmd-${reactId}-${Date.now()}`; + mermaid + .render(renderId, chart) + .then(({ svg }) => { + if (cancelled || !ref.current) return; + ref.current.innerHTML = svg; + for (const [nodeId, script] of Object.entries(ids)) { + const el = findNode(ref.current, nodeId); + if (!el) continue; + el.style.cursor = "pointer"; + el.setAttribute("title", script); + el.addEventListener("click", () => { + navigate(`/scripts?edit=${encodeURIComponent(script)}`); + }); + } + }) + .catch((err) => { + if (cancelled || !ref.current) return; + ref.current.innerHTML = ""; + const p = document.createElement("p"); + p.className = "text-error text-sm"; + p.textContent = "Could not render diagram"; + ref.current.appendChild(p); + }); + + return () => { + cancelled = true; + }; + }, [chart, scriptKey, theme, navigate, reactId]); + + if (!chart) return null; + + return ( +
+ ); +} diff --git a/packages/web/src/index.css b/packages/web/src/index.css new file mode 100644 index 0000000..de45d5a --- /dev/null +++ b/packages/web/src/index.css @@ -0,0 +1,14 @@ +@import "tailwindcss"; +@plugin "daisyui" { + themes: light --default, dark --prefersdark; +} + +html, +body, +#root { + min-height: 100%; +} + +.mermaid-clickable [id^="s"] { + cursor: pointer; +} diff --git a/packages/web/src/lib/format.jsx b/packages/web/src/lib/format.jsx new file mode 100644 index 0000000..1cd3b04 --- /dev/null +++ b/packages/web/src/lib/format.jsx @@ -0,0 +1,51 @@ +export function formatTime(iso) { + if (!iso) return "—"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return "—"; + return d.toLocaleString(); +} + +export function StatusBadge({ status }) { + const cls = + status === "success" + ? "badge-success" + : status === "failed" + ? "badge-error" + : status === "running" + ? "badge-warning" + : "badge-ghost"; + return {status ?? "—"}; +} + +/** Config load + last-run health for the workflows list. */ +export function WorkflowStatusBadge({ workflow }) { + if (workflow.loadError) { + return ( + + broken + + ); + } + if (!workflow.enabled) { + return disabled; + } + if (workflow.lastStatus === "failed") { + return failed; + } + if (workflow.lastStatus === "running") { + return running; + } + if (workflow.lastStatus === "success") { + return working; + } + return never run; +} + +export function levelName(level) { + if (level <= 10) return "trace"; + if (level <= 20) return "debug"; + if (level <= 30) return "info"; + if (level <= 40) return "warn"; + if (level <= 50) return "error"; + return "fatal"; +} diff --git a/packages/web/src/lib/workflow-mermaid.js b/packages/web/src/lib/workflow-mermaid.js new file mode 100644 index 0000000..b1b98a7 --- /dev/null +++ b/packages/web/src/lib/workflow-mermaid.js @@ -0,0 +1,67 @@ +function sanitize(label) { + const cleaned = String(label ?? "") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 48); + return cleaned || "node"; +} + +function parseStep(step) { + if (typeof step === "string") return step; + if (step?.script) return step.script; + return null; +} + +export function workflowToArchitecture(parsed) { + /** @type {Record} */ + const scriptIds = {}; + if (!parsed || typeof parsed !== "object") { + return { chart: "", scriptIds }; + } + + const groupId = "wf"; + const name = sanitize(parsed.name || "workflow"); + const lines = ["architecture-beta", ` group ${groupId}[${name}]`]; + + const triggerIds = []; + const triggers = Array.isArray(parsed.triggers) ? parsed.triggers : []; + triggers.forEach((t, i) => { + const id = `t${i}`; + const icon = t?.type === "cron" ? "cloud" : "internet"; + const label = + t?.type === "cron" + ? `cron ${t.schedule ?? ""}` + : `${t?.method ?? "POST"} ${t?.path ?? ""}`; + lines.push(` service ${id}(${icon})[${sanitize(label)}] in ${groupId}`); + triggerIds.push(id); + }); + + const scripts = Array.isArray(parsed.scripts) ? parsed.scripts : []; + let prev = null; + let firstScript = null; + scripts.forEach((step, i) => { + const script = parseStep(step); + if (!script) return; + const id = `s${i}`; + scriptIds[id] = script; + lines.push(` service ${id}(server)[${sanitize(script)}] in ${groupId}`); + if (!firstScript) firstScript = id; + if (prev) lines.push(` ${prev}:R -- L:${id}`); + prev = id; + }); + + // All triggers point at the first script (not chained to each other). + if (firstScript) { + for (const tid of triggerIds) { + lines.push(` ${tid}:R -- L:${firstScript}`); + } + } + + // Keep multiple triggers in one column so they don't overlap. + if (triggerIds.length >= 2) { + lines.push(` align column ${triggerIds.join(" ")}`); + } + + return { chart: lines.join("\n"), scriptIds }; +} diff --git a/packages/web/src/main.jsx b/packages/web/src/main.jsx new file mode 100644 index 0000000..22ad123 --- /dev/null +++ b/packages/web/src/main.jsx @@ -0,0 +1,28 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ThemeProvider } from "./theme.jsx"; +import { App } from "./App.jsx"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById("root")).render( + + + + + + + + + , +); diff --git a/packages/web/src/pages/AuthPage.jsx b/packages/web/src/pages/AuthPage.jsx new file mode 100644 index 0000000..5bfb0d2 --- /dev/null +++ b/packages/web/src/pages/AuthPage.jsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { LuLogIn, LuUserPlus } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { useLogin, useRegister } from "../api/hooks.js"; + +export function AuthPage({ needsSetup }) { + const login = useLogin(); + const register = useRegister(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const mutation = needsSetup ? register : login; + const error = mutation.isError ? errorMessage(mutation.error) : null; + + function onSubmit(e) { + e.preventDefault(); + mutation.mutate({ username, password }); + } + + return ( +
+
+ + {needsSetup ? "Create admin" : "Sign in"} + + + setUsername(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + minLength={8} + /> + {error ?

{error}

: null} + +
+
+ ); +} diff --git a/packages/web/src/pages/EventDetailPage.jsx b/packages/web/src/pages/EventDetailPage.jsx new file mode 100644 index 0000000..f7b1467 --- /dev/null +++ b/packages/web/src/pages/EventDetailPage.jsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { LuArrowLeft } from "react-icons/lu"; +import { useRun } from "../api/hooks.js"; +import { formatTime, levelName, StatusBadge } from "../lib/format.jsx"; + +export function EventDetailPage() { + const { id } = useParams(); + const { data: run, isLoading, error } = useRun(id); + const [wordWrap, setWordWrap] = useState(true); + + if (isLoading) return ; + if (error || !run) return

Event not found

; + + return ( +
+
+ + + +

{run.workflow_name || run.workflow}

+ +
+ +
+
+
Started
+
{formatTime(run.started_at)}
+
+
+
Finished
+
{formatTime(run.finished_at)}
+
+
+
Trigger
+
+ {run.trigger_type} + {run.trigger_detail ? ` · ${run.trigger_detail}` : ""} +
+
+
+
Duration
+
{run.duration_ms != null ? `${run.duration_ms}ms` : "—"}
+
+
+ + {run.error ? ( +
+ {run.error} +
+ ) : null} + +
+

Steps

+
+ + + + + + + + + + + + {(run.steps ?? []).map((s) => ( + + + + + + + + ))} + +
#ScriptStatusDurationError
{s.step_index} + + {s.script} + + + + {s.duration_ms != null ? `${s.duration_ms}ms` : "—"}{s.error || ""}
+
+
+ +
+
+

Logs

+ +
+
+ {(run.logs ?? []).length === 0 ? ( +
+ — + no logs +
+ ) : ( + run.logs.map((l) => ( +
+ + {levelName(l.level)} + + + {l.ts} {l.msg ?? ""} + {l.payload ? ` ${JSON.stringify(l.payload)}` : ""} + +
+ )) + )} +
+
+
+ ); +} diff --git a/packages/web/src/pages/EventsPage.jsx b/packages/web/src/pages/EventsPage.jsx new file mode 100644 index 0000000..fc4e22e --- /dev/null +++ b/packages/web/src/pages/EventsPage.jsx @@ -0,0 +1,82 @@ +import { Link, useSearchParams } from "react-router-dom"; +import { useRuns } from "../api/hooks.js"; +import { formatTime, StatusBadge } from "../lib/format.jsx"; + +export function EventsPage() { + const [params, setParams] = useSearchParams(); + const workflow = params.get("workflow") || ""; + const status = params.get("status") || ""; + const { data: runs = [], isLoading } = useRuns({ + workflow: workflow || undefined, + status: status || undefined, + limit: 100, + }); + + function update(key, value) { + const next = new URLSearchParams(params); + if (value) next.set(key, value); + else next.delete(key); + setParams(next); + } + + return ( +
+

Events

+
+ update("workflow", e.target.value)} + /> + +
+ {isLoading ? ( + + ) : ( +
+ + + + + + + + + + + + {runs.map((r) => ( + + + + + + + + ))} + +
StatusWorkflowTriggerStartedDuration
+ + + + {r.workflow_name || r.workflow} + + + {r.trigger_type} + {r.trigger_detail ? ` · ${r.trigger_detail}` : ""} + {formatTime(r.started_at)}{r.duration_ms != null ? `${r.duration_ms}ms` : "—"}
+
+ )} +
+ ); +} diff --git a/packages/web/src/pages/HomePage.jsx b/packages/web/src/pages/HomePage.jsx new file mode 100644 index 0000000..c8cd838 --- /dev/null +++ b/packages/web/src/pages/HomePage.jsx @@ -0,0 +1,124 @@ +import { Link } from "react-router-dom"; +import { + LuActivity, + LuCode, + LuGitBranch, + LuTriangleAlert, +} from "react-icons/lu"; +import { useDashboard } from "../api/hooks.js"; +import { formatTime, StatusBadge } from "../lib/format.jsx"; + +export function HomePage() { + const { data, isLoading, error } = useDashboard(); + + if (isLoading) { + return ; + } + if (error) { + return

Failed to load dashboard

; + } + + const failed = data.needsAttention?.failed ?? []; + const broken = data.needsAttention?.brokenWorkflows ?? []; + + return ( +
+
+
+
+ +
+
Workflows
+
{data.workflowCount}
+
{data.enabledCount} enabled
+
+
+
+ +
+
Scripts
+
{data.scriptCount}
+
+
+
+ +
+
Running
+
{data.running?.length ?? 0}
+
+
+
+ +
+
Attention
+
+ {failed.length + broken.length} +
+
+
+ +
+

Running

+ +
+ +
+

Needs attention

+ {broken.length > 0 ? ( +
    + {broken.map((w) => ( +
  • + + {w.key}: {w.loadError} + +
  • + ))} +
+ ) : null} + +
+ +
+

Recent

+ +
+
+ ); +} + +function RunList({ runs, empty }) { + if (!runs?.length) { + return empty ?

{empty}

: null; + } + return ( +
+ + + + + + + + + + {runs.map((r) => ( + + + + + + ))} + +
StatusWorkflowStarted
+ + + + {r.workflow_name || r.workflow} + + {formatTime(r.started_at)}
+
+ ); +} diff --git a/packages/web/src/pages/ScriptsPage.jsx b/packages/web/src/pages/ScriptsPage.jsx new file mode 100644 index 0000000..8ca9307 --- /dev/null +++ b/packages/web/src/pages/ScriptsPage.jsx @@ -0,0 +1,198 @@ +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { LuPencil, LuPlus, LuSave, LuTrash2, LuX } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useDeleteScript, + useSaveScript, + useScript, + useScripts, +} from "../api/hooks.js"; +import { CodeEditor } from "../components/CodeEditor.jsx"; + +const NEW_TEMPLATE = `export default async function main(ctx) { + return ctx; +} +`; + +export function ScriptsPage() { + const [params, setParams] = useSearchParams(); + const editName = params.get("edit"); + const { data: scripts = [], isLoading } = useScripts(); + const [mode, setMode] = useState(null); + const [name, setName] = useState(""); + const [content, setContent] = useState(""); + const [confirmDelete, setConfirmDelete] = useState(null); + + const existing = useScript(mode === "edit" ? name : null, mode === "edit"); + const save = useSaveScript(); + const del = useDeleteScript(); + + useEffect(() => { + if (editName) { + setMode("edit"); + setName(editName); + } + }, [editName]); + + useEffect(() => { + if (mode === "edit" && existing.data?.content != null) { + setContent(existing.data.content); + } + }, [mode, existing.data]); + + function openAdd() { + setMode("add"); + setName(""); + setContent(NEW_TEMPLATE); + setParams({}); + } + + function openEdit(script) { + setMode("edit"); + setName(script); + setParams({ edit: script }); + } + + function closeForm() { + setMode(null); + setName(""); + setContent(""); + setParams({}); + } + + function onSave(e) { + e.preventDefault(); + const file = name.endsWith(".js") ? name : `${name}.js`; + save.mutate( + { name: file, content }, + { + onSuccess: () => { + setMode("edit"); + setName(file); + setParams({ edit: file }); + }, + }, + ); + } + + return ( +
+
+

Scripts

+ +
+ + {isLoading ? ( + + ) : ( +
+ + + + + + + + {scripts.map((s) => ( + + + + + ))} + +
Name +
{s} + + +
+
+ )} + + {mode ? ( +
+
+

{mode === "add" ? "New script" : name}

+ +
+ {mode === "add" ? ( + setName(e.target.value)} + required + /> + ) : null} + {mode === "edit" && existing.isLoading ? ( + + ) : ( + + )} + {save.isError ? ( +

{errorMessage(save.error)}

+ ) : null} + + + ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} diff --git a/packages/web/src/pages/UsersPage.jsx b/packages/web/src/pages/UsersPage.jsx new file mode 100644 index 0000000..bc4dd47 --- /dev/null +++ b/packages/web/src/pages/UsersPage.jsx @@ -0,0 +1,194 @@ +import { useState } from "react"; +import { LuPencil, LuPlus, LuTrash2, LuX } from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useCreateUser, + useDeleteUser, + useUpdateUser, + useUsers, +} from "../api/hooks.js"; + +export function UsersPage() { + const { data: users = [], isLoading } = useUsers(); + const create = useCreateUser(); + const update = useUpdateUser(); + const del = useDeleteUser(); + const [mode, setMode] = useState(null); + const [form, setForm] = useState({ + username: "", + password: "", + role: "operator", + id: null, + }); + const [confirmDelete, setConfirmDelete] = useState(null); + + function openAdd() { + setMode("add"); + setForm({ username: "", password: "", role: "operator", id: null }); + } + + function openEdit(u) { + setMode("edit"); + setForm({ username: u.username, password: "", role: u.role, id: u.id }); + } + + function closeForm() { + setMode(null); + } + + function onSubmit(e) { + e.preventDefault(); + if (mode === "add") { + create.mutate( + { + username: form.username, + password: form.password, + role: form.role, + }, + { onSuccess: closeForm }, + ); + } else { + const body = { id: form.id, role: form.role }; + if (form.password) body.password = form.password; + update.mutate(body, { onSuccess: closeForm }); + } + } + + const mutation = mode === "add" ? create : update; + + return ( +
+
+

Users

+ +
+ + {isLoading ? ( + + ) : ( +
+ + + + + + + + + {users.map((u) => ( + + + + + + ))} + +
UsernameRole +
{u.username} + {u.role} + + + +
+
+ )} + + {mode ? ( +
+
+ + {mode === "add" ? "New user" : form.username} + + +
+ {mode === "add" ? ( + <> + + setForm({ ...form, username: e.target.value })} + required + /> + + ) : null} + + setForm({ ...form, password: e.target.value })} + required={mode === "add"} + minLength={mode === "add" ? 8 : undefined} + placeholder={mode === "edit" ? "leave blank to keep" : ""} + /> + + + {mutation.isError ? ( +

{errorMessage(mutation.error)}

+ ) : null} + +
+ ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete.username}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} diff --git a/packages/web/src/pages/WorkflowsPage.jsx b/packages/web/src/pages/WorkflowsPage.jsx new file mode 100644 index 0000000..3a9f393 --- /dev/null +++ b/packages/web/src/pages/WorkflowsPage.jsx @@ -0,0 +1,384 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { parse as parseYaml } from "yaml"; +import { + LuActivity, + LuPencil, + LuPlay, + LuPlus, + LuRefreshCw, + LuSave, + LuTrash2, + LuX, +} from "react-icons/lu"; +import { errorMessage } from "../api/client.js"; +import { + useDeleteWorkflow, + useOwners, + useReregisterWorkflows, + useRunWorkflow, + useSaveWorkflow, + useWorkflow, + useWorkflows, +} from "../api/hooks.js"; +import { CodeEditor } from "../components/CodeEditor.jsx"; +import { MermaidDiagram } from "../components/MermaidDiagram.jsx"; +import { formatTime, WorkflowStatusBadge } from "../lib/format.jsx"; +import { workflowToArchitecture } from "../lib/workflow-mermaid.js"; + +const NEW_YAML = `name: new workflow +scripts: + - get-current-time.js +triggers: + - type: HTTP + method: POST + path: /new +`; + +export function WorkflowsPage() { + const navigate = useNavigate(); + const [params, setParams] = useSearchParams(); + const editParam = params.get("edit"); + const { data: workflows = [], isLoading } = useWorkflows(); + const { data: owners = [] } = useOwners(); + const [mode, setMode] = useState(null); + const [owner, setOwner] = useState("default"); + const [file, setFile] = useState(""); + const [content, setContent] = useState(""); + const [confirmDelete, setConfirmDelete] = useState(null); + const [runError, setRunError] = useState(null); + + const existing = useWorkflow( + mode === "edit" ? owner : null, + mode === "edit" ? file : null, + mode === "edit", + ); + const save = useSaveWorkflow(); + const del = useDeleteWorkflow(); + const run = useRunWorkflow(); + const reregister = useReregisterWorkflows(); + + useEffect(() => { + if (!editParam) return; + const slash = editParam.indexOf("/"); + if (slash === -1) return; + setMode("edit"); + setOwner(editParam.slice(0, slash)); + setFile(editParam.slice(slash + 1)); + }, [editParam]); + + useEffect(() => { + if (mode === "edit" && existing.data?.content != null) { + setContent(existing.data.content); + } + }, [mode, existing.data]); + + const { parsed, parseError, mermaid } = useMemo(() => { + try { + const parsedYaml = parseYaml(content); + const arch = workflowToArchitecture(parsedYaml); + return { parsed: parsedYaml, parseError: null, mermaid: arch }; + } catch (err) { + return { + parsed: null, + parseError: err instanceof Error ? err.message : String(err), + mermaid: { chart: "", scriptIds: {} }, + }; + } + }, [content]); + + function openAdd() { + setMode("add"); + setOwner(owners[0] || "default"); + setFile(""); + setContent(NEW_YAML); + setParams({}); + } + + function openEdit(w) { + setMode("edit"); + setOwner(w.owner); + setFile(w.file); + setParams({ edit: `${w.owner}/${w.file}` }); + } + + function closeForm() { + setMode(null); + setFile(""); + setContent(""); + setParams({}); + } + + function onSave(e) { + e.preventDefault(); + const yamlFile = file.endsWith(".yaml") || file.endsWith(".yml") ? file : `${file}.yaml`; + save.mutate( + { owner, file: yamlFile, content }, + { + onSuccess: () => { + setMode("edit"); + setFile(yamlFile); + setParams({ edit: `${owner}/${yamlFile}` }); + }, + }, + ); + } + + function onRun(w) { + setRunError(null); + run.mutate( + { owner: w.owner, file: w.file }, + { + onSuccess: (data) => { + if (data?.runId) navigate(`/events/${data.runId}`); + }, + onError: (err) => { + const runId = err?.response?.data?.runId; + if (runId) { + navigate(`/events/${runId}`); + return; + } + setRunError(`${w.key}: ${errorMessage(err)}`); + }, + }, + ); + } + + const runningKey = + run.isPending && run.variables + ? `${run.variables.owner}/${run.variables.file}` + : null; + + return ( +
+
+

Workflows

+
+ + +
+
+ + {reregister.isError ? ( +

{errorMessage(reregister.error)}

+ ) : null} + + {runError ?

{runError}

: null} + + {isLoading ? ( + + ) : ( +
+ + + + + + + + + + + + + {workflows.map((w) => ( + + + + + + + + + + ))} + +
NameOwnerStatusTriggersLast runRuns +
{w.name}{w.owner} + + + + {formatTime(w.lastInvokedAt)}{w.invocationCount} + + + + + + +
+
+ )} + + {mode ? ( +
+
+

+ {mode === "add" ? "New workflow" : `${owner}/${file}`} +

+ +
+ {mode === "add" ? ( +
+ setOwner(e.target.value)} + required + /> + setFile(e.target.value)} + required + /> +
+ ) : null} + {mode === "edit" && existing.isLoading ? ( + + ) : ( +
+ +
+ {parseError ? ( +

{parseError}

+ ) : ( + + )} + {parsed?.name ? ( +

{parsed.name}

+ ) : null} +
+
+ )} + {save.isError ? ( +

{errorMessage(save.error)}

+ ) : null} + + + ) : null} + + {confirmDelete ? ( + +
+

Delete {confirmDelete.key}?

+ {del.isError ? ( +

{errorMessage(del.error)}

+ ) : null} +
+ + +
+
+
+ +
+
+ ) : null} +
+ ); +} + +function triggerLabel(t) { + const type = String(t?.type ?? "").toLowerCase(); + if (type === "cron") return t.schedule || "cron"; + if (type === "http") return t.path || "/"; + return t?.type ?? "—"; +} + +function triggerKind(t) { + const type = String(t?.type ?? "").toLowerCase(); + if (type === "http") return t.method || "POST"; + return t?.type ?? "—"; +} + +function TriggerList({ triggers }) { + if (!triggers?.length) return —; + return ( +
    + {triggers.map((t, i) => ( +
  • + {triggerKind(t)} {triggerLabel(t)} +
  • + ))} +
+ ); +} diff --git a/packages/web/src/theme.jsx b/packages/web/src/theme.jsx new file mode 100644 index 0000000..701c37e --- /dev/null +++ b/packages/web/src/theme.jsx @@ -0,0 +1,27 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +const ThemeContext = createContext(null); + +export function ThemeProvider({ children }) { + const [theme, setTheme] = useState(() => { + if (typeof localStorage === "undefined") return "light"; + return localStorage.getItem("theme") || "light"; + }); + + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + }, [theme]); + + const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark")); + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js new file mode 100644 index 0000000..0c6e009 --- /dev/null +++ b/packages/web/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + proxy: { + "/api": { target: "http://127.0.0.1:9000", changeOrigin: true }, + "/admin": { target: "http://127.0.0.1:9000", changeOrigin: true }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e55a8bf..1564770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,28 @@ settings: importers: - .: + .: {} + + packages/server: dependencies: + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.1.2 + '@fastify/cors': + specifier: ^11.1.0 + version: 11.3.0 + '@fastify/jwt': + specifier: ^9.1.0 + version: 9.1.0 + '@fastify/static': + specifier: ^8.2.0 + version: 8.3.0 axios: specifier: ^1.19.0 version: 1.19.0 + bcryptjs: + specifier: ^3.0.2 + version: 3.0.3 better-sqlite3: specifier: ^13.0.3 version: 13.0.3 @@ -36,11 +53,314 @@ importers: specifier: ^2.9.0 version: 2.9.0 + packages/web: + dependencies: + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/react-query': + specifier: ^5.84.0 + version: 5.101.4(react@19.2.8) + axios: + specifier: ^1.11.0 + version: 1.19.0 + mermaid: + specifier: ^11.9.0 + version: 11.16.1 + react: + specifier: ^19.1.1 + version: 19.2.8 + react-dom: + specifier: ^19.1.1 + version: 19.2.8(react@19.2.8) + react-icons: + specifier: ^5.5.0 + version: 5.7.0(react@19.2.8) + react-router-dom: + specifier: ^7.8.0 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + yaml: + specifier: ^2.8.1 + version: 2.9.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.11 + version: 4.3.3(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.2.0(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + daisyui: + specifier: ^5.0.50 + version: 5.7.16 + tailwindcss: + specifier: ^4.1.11 + version: 4.3.3 + vite: + specifier: ^7.1.2 + version: 7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + packages: + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@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.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + 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/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@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] + + '@fastify/accept-negotiator@2.1.0': + resolution: {integrity: sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==} + '@fastify/ajv-compiler@4.0.6': resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + '@fastify/cookie@11.1.2': + resolution: {integrity: sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==} + + '@fastify/cors@11.3.0': + resolution: {integrity: sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -50,15 +370,422 @@ packages: '@fastify/forwarded@3.0.2': resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + '@fastify/jwt@9.1.0': + resolution: {integrity: sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==} + '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/send@4.1.1': + resolution: {integrity: sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==} + + '@fastify/static@8.3.0': + resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=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.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} + + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + 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 + + '@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] + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@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/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} + peerDependencies: + react: ^18 || ^19 + + '@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/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@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 + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -77,6 +804,9 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -90,14 +820,42 @@ packages: axios@1.19.0: resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.14: + resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + better-sqlite3@13.0.3: resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} engines: {node: '>=22'} + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} @@ -109,13 +867,204 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.1: + resolution: {integrity: sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + daisyui@5.7.16: + resolution: {integrity: sha512-V9CJxvrWIXTDuP/0tpijjWaKaKJNYo2IrULJfTWs3/DjW9rPqswk1n8NCLQRLrTutmlG2Ech7nkJDpCnF+6Dzw==} + date-fns@4.4.0: resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -134,18 +1083,45 @@ packages: supports-color: optional: true + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + 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'} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.406: + resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -162,10 +1138,21 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + + 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-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} @@ -179,6 +1166,10 @@ packages: fast-json-stringify@7.0.1: resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + fast-jwt@5.0.6: + resolution: {integrity: sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==} + engines: {node: '>=20'} + fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} @@ -188,12 +1179,37 @@ packages: fast-uri@4.1.2: resolution: {integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==} + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify-plugin@6.0.0: + resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} + fastify@5.12.0: resolution: {integrity: sha512-A3RNEaDIHWaxFW8n8rNJaW1wQ+XAXuoU71llfUQJjuh5WaYLmKfRhhanaJBOx8m2EBPQkR6sDBovYdHNe9F6rA==} + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + find-my-way@9.7.0: resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} engines: {node: '>=20'} @@ -207,13 +1223,26 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -229,10 +1258,22 @@ packages: getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -245,10 +1286,31 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + interpret@2.2.0: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} @@ -261,16 +1323,47 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonata@2.2.2: resolution: {integrity: sha512-XDFH2PuaAXv0AXJEWwElXbqADmKFGKMLMkFb+qOz0EhjQW2EIJ6pgtordLU+4u3IVERyBfqy6bi6kZKEncvRqA==} engines: {node: '>= 8'} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + knex@3.3.0: resolution: {integrity: sha512-LgWl031hNuLv9Lhxdd9093zULa2aNcoP04Bk29e5NidgRcEr/T0rAr2WYi4BhjTiCDoQiRU56meUE5rppLKZzQ==} engines: {node: '>=16'} @@ -305,16 +1398,118 @@ packages: tedious: optional: true + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + 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'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mermaid@11.16.1: + resolution: {integrity: sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -323,12 +1518,39 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mnemonist@0.40.4: + resolution: {integrity: sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==} + + monaco-editor@0.56.0: + resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + node-addon-api@8.9.2: resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} engines: {node: ^18 || ^20 || >= 21} @@ -337,16 +1559,47 @@ packages: resolution: {integrity: sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==} engines: {node: '>=20'} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pg-connection-string@2.6.2: resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -360,6 +1613,16 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + process-warning@4.0.1: resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} @@ -373,6 +1636,41 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-icons@5.7.0: + resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -408,6 +1706,23 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -416,9 +1731,19 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + 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'} @@ -427,17 +1752,56 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + 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'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + 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'} + tarn@3.1.2: resolution: {integrity: sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==} engines: {node: '>=8.0.0'} @@ -450,10 +1814,88 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + toad-cache@3.7.4: resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} engines: {node: '>=20'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -461,12 +1903,223 @@ packages: snapshots: + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + + '@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.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@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.8 + '@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.28.8 + 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.8 + '@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.8 + 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.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + 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/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.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 + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@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 + + '@fastify/accept-negotiator@2.1.0': {} + '@fastify/ajv-compiler@4.0.6': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) fast-uri: 4.1.2 + '@fastify/cookie@11.1.2': + dependencies: + cookie: 2.0.1 + fastify-plugin: 6.0.0 + + '@fastify/cors@11.3.0': + dependencies: + fastify-plugin: 6.0.0 + toad-cache: 3.7.4 + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.1.0': @@ -475,6 +2128,14 @@ snapshots: '@fastify/forwarded@3.0.2': {} + '@fastify/jwt@9.1.0': + dependencies: + '@fastify/error': 4.2.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 5.0.6 + fastify-plugin: 5.1.0 + steed: 1.1.3 + '@fastify/merge-json-schemas@0.2.1': dependencies: dequal: 2.0.3 @@ -484,8 +2145,388 @@ snapshots: '@fastify/forwarded': 3.0.2 ipaddr.js: 2.5.0 + '@fastify/send@4.1.1': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@8.3.0': + dependencies: + '@fastify/accept-negotiator': 2.1.0 + '@fastify/send': 4.1.1 + content-disposition: 0.5.4 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 11.1.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@isaacs/cliui@9.0.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@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.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/ms@2.0.2': {} + + '@mermaid-js/parser@1.2.0': + dependencies: + '@chevrotain/types': 11.1.2 + + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.56.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@pinojs/redact@0.4.0': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + 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/vite@4.3.3(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + + '@tanstack/query-core@5.101.4': {} + + '@tanstack/react-query@5.101.4(react@19.2.8)': + dependencies: + '@tanstack/query-core': 5.101.4 + react: 19.2.8 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@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.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-react@5.2.0(vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.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.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + abstract-logging@2.0.1: {} agent-base@6.0.2: @@ -505,6 +2546,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -524,15 +2572,37 @@ snapshots: - debug - supports-color + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.14: {} + + bcryptjs@3.0.3: {} + better-sqlite3@13.0.3: dependencies: node-addon-api: 8.9.2 + bn.js@4.12.5: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.14 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.406 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 + caniuse-lite@1.0.30001809: {} + colorette@2.0.19: {} combined-stream@1.0.8: @@ -541,10 +2611,224 @@ snapshots: commander@10.0.1: {} + commander@7.2.0: {} + + commander@8.3.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cookie@2.0.1: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.1 + + cytoscape-fcose@2.2.0(cytoscape@3.34.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.1 + + cytoscape@3.34.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + daisyui@5.7.16: {} + date-fns@4.4.0: {} + dayjs@1.11.21: {} + debug@4.3.4: dependencies: ms: 2.1.2 @@ -553,16 +2837,43 @@ snapshots: dependencies: ms: 2.1.3 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.406: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -578,8 +2889,41 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es-toolkit@1.50.0: {} + + 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-html@1.0.3: {} + esm@3.2.25: {} fast-decode-uri-component@1.0.1: {} @@ -595,6 +2939,13 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-jwt@5.0.6: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.40.4 + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 @@ -603,6 +2954,14 @@ snapshots: fast-uri@4.1.2: {} + fastfall@1.5.1: + dependencies: + reusify: 1.1.0 + + fastify-plugin@5.1.0: {} + + fastify-plugin@6.0.0: {} + fastify@5.12.0: dependencies: '@fastify/ajv-compiler': 4.0.6 @@ -621,10 +2980,24 @@ snapshots: semver: 7.8.5 toad-cache: 3.7.4 + fastparallel@2.4.1: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 + fastseries@1.7.2: + dependencies: + reusify: 1.1.0 + xtend: 4.0.2 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + find-my-way@9.7.0: dependencies: fast-deep-equal: 3.1.3 @@ -633,6 +3006,11 @@ snapshots: follow-redirects@1.16.0: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -641,8 +3019,13 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -665,8 +3048,21 @@ snapshots: getopts@2.3.0: {} + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.6 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + + hachure-fill@0.5.2: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -677,6 +3073,14 @@ snapshots: dependencies: function-bind: 1.1.2 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -684,6 +3088,18 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + inherits@2.0.4: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + interpret@2.2.0: {} ipaddr.js@2.5.0: {} @@ -692,14 +3108,34 @@ snapshots: dependencies: hasown: 2.0.4 + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 json-schema-traverse@1.0.0: {} + json5@2.2.3: {} + jsonata@2.2.2: {} + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + knex@3.3.0(better-sqlite3@13.0.3): dependencies: colorette: 2.0.19 @@ -721,36 +3157,171 @@ snapshots: transitivePeerDependencies: - supports-color + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + light-my-request@6.6.0: dependencies: cookie: 1.1.1 process-warning: 4.0.1 set-cookie-parser: 2.7.2 + 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 + + lodash-es@4.18.1: {} + lodash@4.18.1: {} + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@14.0.0: {} + + marked@16.4.2: {} + math-intrinsics@1.1.0: {} + mermaid@11.16.1: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.4 + '@mermaid-js/parser': 1.2.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.1) + cytoscape-fcose: 2.2.0(cytoscape@3.34.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.13 + es-toolkit: 1.50.0 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.1 + mime-db@1.52.0: {} mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime@3.0.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minipass@7.1.3: {} + + mnemonist@0.40.4: + dependencies: + obliterator: 2.0.5 + + monaco-editor@0.56.0: + dependencies: + dompurify: 3.4.8 + marked: 14.0.0 + ms@2.1.2: {} ms@2.1.3: {} + nanoid@3.3.18: {} + node-addon-api@8.9.2: {} node-cron@4.6.0: {} + node-releases@2.0.53: {} + + obliterator@2.0.5: {} + on-exit-leak-free@2.1.2: {} + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.8.0: {} + + path-data-parser@0.1.0: {} + + path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + pg-connection-string@2.6.2: {} + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -776,6 +3347,19 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.2.0 + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + process-warning@4.0.1: {} process-warning@5.1.0: {} @@ -784,6 +3368,33 @@ snapshots: quick-format-unescaped@4.0.4: {} + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-icons@5.7.0(react@19.2.8): + dependencies: + react: 19.2.8 + + react-refresh@0.18.0: {} + + react-router-dom@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + + react@19.2.8: {} + real-require@0.2.0: {} real-require@1.0.0: {} @@ -809,26 +3420,107 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.3: {} + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safe-buffer@5.2.1: {} + safe-regex2@5.1.1: dependencies: ret: 0.5.0 safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} + semver@7.8.5: {} set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + split2@4.2.0: {} + state-local@1.0.7: {} + + statuses@2.0.2: {} + + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.20.1 + fastseries: 1.7.2 + reusify: 1.1.0 + + stylis@4.4.0: {} + supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + tarn@3.1.2: {} thread-stream@4.2.0: @@ -837,6 +3529,47 @@ snapshots: tildify@2.0.0: {} + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + toad-cache@3.7.4: {} + toidentifier@1.0.1: {} + + ts-dedent@2.3.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@14.0.1: {} + + vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + yaml: 2.9.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + xtend@4.0.2: {} + + yallist@3.1.1: {} + yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..924b55f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/*