diff --git a/.dockerignore b/.dockerignore index 021a3bb229..eef105470a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -68,5 +68,10 @@ logs/ Thumbs.db .DS_Store +# Web dashboard build artifacts +web/node_modules/ +web/dist/ +web/.env + # uv .python-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 118950c39f..46001243fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,10 +99,107 @@ jobs: name: test-results-${{ matrix.python-version }} path: junit.xml + dashboard-lint: + name: Dashboard Lint + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + working-directory: web + - run: npm run lint + working-directory: web + + dashboard-type-check: + name: Dashboard Type Check + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + working-directory: web + - run: npm run type-check + working-directory: web + + dashboard-test: + name: Dashboard Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + working-directory: web + - run: npm run test -- --coverage + working-directory: web + + dashboard-build: + name: Dashboard Build + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + working-directory: web + - name: Production build + run: npm run build + working-directory: web + + dashboard-audit: + name: Dashboard Security Audit + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + working-directory: web + - name: npm audit (critical + high) + run: npm audit --audit-level=high + working-directory: web + ci-pass: name: CI Pass if: always() - needs: [lint, type-check, test] + needs: [lint, type-check, test, dashboard-lint, dashboard-type-check, dashboard-test, dashboard-build, dashboard-audit] runs-on: ubuntu-latest permissions: {} steps: @@ -111,11 +208,21 @@ jobs: LINT_RESULT: ${{ needs.lint.result }} TYPE_CHECK_RESULT: ${{ needs.type-check.result }} TEST_RESULT: ${{ needs.test.result }} + DASHBOARD_LINT_RESULT: ${{ needs.dashboard-lint.result }} + DASHBOARD_TYPE_CHECK_RESULT: ${{ needs.dashboard-type-check.result }} + DASHBOARD_TEST_RESULT: ${{ needs.dashboard-test.result }} + DASHBOARD_BUILD_RESULT: ${{ needs.dashboard-build.result }} + DASHBOARD_AUDIT_RESULT: ${{ needs.dashboard-audit.result }} run: | if [[ "$LINT_RESULT" != "success" || \ "$TYPE_CHECK_RESULT" != "success" || \ - "$TEST_RESULT" != "success" ]]; then - echo "CI failed: lint=$LINT_RESULT, type-check=$TYPE_CHECK_RESULT, test=$TEST_RESULT" + "$TEST_RESULT" != "success" || \ + "$DASHBOARD_LINT_RESULT" != "success" || \ + "$DASHBOARD_TYPE_CHECK_RESULT" != "success" || \ + "$DASHBOARD_TEST_RESULT" != "success" || \ + "$DASHBOARD_BUILD_RESULT" != "success" || \ + "$DASHBOARD_AUDIT_RESULT" != "success" ]]; then + echo "CI failed: lint=$LINT_RESULT, type-check=$TYPE_CHECK_RESULT, test=$TEST_RESULT, dashboard-lint=$DASHBOARD_LINT_RESULT, dashboard-type-check=$DASHBOARD_TYPE_CHECK_RESULT, dashboard-test=$DASHBOARD_TEST_RESULT, dashboard-build=$DASHBOARD_BUILD_RESULT, dashboard-audit=$DASHBOARD_AUDIT_RESULT" exit 1 fi echo "All CI checks passed" diff --git a/.gitignore b/.gitignore index 3ac671297a..411d6ec7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ htmlcov/ coverage.xml .coverage .coverage.* +web/coverage/ # Environment variables .env @@ -49,6 +50,11 @@ Thumbs.db # Web UI web/node_modules/ web/dist/ +*.tsbuildinfo +web/vite.config.d.ts +web/vite.config.js +web/vitest.config.d.ts +web/vitest.config.js # Documentation build output (Zensical) _site/ diff --git a/CLAUDE.md b/CLAUDE.md index b0bb0bed72..7e144dab31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ - **What**: Framework for building synthetic organizations — autonomous AI agents orchestrated as a virtual company - **Python**: 3.14+ (PEP 649 native lazy annotations) - **License**: BUSL-1.1 (converts to Apache 2.0 on 2030-02-27) -- **Layout**: `src/ai_company/` (src layout), `tests/` (unit/integration/e2e) +- **Layout**: `src/ai_company/` (src layout), `tests/` (unit/integration/e2e), `web/` (Vue 3 dashboard) - **Design**: [DESIGN_SPEC.md](DESIGN_SPEC.md) (pointer to `docs/design/` pages) ## Design Spec (MANDATORY) @@ -43,6 +43,17 @@ uv run zensical build # build docs (output: _site/docs/) uv run zensical serve # local docs preview (http://127.0.0.1:8000) ``` +### Web Dashboard + +```bash +npm --prefix web install # install frontend deps +npm --prefix web run dev # dev server (http://localhost:5173) +npm --prefix web run build # production build +npm --prefix web run lint # ESLint +npm --prefix web run type-check # vue-tsc type checking +npm --prefix web run test # Vitest unit tests +``` + ## Documentation - **Docs source**: `docs/` (Markdown, built with Zensical) @@ -75,7 +86,7 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy) ``` - **Backend**: 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened -- **Web**: `nginxinc/nginx-unprivileged`, SPA routing, API/WebSocket proxy to backend +- **Web**: `nginxinc/nginx-unprivileged`, Vue 3 SPA (PrimeVue + Tailwind CSS), SPA routing, API/WebSocket proxy to backend - **Config**: all Docker files in `docker/` — Dockerfiles, compose, `.env.example` - **CI**: `.github/workflows/docker.yml` — build → scan → push to GHCR + cosign sign (images only pushed after Trivy/Grype scans pass) - **Build context**: single root `.dockerignore` (both images build with `context: .`) @@ -101,6 +112,18 @@ src/ai_company/ security/ # SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume) templates/ # Pre-built company templates, personality presets, and builder tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), MCP bridge (mcp/), role-based access + +web/ # Vue 3 + PrimeVue + Tailwind CSS dashboard + src/ + api/ # Axios client, endpoint modules, TypeScript types (mirrors backend Pydantic models) + components/ # Vue components organized by feature (agents/, approvals/, budget/, common/, dashboard/, layout/, messages/, org-chart/, tasks/) + composables/ # Reusable composition functions (useAuth, usePolling, useOptimisticUpdate) + router/ # Vue Router config with auth guards + stores/ # Pinia stores (auth, agents, tasks, budget, messages, approvals, websocket, analytics, company, providers) + styles/ # Global CSS and PrimeVue theme configuration + utils/ # Constants, formatters, error helpers + views/ # Page-level components (LoginPage, SetupPage, PlaceholderHome; feature pages in PR 2) + __tests__/ # Vitest unit tests (organized by feature) ``` ## Shell Usage @@ -207,3 +230,4 @@ src/ai_company/ - **Groups**: `test` (pytest + plugins), `dev` (includes test + ruff, mypy, pre-commit, commitizen) - **Required**: `mem0ai` (Mem0 memory backend — the default and currently only backend) - **Install**: `uv sync` installs everything (dev group is default) +- **Web dashboard**: Node.js 20+, dependencies in `web/package.json` (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc) diff --git a/README.md b/README.md index 821d9cb412..764487dceb 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ graph TB ## Status -Core framework complete — agent engine, multi-agent coordination, API, security, HR, memory (including Mem0 backend adapter), and budget systems are implemented. Remaining: approval workflow gates, CLI, web dashboard. See the [roadmap](docs/roadmap/index.md) for details. +Core framework complete — agent engine, multi-agent coordination, API, security, HR, memory (including Mem0 backend adapter), and budget systems are implemented. Web dashboard (Vue 3 + PrimeVue + Tailwind CSS) is built. Remaining: approval workflow gates, CLI. See the [roadmap](docs/roadmap/index.md) for details. ## License diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index 301a2477fc..b52ba6aecd 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -1,9 +1,23 @@ # syntax=docker/dockerfile:1 # ============================================================================= -# SynthOrg Web — Non-root nginx container (CIS hardening applied via compose.yml) +# SynthOrg Web — Multi-stage build: Node builder → nginx runtime +# CIS hardening applied via compose.yml (no-new-privileges, drop caps, etc.) # ============================================================================= +# Stage 1: Build Vue dashboard (non-root for defense-in-depth) +FROM node:22-alpine@sha256:3a4802e64ab5181c7870d6ddd8c824c2efc42873baae37d1971451668659483b AS builder +RUN addgroup -S build && adduser -S build -G build +WORKDIR /app +COPY --chown=build:build web/package.json web/package-lock.json ./ +# Run npm ci as build user so node_modules (including .vite cache) is owned by +# the same user that runs the build — avoids permission errors in Vite. +USER build +RUN npm ci +COPY --chown=build:build web/ ./ +RUN npm run build + +# Stage 2: Serve with nginx FROM nginxinc/nginx-unprivileged:1.29.5-alpine@sha256:aec540f08f99df3c830549d5dd7bfaf63e01cbbb499e37400c5af9f8e8554e9f LABEL org.opencontainers.image.title="synthorg-web" \ @@ -14,7 +28,7 @@ LABEL org.opencontainers.image.title="synthorg-web" \ org.opencontainers.image.vendor="Aureliolo" COPY web/nginx.conf /etc/nginx/conf.d/default.conf -COPY web/index.html web/style.css web/app.js /usr/share/nginx/html/ +COPY --from=builder /app/dist/ /usr/share/nginx/html/ EXPOSE 8080 diff --git a/docs/design/operations.md b/docs/design/operations.md index b788d570b8..70f69506d9 100644 --- a/docs/design/operations.md +++ b/docs/design/operations.md @@ -933,7 +933,7 @@ future CLI tool are thin clients that call the API -- they contain no business l | | +-------v--+ +---v--------+ | Web UI | | CLI Tool | - | (Future) | | (Future) | + | (Vue 3) | | (Future) | +----------+ +-----------+ ``` @@ -966,10 +966,11 @@ future CLI tool are thin clients that call the API -- they contain no business l ### Web UI Features -!!! warning "Planned" +!!! info "In Progress" - The Web UI is a planned future component (Vue 3). The API is fully self-sufficient for - all operations. + The Web UI is being built as a Vue 3 + PrimeVue + Tailwind CSS dashboard (core + infrastructure merged; page views and feature components in progress). The API + remains fully self-sufficient for all operations — the dashboard is a thin client. - **Dashboard**: Real-time company overview, active tasks, spending - **Org Chart**: Visual hierarchy, click to inspect any agent diff --git a/docs/getting_started.md b/docs/getting_started.md index 84ebe6543a..87fee13caf 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -121,13 +121,26 @@ synthorg/ e2e/ # Full system tests docs/ # Developer documentation docker/ # Dockerfiles, Compose, .env.example - web/ # Web UI scaffold (nginx + placeholder) + web/ # Vue 3 web dashboard (PrimeVue + Tailwind CSS) .github/ # CI workflows, dependabot, actions pyproject.toml # Project config (deps, tools, linters) DESIGN_SPEC.md # Pointer to design specification pages CLAUDE.md # AI assistant quick reference ``` +## Web Dashboard Development + +The Vue 3 dashboard lives in `web/`. Prerequisites: **Node.js 20+**. + +```bash +npm --prefix web install # install frontend deps +npm --prefix web run dev # dev server at http://localhost:5173 +npm --prefix web run lint # ESLint +npm --prefix web run type-check # vue-tsc type checking +npm --prefix web run test # Vitest unit tests +npm --prefix web run build # production build +``` + ## IDE Setup ### VS Code / Cursor diff --git a/docs/roadmap/index.md b/docs/roadmap/index.md index ef10fae0ee..ece00602e2 100644 --- a/docs/roadmap/index.md +++ b/docs/roadmap/index.md @@ -18,13 +18,18 @@ The SynthOrg core framework is complete. The following subsystems are built and - Configuration (YAML loading, Pydantic validation, company templates with inheritance) - Container packaging (Docker, Chainguard distroless, CI/CD pipelines) +## In Progress + +| Area | Description | +|------|-------------| +| **Web dashboard** | Vue 3 + PrimeVue + Tailwind CSS frontend for monitoring and managing the synthetic organization (core infrastructure merged, page views pending) | + ## Remaining Work | Area | Description | |------|-------------| | **Approval workflow gates** | Runtime wiring for human-in-the-loop approval queues | | **CLI** | Terminal interface wrapping the REST API (may not be needed) | -| **Web dashboard** | Vue 3 frontend for monitoring and managing the synthetic organization | ## Tracking diff --git a/docs/user_guide.md b/docs/user_guide.md index 1b4a59e791..e992d78b96 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -17,10 +17,10 @@ docker compose -f docker/compose.yml up -d The web dashboard is at [http://localhost:3000](http://localhost:3000). -Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup and templates will be configurable through the dashboard once available. +Container configuration (ports, storage paths, log level) is defined in `docker/.env`. Organization setup is done via the dashboard. Custom template editing through the UI is planned for a future release. -!!! danger "Work in Progress" - SynthOrg is under active development. The web dashboard, templates, and many features described here are **not yet available**. Check the [GitHub repository](https://github.com/Aureliolo/synthorg) for current status. +!!! info "Active Development" + SynthOrg is under active development. The web dashboard is available for monitoring and managing the organization. Templates and some features described here may evolve. Check the [GitHub repository](https://github.com/Aureliolo/synthorg) for current status. ## Templates diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000000..d7f6f83634 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,2 @@ +# API base URL — empty for production (relative paths), set for dev if not using Vite proxy +VITE_API_BASE_URL= diff --git a/web/app.js b/web/app.js deleted file mode 100644 index 5540241165..0000000000 --- a/web/app.js +++ /dev/null @@ -1,29 +0,0 @@ -(function () { - var el = document.getElementById("status"); - var text = document.getElementById("status-text"); - - function check() { - fetch("/api/v1/health") - .then(function (r) { - if (!r.ok) { throw new Error("HTTP " + r.status); } - return r.json(); - }) - .then(function (data) { - var s = data.data && data.data.status; - if (s === "healthy") { - el.className = "status status-connected"; - text.textContent = "Backend connected (v" + (data.data && data.data.version || "?") + ")"; - } else { - el.className = "status status-disconnected"; - text.textContent = "Backend unhealthy (" + (s || "unknown") + ")"; - } - }) - .catch(function () { - el.className = "status status-disconnected"; - text.textContent = "Backend unreachable"; - }); - } - - check(); - setInterval(check, 15000); -})(); diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 0000000000..77732bf84c --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1,15 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000000..5bc11306fa --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,36 @@ +import pluginVue from 'eslint-plugin-vue' +import pluginSecurity from 'eslint-plugin-security' +import tsParser from '@typescript-eslint/parser' + +export default [ + { + ignores: ['dist/**'], + }, + ...pluginVue.configs['flat/essential'], + pluginSecurity.configs.recommended, + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: tsParser, + }, + }, + }, + { + files: ['**/*.ts'], + languageOptions: { + parser: tsParser, + }, + }, + { + rules: { + 'vue/no-v-html': 'warn', + }, + }, + { + files: ['src/App.vue', 'src/components/layout/Sidebar.vue', 'src/components/layout/Topbar.vue'], + rules: { + 'vue/multi-word-component-names': 'off', + }, + }, +] diff --git a/web/index.html b/web/index.html index 40b3df6266..d5145b53f9 100644 --- a/web/index.html +++ b/web/index.html @@ -1,20 +1,14 @@ - - - - - SynthOrg - - - -
-

SynthOrg

-

Dashboard — Coming Soon

-
- - Checking backend... -
-
- - + + + + + + + SynthOrg Dashboard + + +
Loading…
+ + diff --git a/web/nginx.conf b/web/nginx.conf index 9765f86634..10d63914e1 100644 --- a/web/nginx.conf +++ b/web/nginx.conf @@ -1,3 +1,8 @@ +map $http_x_forwarded_proto $x_forwarded_proto { + default $http_x_forwarded_proto; + '' $scheme; +} + server { listen 8080; server_name _; @@ -19,35 +24,42 @@ server { add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; font-src 'self'" always; + # style-src 'unsafe-inline' required by PrimeVue which injects dynamic inline styles. + # connect-src 'self' covers same-origin ws:/wss: in modern browsers (Chrome 73+, Firefox 45+). + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # SPA routing — try static files, fall back to index.html location / { try_files $uri $uri/ /index.html; } - # API proxy to backend service - location /api/ { + # WebSocket proxy — MUST be before generic /api/ block + location /api/v1/ws { proxy_pass http://backend:8000; - proxy_connect_timeout 5s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; + proxy_connect_timeout 5s; + proxy_send_timeout 30s; + proxy_read_timeout 3600s; + # Suppress access logs to avoid recording JWT query parameter + access_log off; } - # WebSocket proxy - location /ws { + # API proxy to backend service + location /api/ { proxy_pass http://backend:8000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + proxy_connect_timeout 5s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 86400s; + proxy_set_header X-Forwarded-Proto $x_forwarded_proto; } } diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000000..5b89a83bba --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,6172 @@ +{ + "name": "synthorg-dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "synthorg-dashboard", + "version": "0.1.0", + "dependencies": { + "@primevue/themes": "^4", + "@tailwindcss/vite": "^4", + "@vue-flow/controls": "^1", + "@vue-flow/core": "^1", + "@vue-flow/minimap": "^1", + "axios": "^1", + "echarts": "^5", + "pinia": "^2", + "primevue": "^4", + "tailwindcss": "^4", + "vue": "^3.5", + "vue-draggable-plus": "^0.6", + "vue-echarts": "^7", + "vue-router": "^4" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "@typescript-eslint/parser": "^8.57.0", + "@vitejs/plugin-vue": "^5", + "@vitest/coverage-v8": "^3.2.4", + "@vue/test-utils": "^2", + "@vue/tsconfig": "^0.7", + "eslint": "^9", + "eslint-plugin-security": "^4.0.0", + "eslint-plugin-vue": "^9", + "jsdom": "^26", + "typescript": "^5.7", + "typescript-eslint": "^8.57.0", + "vitest": "^3", + "vue-tsc": "^2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", + "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", + "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/themes": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz", + "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", + "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.4.tgz", + "integrity": "sha512-lYJJB3wTrDJ8MkLctzHfrPZAqXVxoatjIsswSJzupatf6ZogJHVYADUKcn1JAkLLk8dtV1FA2AxDek663fHO5Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/utils": "^0.6.2" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.4.tgz", + "integrity": "sha512-DxgryEc7ZmUqcEhYMcxGBRyFzdtLIoy3jLtlH1zsVSRZaG+iSAcjQ88nvfkZxGUZtZBFL7sRjF6KLq3bJZJwUw==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/themes": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.5.4.tgz", + "integrity": "sha512-rUFZxMHLanTZdvZq4zgZPk+KRBZ3s7fE3bBK32OrZBkHQhEJmkJ7Ftd4w4QFlXyz1B7c+k5invZiOOCjwHXg9Q==", + "deprecated": "Deprecated. This package is no longer maintained. Please migrate to @primeuix/themes: https://www.npmjs.com/package/@primeuix/themes", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/themes": "^2.0.2" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-flow/controls": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz", + "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz", + "integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/minimap": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@vue-flow/minimap/-/minimap-1.5.4.tgz", + "integrity": "sha512-l4C+XTAXnRxsRpUdN7cAVFBennC1sVRzq4bDSpVK+ag7tdMczAnhFYGgbLkUw3v3sY6gokyWwMl8CDonp8eB2g==", + "license": "MIT", + "dependencies": { + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-security": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.0.tgz", + "integrity": "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/primevue": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.4.tgz", + "integrity": "sha512-nTyEohZABFJhVIpeUxgP0EJ8vKcJAhD+Z7DYj95e7ie/MNUCjRNcGjqmE1cXtXi4z54qDfTSI9h2uJ51qz2DIw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/styles": "^2.0.2", + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4", + "@primevue/icons": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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 + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-draggable-plus": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.6.1.tgz", + "integrity": "sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg==", + "license": "MIT", + "dependencies": { + "@types/sortablejs": "^1.15.8" + }, + "peerDependencies": { + "@types/sortablejs": "^1.15.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-7.0.3.tgz", + "integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==", + "license": "MIT", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.5.1", + "vue": "^2.7.0 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000000..8129b28de5 --- /dev/null +++ b/web/package.json @@ -0,0 +1,48 @@ +{ + "name": "synthorg-dashboard", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint src/", + "type-check": "vue-tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@primevue/themes": "^4", + "@tailwindcss/vite": "^4", + "@vue-flow/controls": "^1", + "@vue-flow/core": "^1", + "@vue-flow/minimap": "^1", + "axios": "^1", + "echarts": "^5", + "pinia": "^2", + "primevue": "^4", + "tailwindcss": "^4", + "vue": "^3.5", + "vue-draggable-plus": "^0.6", + "vue-echarts": "^7", + "vue-router": "^4" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "vite": "^6", + "@typescript-eslint/parser": "^8.57.0", + "@vitejs/plugin-vue": "^5", + "@vitest/coverage-v8": "^3.2.4", + "@vue/test-utils": "^2", + "@vue/tsconfig": "^0.7", + "eslint": "^9", + "eslint-plugin-security": "^4.0.0", + "eslint-plugin-vue": "^9", + "jsdom": "^26", + "typescript": "^5.7", + "typescript-eslint": "^8.57.0", + "vitest": "^3", + "vue-tsc": "^2" + } +} diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000000..b1a03b1382 --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1,4 @@ + + + S + diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000000..0114f27af9 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/__tests__/api/client.test.ts b/web/src/__tests__/api/client.test.ts new file mode 100644 index 0000000000..2432aebcd7 --- /dev/null +++ b/web/src/__tests__/api/client.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { unwrap, unwrapPaginated, apiClient } from '@/api/client' +import { AxiosHeaders, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' + +function mockResponse(data: T): AxiosResponse { + return { + data, + status: 200, + statusText: 'OK', + headers: {}, + config: {} as AxiosResponse['config'], + } +} + +describe('request interceptor', () => { + beforeEach(() => { + localStorage.clear() + }) + + function makeConfig(): InternalAxiosRequestConfig { + return { headers: new AxiosHeaders() } as InternalAxiosRequestConfig + } + + function getInterceptor() { + const handlers = (apiClient.interceptors.request as unknown as { handlers: Array<{ fulfilled?: (c: InternalAxiosRequestConfig) => InternalAxiosRequestConfig }> }).handlers + const interceptor = handlers?.[0]?.fulfilled + expect(typeof interceptor).toBe('function') + return interceptor! + } + + it('attaches JWT token to request headers', () => { + localStorage.setItem('auth_token', 'test-jwt-token') + const config = makeConfig() + const interceptor = getInterceptor() + const result = interceptor(config) + expect(result.headers.get('Authorization')).toBe('Bearer test-jwt-token') + }) + + it('does not attach Authorization when no token', () => { + const config = makeConfig() + const interceptor = getInterceptor() + const result = interceptor(config) + expect(result.headers.get('Authorization')).toBeUndefined() + }) +}) + +describe('unwrap', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('extracts data from successful response', () => { + const response = mockResponse({ data: { id: '1', name: 'test' }, error: null, success: true }) + const result = unwrap(response) + expect(result).toEqual({ id: '1', name: 'test' }) + }) + + it('throws on error response', () => { + const response = mockResponse({ data: null, error: 'Not found', success: false }) + expect(() => unwrap(response)).toThrow('Not found') + }) + + it('throws on success:false with null data and null error', () => { + const response = mockResponse({ data: null, error: null, success: false }) + expect(() => unwrap(response)).toThrow('Unknown API error') + }) + + it('throws on success:true with null data', () => { + const response = mockResponse({ data: null, error: null, success: true }) + expect(() => unwrap(response)).toThrow('Unknown API error') + }) +}) + +describe('unwrapPaginated', () => { + it('extracts paginated data', () => { + const response = mockResponse({ + data: [{ id: '1' }, { id: '2' }], + error: null, + success: true, + pagination: { total: 10, offset: 0, limit: 50 }, + }) + const result = unwrapPaginated(response) + expect(result.data).toHaveLength(2) + expect(result.total).toBe(10) + expect(result.offset).toBe(0) + expect(result.limit).toBe(50) + }) + + it('throws on error', () => { + const response = mockResponse({ + data: null, + error: 'Server error', + success: false, + pagination: null, + }) + expect(() => unwrapPaginated(response)).toThrow('Server error') + }) + + it('throws on success with missing pagination', () => { + const response = mockResponse({ + data: [{ id: '1' }], + error: null, + success: true, + pagination: null, + }) + expect(() => unwrapPaginated(response)).toThrow('Unexpected API response format') + }) + + it('throws on success with non-array data', () => { + const response = mockResponse({ + data: 'not-an-array', + error: null, + success: true, + pagination: { total: 0, offset: 0, limit: 50 }, + }) + expect(() => unwrapPaginated(response)).toThrow('Unexpected API response format') + }) +}) diff --git a/web/src/__tests__/components/EmptyState.test.ts b/web/src/__tests__/components/EmptyState.test.ts new file mode 100644 index 0000000000..2e49fa1320 --- /dev/null +++ b/web/src/__tests__/components/EmptyState.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import EmptyState from '@/components/common/EmptyState.vue' + +describe('EmptyState', () => { + it('renders title', () => { + const wrapper = mount(EmptyState, { + props: { title: 'No items found' }, + }) + expect(wrapper.text()).toContain('No items found') + }) + + it('renders message when provided', () => { + const wrapper = mount(EmptyState, { + props: { title: 'Empty', message: 'Nothing here yet' }, + }) + expect(wrapper.text()).toContain('Nothing here yet') + }) + + it('renders icon with correct class and aria-hidden', () => { + const wrapper = mount(EmptyState, { + props: { title: 'Empty', icon: 'pi pi-inbox' }, + }) + const icon = wrapper.find('i') + expect(icon.exists()).toBe(true) + expect(icon.classes()).toContain('pi') + expect(icon.classes()).toContain('pi-inbox') + expect(icon.attributes('aria-hidden')).toBe('true') + }) + + it('does not render message when not provided', () => { + const wrapper = mount(EmptyState, { + props: { title: 'Empty' }, + }) + const paragraphs = wrapper.findAll('p') + expect(paragraphs).toHaveLength(0) + }) + + it('renders action slot when provided', () => { + const wrapper = mount(EmptyState, { + props: { title: 'Empty' }, + slots: { action: '' }, + }) + const button = wrapper.find('button') + expect(button.exists()).toBe(true) + expect(button.text()).toBe('Create Item') + }) + + it('does not render action container when slot is not provided', () => { + const wrapper = mount(EmptyState, { + props: { title: 'Empty' }, + }) + // The mt-4 div should not render when no action slot is provided + const actionDivs = wrapper.findAll('.mt-4') + expect(actionDivs).toHaveLength(0) + }) +}) diff --git a/web/src/__tests__/components/PageHeader.test.ts b/web/src/__tests__/components/PageHeader.test.ts new file mode 100644 index 0000000000..dd2f97584a --- /dev/null +++ b/web/src/__tests__/components/PageHeader.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import PageHeader from '@/components/common/PageHeader.vue' + +describe('PageHeader', () => { + it('renders title', () => { + const wrapper = mount(PageHeader, { + props: { title: 'Dashboard' }, + }) + expect(wrapper.find('h1').text()).toBe('Dashboard') + }) + + it('renders subtitle when provided', () => { + const wrapper = mount(PageHeader, { + props: { title: 'Dashboard', subtitle: 'Overview' }, + }) + expect(wrapper.text()).toContain('Overview') + }) + + it('does not render subtitle when not provided', () => { + const wrapper = mount(PageHeader, { + props: { title: 'Dashboard' }, + }) + const paragraphs = wrapper.findAll('p') + expect(paragraphs).toHaveLength(0) + }) + + it('renders actions slot', () => { + const wrapper = mount(PageHeader, { + props: { title: 'Dashboard' }, + slots: { actions: '' }, + }) + expect(wrapper.find('button').exists()).toBe(true) + expect(wrapper.find('button').text()).toBe('Action') + }) +}) diff --git a/web/src/__tests__/components/StatusBadge.test.ts b/web/src/__tests__/components/StatusBadge.test.ts new file mode 100644 index 0000000000..baeebc2fa7 --- /dev/null +++ b/web/src/__tests__/components/StatusBadge.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import StatusBadge from '@/components/common/StatusBadge.vue' + +describe('StatusBadge', () => { + it('renders status value as label', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'in_progress' }, + }) + expect(wrapper.text()).toContain('In Progress') + }) + + it('renders priority type', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'critical', type: 'priority' }, + }) + expect(wrapper.text()).toContain('Critical') + }) + + it('renders risk type', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'high', type: 'risk' }, + }) + expect(wrapper.text()).toContain('High') + }) + + it('applies correct color classes for known status', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'completed' }, + }) + const tag = wrapper.find('.p-tag') + expect(tag.exists()).toBe(true) + expect(tag.classes()).toContain('bg-green-600') + expect(tag.classes()).toContain('text-green-100') + }) + + it('applies correct color classes for priority', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'high', type: 'priority' }, + }) + const tag = wrapper.find('.p-tag') + expect(tag.exists()).toBe(true) + expect(tag.classes()).toContain('bg-orange-600') + expect(tag.classes()).toContain('text-orange-100') + }) + + it('falls back to slate for unknown value', () => { + const wrapper = mount(StatusBadge, { + props: { value: 'unknown_status' }, + }) + const tag = wrapper.find('.p-tag') + expect(tag.exists()).toBe(true) + expect(tag.classes()).toContain('bg-slate-600') + expect(tag.classes()).toContain('text-slate-200') + }) +}) diff --git a/web/src/__tests__/composables/useOptimisticUpdate.test.ts b/web/src/__tests__/composables/useOptimisticUpdate.test.ts new file mode 100644 index 0000000000..a06e9c03c2 --- /dev/null +++ b/web/src/__tests__/composables/useOptimisticUpdate.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, vi } from 'vitest' +import { useOptimisticUpdate } from '@/composables/useOptimisticUpdate' + +vi.mock('@/utils/errors', () => ({ + getErrorMessage: (err: unknown) => (err instanceof Error ? err.message : 'Unknown error'), +})) + +describe('useOptimisticUpdate', () => { + it('returns pending, error, and execute', () => { + const { pending, error, execute } = useOptimisticUpdate() + expect(pending.value).toBe(false) + expect(error.value).toBeNull() + expect(typeof execute).toBe('function') + }) + + it('sets pending during execution and clears after', async () => { + const { pending, execute } = useOptimisticUpdate() + let pendingDuringAction = false + + await execute( + () => () => {}, + () => { + pendingDuringAction = pending.value + return Promise.resolve('done') + }, + ) + + expect(pendingDuringAction).toBe(true) + expect(pending.value).toBe(false) + }) + + it('returns server action result on success', async () => { + const { execute } = useOptimisticUpdate() + + const result = await execute( + () => () => {}, + () => Promise.resolve({ id: '123' }), + ) + + expect(result).toEqual({ id: '123' }) + }) + + it('rolls back and returns null on server action failure', async () => { + const { error, execute } = useOptimisticUpdate() + let state = 'original' + + const result = await execute( + () => { + state = 'optimistic' + return () => { + state = 'original' + } + }, + () => Promise.reject(new Error('Server error')), + ) + + expect(result).toBeNull() + expect(state).toBe('original') + expect(error.value).toBe('Server error') + }) + + it('handles rollback failure gracefully', async () => { + const { error, execute } = useOptimisticUpdate() + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + try { + await execute( + () => () => { + throw new Error('Rollback boom') + }, + () => Promise.reject(new Error('Server error')), + ) + + expect(error.value).toBe('Server error') + // Rollback errors are logged via getErrorMessage (string), not raw Error + expect(consoleSpy).toHaveBeenCalledWith('Rollback failed:', 'Rollback boom') + } finally { + consoleSpy.mockRestore() + } + }) + + it('clears error on next execution', async () => { + const { error, execute } = useOptimisticUpdate() + + await execute( + () => () => {}, + () => Promise.reject(new Error('fail')), + ) + expect(error.value).toBe('fail') + + await execute( + () => () => {}, + () => Promise.resolve('ok'), + ) + expect(error.value).toBeNull() + }) +}) diff --git a/web/src/__tests__/composables/usePolling.test.ts b/web/src/__tests__/composables/usePolling.test.ts new file mode 100644 index 0000000000..1046919fef --- /dev/null +++ b/web/src/__tests__/composables/usePolling.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' + +// Mock Vue's onUnmounted since we're not in a component context +vi.mock('vue', async () => { + const actual = await vi.importActual('vue') + return { + ...actual, + onUnmounted: vi.fn(), + } +}) + +import { usePolling } from '@/composables/usePolling' + +describe('usePolling', () => { + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + }) + + it('returns active, start, and stop', () => { + const { active, start, stop } = usePolling(vi.fn().mockResolvedValue(undefined), 1000) + expect(active.value).toBe(false) + expect(typeof start).toBe('function') + expect(typeof stop).toBe('function') + }) + + it('calls fn immediately on start then at intervals', async () => { + vi.useFakeTimers() + const fn = vi.fn().mockResolvedValue(undefined) + const { active, start } = usePolling(fn, 1000) + + start() + expect(active.value).toBe(true) + + // fn is called immediately on start + await vi.advanceTimersByTimeAsync(0) + expect(fn).toHaveBeenCalledTimes(1) + + // After 1 interval + await vi.advanceTimersByTimeAsync(1000) + expect(fn).toHaveBeenCalledTimes(2) + + // After 2 intervals + await vi.advanceTimersByTimeAsync(1000) + expect(fn).toHaveBeenCalledTimes(3) + }) + + it('stop clears timer and sets active to false', async () => { + vi.useFakeTimers() + const fn = vi.fn().mockResolvedValue(undefined) + const { active, start, stop } = usePolling(fn, 1000) + + start() + await vi.advanceTimersByTimeAsync(0) + expect(fn).toHaveBeenCalledTimes(1) + + stop() + expect(active.value).toBe(false) + + await vi.advanceTimersByTimeAsync(3000) + expect(fn).toHaveBeenCalledTimes(1) // no more calls after stop + }) + + it('duplicate start() calls are no-ops', async () => { + vi.useFakeTimers() + const fn = vi.fn().mockResolvedValue(undefined) + const { start } = usePolling(fn, 1000) + + start() + start() // should be no-op + start() // should be no-op + + await vi.advanceTimersByTimeAsync(0) + expect(fn).toHaveBeenCalledTimes(1) // only one immediate call + }) + + it('swallows errors from fn and continues polling', async () => { + vi.useFakeTimers() + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const fn = vi.fn() + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValue(undefined) + const { start } = usePolling(fn, 1000) + + start() + await vi.advanceTimersByTimeAsync(0) // first call — errors + expect(consoleSpy).toHaveBeenCalledWith('Polling error:', expect.any(Error)) + + await vi.advanceTimersByTimeAsync(1000) // second call — succeeds + expect(fn).toHaveBeenCalledTimes(2) + consoleSpy.mockRestore() + }) + + it('does not overlap async calls (waits for previous to finish)', async () => { + vi.useFakeTimers() + let concurrentCalls = 0 + let maxConcurrent = 0 + const fn = vi.fn().mockImplementation(async () => { + concurrentCalls++ + maxConcurrent = Math.max(maxConcurrent, concurrentCalls) + await new Promise((resolve) => setTimeout(resolve, 500)) + concurrentCalls-- + }) + const { start } = usePolling(fn, 100) + + start() + // Advance past several intervals — with setTimeout-based scheduling, + // next tick only starts after previous completes + await vi.advanceTimersByTimeAsync(2000) + expect(maxConcurrent).toBe(1) + }) + + it('throws on interval below minimum', () => { + expect(() => usePolling(vi.fn(), 50)).toThrow('intervalMs must be a finite number >= 100') + }) + + it('throws on non-finite interval', () => { + expect(() => usePolling(vi.fn(), NaN)).toThrow('intervalMs must be a finite number >= 100') + expect(() => usePolling(vi.fn(), Infinity)).toThrow('intervalMs must be a finite number >= 100') + }) +}) diff --git a/web/src/__tests__/router/guards.test.ts b/web/src/__tests__/router/guards.test.ts new file mode 100644 index 0000000000..d464942119 --- /dev/null +++ b/web/src/__tests__/router/guards.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' +import { authGuard } from '@/router/guards' +import { useAuthStore } from '@/stores/auth' + +// Mock the router module (needed by auth store) +vi.mock('@/router', () => ({ + router: { + currentRoute: { value: { path: '/' } }, + push: vi.fn(), + }, +})) + +vi.mock('@/api/endpoints/auth', () => ({ + setup: vi.fn(), + login: vi.fn(), + changePassword: vi.fn(), + getMe: vi.fn(), +})) + +function createRoute(overrides: Partial = {}): RouteLocationNormalized { + return { + path: '/', + name: undefined, + params: {}, + query: {}, + hash: '', + fullPath: '/', + matched: [], + meta: {}, + redirectedFrom: undefined, + ...overrides, + } as RouteLocationNormalized +} + +describe('authGuard', () => { + let next: NavigationGuardNext + + beforeEach(() => { + setActivePinia(createPinia()) + localStorage.clear() + next = vi.fn() + }) + + it('redirects unauthenticated users to /login on protected routes', () => { + const to = createRoute({ path: '/dashboard', fullPath: '/dashboard', meta: {} }) + const from = createRoute() + + authGuard(to, from, next) + expect(next).toHaveBeenCalledWith({ path: '/login', query: { redirect: '/dashboard' } }) + }) + + it('does not add redirect query for root path', () => { + const to = createRoute({ path: '/', fullPath: '/', meta: {} }) + const from = createRoute() + + authGuard(to, from, next) + expect(next).toHaveBeenCalledWith({ path: '/login', query: undefined }) + }) + + it('allows authenticated users to access protected routes', () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + // Re-create Pinia to pick up the token from localStorage + setActivePinia(createPinia()) + // Force the store to read the token + const store = useAuthStore() + expect(store.isAuthenticated).toBe(true) + + const to = createRoute({ path: '/dashboard', meta: {} }) + const from = createRoute() + + authGuard(to, from, next) + expect(next).toHaveBeenCalledWith() + }) + + it('allows unauthenticated users to access public routes', () => { + const to = createRoute({ path: '/login', meta: { requiresAuth: false } }) + const from = createRoute() + + authGuard(to, from, next) + expect(next).toHaveBeenCalledWith() + }) + + it('redirects authenticated users away from public routes to /', () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + setActivePinia(createPinia()) + + const to = createRoute({ path: '/login', meta: { requiresAuth: false } }) + const from = createRoute() + + authGuard(to, from, next) + expect(next).toHaveBeenCalledWith('/') + }) +}) diff --git a/web/src/__tests__/stores/agents.test.ts b/web/src/__tests__/stores/agents.test.ts new file mode 100644 index 0000000000..69105d1254 --- /dev/null +++ b/web/src/__tests__/stores/agents.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useAgentStore } from '@/stores/agents' +import type { AgentConfig, WsEvent } from '@/api/types' + +vi.mock('@/api/endpoints/agents', () => ({ + listAgents: vi.fn(), + getAgent: vi.fn(), + getAutonomy: vi.fn(), + setAutonomy: vi.fn(), +})) + +const mockAgent: AgentConfig = { + id: 'test-uuid-001', + name: 'alice', + role: 'Developer', + level: 'senior', + department: 'engineering', + status: 'active', + model: { + provider: 'test-provider', + model_id: 'example-large-001', + temperature: 0.7, + max_tokens: 4096, + fallback_model: null, + }, + personality: { + traits: [], + communication_style: 'neutral', + risk_tolerance: 'medium', + creativity: 'high', + description: '', + openness: 0.5, + conscientiousness: 0.5, + extraversion: 0.5, + agreeableness: 0.5, + stress_response: 0.5, + decision_making: 'analytical', + collaboration: 'team', + verbosity: 'balanced', + conflict_approach: 'collaborate', + }, + skills: { primary: ['python'], secondary: ['go'] }, + memory: { type: 'session', retention_days: null }, + tools: { access_level: 'standard', allowed: ['file_system', 'git'], denied: [] }, + autonomy_level: null, + hiring_date: '2026-03-01', +} + +describe('useAgentStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('initializes with empty state', () => { + const store = useAgentStore() + expect(store.agents).toEqual([]) + expect(store.total).toBe(0) + }) + + it('handles agent.hired WS event', () => { + const store = useAgentStore() + const event: WsEvent = { + event_type: 'agent.hired', + channel: 'agents', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockAgent }, + } + store.handleWsEvent(event) + expect(store.agents).toHaveLength(1) + expect(store.total).toBe(1) + }) + + it('handles agent.fired WS event', () => { + const store = useAgentStore() + store.agents = [{ ...mockAgent }] + store.total = 1 + const event: WsEvent = { + event_type: 'agent.fired', + channel: 'agents', + timestamp: '2026-03-12T10:01:00Z', + payload: { name: 'alice' }, + } + store.handleWsEvent(event) + expect(store.agents).toHaveLength(0) + expect(store.total).toBe(0) + }) + + it('handles agent.status_changed WS event', () => { + const store = useAgentStore() + store.agents = [{ ...mockAgent }] + const event: WsEvent = { + event_type: 'agent.status_changed', + channel: 'agents', + timestamp: '2026-03-12T10:01:00Z', + payload: { name: 'alice', status: 'on_leave' }, + } + store.handleWsEvent(event) + expect(store.agents[0].status).toBe('on_leave') + }) + + it('does not duplicate agents on repeated agent.hired events', () => { + const store = useAgentStore() + store.agents = [{ ...mockAgent }] + store.total = 1 + const event: WsEvent = { + event_type: 'agent.hired', + channel: 'agents', + timestamp: '2026-03-12T10:01:00Z', + payload: { ...mockAgent }, + } + store.handleWsEvent(event) + expect(store.agents).toHaveLength(1) + expect(store.total).toBe(1) + }) + + it('ignores agent.hired with malformed payload', () => { + const store = useAgentStore() + const event: WsEvent = { + event_type: 'agent.hired', + channel: 'agents', + timestamp: '2026-03-12T10:01:00Z', + payload: { name: 'bob' }, // missing id, role, department + } + store.handleWsEvent(event) + expect(store.agents).toHaveLength(0) + expect(store.total).toBe(0) + }) +}) diff --git a/web/src/__tests__/stores/approvals.test.ts b/web/src/__tests__/stores/approvals.test.ts new file mode 100644 index 0000000000..a3cc2ac76b --- /dev/null +++ b/web/src/__tests__/stores/approvals.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useApprovalStore } from '@/stores/approvals' +import type { ApprovalItem, WsEvent } from '@/api/types' + +const mockListApprovals = vi.fn() +const mockGetApproval = vi.fn() +const mockCreateApproval = vi.fn() +const mockApproveApproval = vi.fn() +const mockRejectApproval = vi.fn() + +vi.mock('@/api/endpoints/approvals', () => ({ + listApprovals: (...args: unknown[]) => mockListApprovals(...args), + getApproval: (...args: unknown[]) => mockGetApproval(...args), + createApproval: (...args: unknown[]) => mockCreateApproval(...args), + approveApproval: (...args: unknown[]) => mockApproveApproval(...args), + rejectApproval: (...args: unknown[]) => mockRejectApproval(...args), +})) + +const mockApproval: ApprovalItem = { + id: 'approval-1', + action_type: 'deploy:production', + title: 'Deploy to prod', + description: 'Deploying v2.0', + requested_by: 'agent-1', + risk_level: 'high', + status: 'pending', + task_id: null, + metadata: {}, + decided_by: null, + decision_reason: null, + created_at: '2026-03-12T10:00:00Z', + decided_at: null, + expires_at: '2026-03-12T11:00:00Z', +} + +describe('useApprovalStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('initializes with empty state', () => { + const store = useApprovalStore() + expect(store.approvals).toEqual([]) + expect(store.pendingCount).toBe(0) + }) + + it('computes pendingCount correctly', () => { + const store = useApprovalStore() + store.approvals = [ + mockApproval, + { ...mockApproval, id: 'approval-2', status: 'approved' }, + ] + expect(store.pendingCount).toBe(1) + }) + + describe('fetchApprovals', () => { + it('fetches and populates approvals', async () => { + mockListApprovals.mockResolvedValue({ data: [mockApproval], total: 1 }) + + const store = useApprovalStore() + await store.fetchApprovals() + + expect(store.approvals).toEqual([mockApproval]) + expect(store.total).toBe(1) + expect(store.loading).toBe(false) + }) + + it('sets error on failure', async () => { + mockListApprovals.mockRejectedValue(new Error('Network error')) + + const store = useApprovalStore() + await store.fetchApprovals() + + expect(store.error).toBe('Network error') + expect(store.loading).toBe(false) + }) + + it('passes filters to API', async () => { + mockListApprovals.mockResolvedValue({ data: [], total: 0 }) + + const store = useApprovalStore() + await store.fetchApprovals({ status: 'pending' }) + + expect(mockListApprovals).toHaveBeenCalledWith({ status: 'pending' }) + }) + }) + + describe('approve', () => { + it('updates approval in list on success', async () => { + const approved = { ...mockApproval, status: 'approved' as const, decided_by: 'admin' } + mockApproveApproval.mockResolvedValue(approved) + + const store = useApprovalStore() + store.approvals = [mockApproval] + const result = await store.approve('approval-1', { comment: 'LGTM' }) + + expect(result).toEqual(approved) + expect(store.approvals[0].status).toBe('approved') + }) + + it('returns null and sets error on failure', async () => { + mockApproveApproval.mockRejectedValue(new Error('Forbidden')) + + const store = useApprovalStore() + store.approvals = [mockApproval] + const result = await store.approve('approval-1') + + expect(result).toBeNull() + expect(store.error).toBe('Forbidden') + }) + + it('clears error before making request', async () => { + mockApproveApproval.mockRejectedValue(new Error('fail')) + + const store = useApprovalStore() + store.approvals = [mockApproval] + + await store.approve('approval-1') + expect(store.error).toBe('fail') + + mockApproveApproval.mockResolvedValue({ ...mockApproval, status: 'approved' }) + await store.approve('approval-1') + expect(store.error).toBeNull() + }) + }) + + describe('reject', () => { + it('updates approval in list on success', async () => { + const rejected = { + ...mockApproval, + status: 'rejected' as const, + decided_by: 'admin', + decision_reason: 'Too risky', + } + mockRejectApproval.mockResolvedValue(rejected) + + const store = useApprovalStore() + store.approvals = [mockApproval] + const result = await store.reject('approval-1', { reason: 'Too risky' }) + + expect(result).toEqual(rejected) + expect(store.approvals[0].status).toBe('rejected') + expect(store.approvals[0].decision_reason).toBe('Too risky') + }) + + it('returns null and sets error on failure', async () => { + mockRejectApproval.mockRejectedValue(new Error('Not found')) + + const store = useApprovalStore() + store.approvals = [mockApproval] + const result = await store.reject('approval-1', { reason: 'test' }) + + expect(result).toBeNull() + expect(store.error).toBe('Not found') + }) + }) + + describe('WS events', () => { + it('handles approval.submitted WS event', () => { + const store = useApprovalStore() + const event: WsEvent = { + event_type: 'approval.submitted', + channel: 'approvals', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockApproval }, + } + store.handleWsEvent(event) + expect(store.approvals).toHaveLength(1) + }) + + it('handles approval.approved WS event', () => { + const store = useApprovalStore() + store.approvals = [mockApproval] + const event: WsEvent = { + event_type: 'approval.approved', + channel: 'approvals', + timestamp: '2026-03-12T10:01:00Z', + payload: { id: 'approval-1', status: 'approved', decided_by: 'admin' }, + } + store.handleWsEvent(event) + expect(store.approvals[0].status).toBe('approved') + }) + + it('handles approval.rejected WS event', () => { + const store = useApprovalStore() + store.approvals = [mockApproval] + const event: WsEvent = { + event_type: 'approval.rejected', + channel: 'approvals', + timestamp: '2026-03-12T10:01:00Z', + payload: { id: 'approval-1', status: 'rejected', decided_by: 'admin', decision_reason: 'Too risky' }, + } + store.handleWsEvent(event) + expect(store.approvals[0].status).toBe('rejected') + }) + + it('handles approval.expired WS event', () => { + const store = useApprovalStore() + store.approvals = [mockApproval] + const event: WsEvent = { + event_type: 'approval.expired', + channel: 'approvals', + timestamp: '2026-03-12T11:01:00Z', + payload: { id: 'approval-1', status: 'expired' }, + } + store.handleWsEvent(event) + expect(store.approvals[0].status).toBe('expired') + }) + + it('does not duplicate approvals on repeated events', () => { + const store = useApprovalStore() + store.approvals = [mockApproval] + store.total = 1 + const event: WsEvent = { + event_type: 'approval.submitted', + channel: 'approvals', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockApproval }, + } + store.handleWsEvent(event) + expect(store.approvals).toHaveLength(1) + expect(store.total).toBe(1) + }) + }) +}) diff --git a/web/src/__tests__/stores/auth.test.ts b/web/src/__tests__/stores/auth.test.ts new file mode 100644 index 0000000000..bc1e323e82 --- /dev/null +++ b/web/src/__tests__/stores/auth.test.ts @@ -0,0 +1,246 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useAuthStore } from '@/stores/auth' + +// Mock the router +vi.mock('@/router', () => ({ + router: { + currentRoute: { value: { path: '/dashboard' } }, + push: vi.fn(), + }, +})) + +// Mock the auth API module +const mockSetup = vi.fn() +const mockLogin = vi.fn() +const mockChangePassword = vi.fn() +const mockGetMe = vi.fn() + +vi.mock('@/api/endpoints/auth', () => ({ + setup: (...args: unknown[]) => mockSetup(...args), + login: (...args: unknown[]) => mockLogin(...args), + changePassword: (...args: unknown[]) => mockChangePassword(...args), + getMe: (...args: unknown[]) => mockGetMe(...args), +})) + +describe('useAuthStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + localStorage.clear() + vi.clearAllMocks() + }) + + it('initializes with no auth', () => { + const store = useAuthStore() + expect(store.isAuthenticated).toBe(false) + expect(store.user).toBeNull() + expect(store.token).toBeNull() + }) + + it('initializes with token from localStorage', () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + const store = useAuthStore() + expect(store.token).toBe('test-token') + expect(store.isAuthenticated).toBe(true) + }) + + it('does not restore expired tokens', () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() - 1000)) + const store = useAuthStore() + expect(store.token).toBeNull() + expect(store.isAuthenticated).toBe(false) + }) + + it('logout clears auth state', () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + const store = useAuthStore() + store.logout() + expect(store.token).toBeNull() + expect(store.user).toBeNull() + expect(store.isAuthenticated).toBe(false) + expect(localStorage.getItem('auth_token')).toBeNull() + }) + + it('mustChangePassword defaults to false', () => { + const store = useAuthStore() + expect(store.mustChangePassword).toBe(false) + }) + + it('userRole is null when no user', () => { + const store = useAuthStore() + expect(store.userRole).toBeNull() + }) + + describe('login', () => { + it('sets token and fetches user on success', async () => { + mockLogin.mockResolvedValue({ + token: 'new-token', + expires_in: 3600, + must_change_password: false, + }) + mockGetMe.mockResolvedValue({ + id: 'user-1', + username: 'admin', + role: 'ceo', + must_change_password: false, + }) + + const store = useAuthStore() + const result = await store.login('admin', 'password123') + + expect(result.token).toBe('new-token') + expect(store.token).toBe('new-token') + expect(store.isAuthenticated).toBe(true) + expect(store.user?.username).toBe('admin') + expect(store.userRole).toBe('ceo') + expect(localStorage.getItem('auth_token')).toBe('new-token') + }) + + it('clears auth if fetchUser fails after login', async () => { + mockLogin.mockResolvedValue({ + token: 'new-token', + expires_in: 3600, + must_change_password: false, + }) + mockGetMe.mockRejectedValue(new Error('Network error')) + + const store = useAuthStore() + await expect(store.login('admin', 'password123')).rejects.toThrow( + 'Login succeeded but failed to load user profile. Please check your connection and try again.', + ) + expect(store.token).toBeNull() + expect(store.isAuthenticated).toBe(false) + }) + + it('sets loading during login', async () => { + const store = useAuthStore() + let loadingDuringCall = false + mockLogin.mockImplementation(() => { + loadingDuringCall = store.loading + return Promise.resolve({ + token: 'new-token', + expires_in: 3600, + must_change_password: false, + }) + }) + mockGetMe.mockResolvedValue({ + id: 'user-1', + username: 'admin', + role: 'ceo', + must_change_password: false, + }) + + await store.login('admin', 'password123') + expect(loadingDuringCall).toBe(true) + expect(store.loading).toBe(false) // cleared in finally + }) + }) + + describe('setup', () => { + it('sets token and fetches user on success', async () => { + mockSetup.mockResolvedValue({ + token: 'setup-token', + expires_in: 3600, + must_change_password: true, + }) + mockGetMe.mockResolvedValue({ + id: 'user-1', + username: 'admin', + role: 'ceo', + must_change_password: true, + }) + + const store = useAuthStore() + const result = await store.setup('admin', 'password123') + + expect(result.token).toBe('setup-token') + expect(store.token).toBe('setup-token') + expect(store.user?.id).toBe('user-1') + expect(store.user?.role).toBe('ceo') + expect(store.mustChangePassword).toBe(true) + expect(mockGetMe).toHaveBeenCalled() + }) + + it('clears auth if fetchUser fails after setup', async () => { + mockSetup.mockResolvedValue({ + token: 'setup-token', + expires_in: 3600, + must_change_password: true, + }) + mockGetMe.mockRejectedValue(new Error('Network error')) + + const store = useAuthStore() + await expect(store.setup('admin', 'password123')).rejects.toThrow( + 'Setup succeeded but failed to load user profile. Please check your connection and try again.', + ) + expect(store.token).toBeNull() + expect(store.isAuthenticated).toBe(false) + }) + }) + + describe('fetchUser', () => { + it('clears auth on 401 response', async () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + const axiosError = Object.assign(new Error('Unauthorized'), { + isAxiosError: true, + response: { status: 401 }, + }) + mockGetMe.mockRejectedValue(axiosError) + + const store = useAuthStore() + await store.fetchUser() + + expect(store.token).toBeNull() + expect(store.isAuthenticated).toBe(false) + }) + + it('does not clear auth on 500 — re-throws', async () => { + localStorage.setItem('auth_token', 'test-token') + localStorage.setItem('auth_token_expires_at', String(Date.now() + 3600_000)) + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + const axiosError = Object.assign(new Error('Internal Server Error'), { + isAxiosError: true, + response: { status: 500 }, + }) + mockGetMe.mockRejectedValue(axiosError) + + const store = useAuthStore() + await expect(store.fetchUser()).rejects.toThrow('Internal Server Error') + + expect(store.token).toBe('test-token') + expect(store.isAuthenticated).toBe(true) + } finally { + consoleSpy.mockRestore() + } + }) + + it('does nothing without token', async () => { + const store = useAuthStore() + await store.fetchUser() + expect(mockGetMe).not.toHaveBeenCalled() + }) + }) + + describe('changePassword', () => { + it('updates user with result', async () => { + const updatedUser = { + id: 'user-1', + username: 'admin', + role: 'ceo' as const, + must_change_password: false, + } + mockChangePassword.mockResolvedValue(updatedUser) + + const store = useAuthStore() + const result = await store.changePassword('old', 'new') + + expect(result).toEqual(updatedUser) + expect(store.user).toEqual(updatedUser) + }) + }) +}) diff --git a/web/src/__tests__/stores/budget.test.ts b/web/src/__tests__/stores/budget.test.ts new file mode 100644 index 0000000000..67c921f192 --- /dev/null +++ b/web/src/__tests__/stores/budget.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useBudgetStore } from '@/stores/budget' +import type { BudgetConfig, CostRecord, AgentSpending, WsEvent } from '@/api/types' + +const mockGetBudgetConfig = vi.fn() +const mockListCostRecords = vi.fn() +const mockGetAgentSpending = vi.fn() + +vi.mock('@/api/endpoints/budget', () => ({ + getBudgetConfig: (...args: unknown[]) => mockGetBudgetConfig(...args), + listCostRecords: (...args: unknown[]) => mockListCostRecords(...args), + getAgentSpending: (...args: unknown[]) => mockGetAgentSpending(...args), +})) + +const mockRecord: CostRecord = { + agent_id: 'alice', + task_id: 'task-1', + provider: 'test-provider', + model: 'example-large-001', + input_tokens: 100, + output_tokens: 50, + cost_usd: 0.005, + timestamp: '2026-03-12T10:00:00Z', + call_category: null, +} + +describe('useBudgetStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('initializes with empty state', () => { + const store = useBudgetStore() + expect(store.config).toBeNull() + expect(store.records).toEqual([]) + expect(store.totalRecords).toBe(0) + expect(store.loading).toBe(false) + expect(store.error).toBeNull() + }) + + describe('fetchConfig', () => { + it('sets config on success', async () => { + const mockConfig: BudgetConfig = { + total_monthly: 1000, + alerts: { warn_at: 0.8, critical_at: 0.95, hard_stop_at: 1.0 }, + per_task_limit: 10, + per_agent_daily_limit: 100, + auto_downgrade: { enabled: false, threshold: 0.9, downgrade_map: [], boundary: 'task_assignment' }, + reset_day: 1, + } + mockGetBudgetConfig.mockResolvedValue(mockConfig) + + const store = useBudgetStore() + await store.fetchConfig() + + expect(store.config).toEqual(mockConfig) + expect(store.loading).toBe(false) + expect(store.error).toBeNull() + }) + + it('sets error on failure', async () => { + mockGetBudgetConfig.mockRejectedValue(new Error('Unauthorized')) + + const store = useBudgetStore() + await store.fetchConfig() + + expect(store.config).toBeNull() + expect(store.error).toBe('Unauthorized') + expect(store.loading).toBe(false) + }) + }) + + describe('fetchRecords', () => { + it('sets records on success', async () => { + mockListCostRecords.mockResolvedValue({ data: [mockRecord], total: 1 }) + + const store = useBudgetStore() + await store.fetchRecords() + + expect(store.records).toEqual([mockRecord]) + expect(store.totalRecords).toBe(1) + expect(store.loading).toBe(false) + }) + + it('sets error on failure', async () => { + mockListCostRecords.mockRejectedValue(new Error('Server error')) + + const store = useBudgetStore() + await store.fetchRecords() + + expect(store.records).toEqual([]) + expect(store.error).toBe('Server error') + }) + }) + + describe('fetchAgentSpending', () => { + it('returns spending on success', async () => { + const mockSpending: AgentSpending = { agent_id: 'alice', total_cost_usd: 1.5 } + mockGetAgentSpending.mockResolvedValue(mockSpending) + + const store = useBudgetStore() + const result = await store.fetchAgentSpending('alice') + + expect(result).toEqual(mockSpending) + expect(store.loading).toBe(false) + expect(store.error).toBeNull() + }) + + it('returns null and sets error on failure', async () => { + mockGetAgentSpending.mockRejectedValue(new Error('Not found')) + + const store = useBudgetStore() + const result = await store.fetchAgentSpending('alice') + + expect(result).toBeNull() + expect(store.error).toBe('Not found') + }) + }) + + describe('WS events', () => { + it('handles budget.record_added WS event', () => { + const store = useBudgetStore() + const event: WsEvent = { + event_type: 'budget.record_added', + channel: 'budget', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockRecord }, + } + store.handleWsEvent(event) + expect(store.records).toHaveLength(1) + expect(store.records[0].cost_usd).toBe(0.005) + expect(store.totalRecords).toBe(1) + }) + + it('ignores WS event with invalid payload', () => { + const store = useBudgetStore() + const event: WsEvent = { + event_type: 'budget.record_added', + channel: 'budget', + timestamp: '2026-03-12T10:00:00Z', + payload: { not_a_record: true }, + } + store.handleWsEvent(event) + expect(store.records).toHaveLength(0) + expect(store.totalRecords).toBe(0) + }) + }) +}) diff --git a/web/src/__tests__/stores/messages.test.ts b/web/src/__tests__/stores/messages.test.ts new file mode 100644 index 0000000000..0e4d3ca564 --- /dev/null +++ b/web/src/__tests__/stores/messages.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useMessageStore } from '@/stores/messages' +import type { WsEvent } from '@/api/types' + +vi.mock('@/api/endpoints/messages', () => ({ + listMessages: vi.fn(), + listChannels: vi.fn(), +})) + +describe('useMessageStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('initializes with empty state', () => { + const store = useMessageStore() + expect(store.messages).toEqual([]) + expect(store.channels).toEqual([]) + expect(store.activeChannel).toBeNull() + }) + + it('handles message.sent WS event', () => { + const store = useMessageStore() + const event: WsEvent = { + event_type: 'message.sent', + channel: 'messages', + timestamp: '2026-03-12T10:00:00Z', + payload: { + id: 'msg-1', + channel: 'general', + sender: 'alice', + content: 'Hello world', + timestamp: '2026-03-12T10:00:00Z', + metadata: {}, + }, + } + store.handleWsEvent(event) + expect(store.messages).toHaveLength(1) + // total is only updated from REST API, not WS events + expect(store.total).toBe(0) + }) + + it('does not increment total for messages filtered by activeChannel', () => { + const store = useMessageStore() + store.setActiveChannel('engineering') + const event: WsEvent = { + event_type: 'message.sent', + channel: 'messages', + timestamp: '2026-03-12T10:00:00Z', + payload: { + id: 'msg-1', + channel: 'general', // does not match activeChannel 'engineering' + sender: 'alice', + content: 'Hello world', + timestamp: '2026-03-12T10:00:00Z', + metadata: {}, + }, + } + store.handleWsEvent(event) + expect(store.messages).toHaveLength(0) + expect(store.total).toBe(0) // not incremented for filtered-out messages + }) + + it('ignores message.sent with malformed payload', () => { + const store = useMessageStore() + const event: WsEvent = { + event_type: 'message.sent', + channel: 'messages', + timestamp: '2026-03-12T10:00:00Z', + payload: { id: 'msg-1' }, // missing channel, sender, content, timestamp + } + store.handleWsEvent(event) + expect(store.messages).toHaveLength(0) + }) + + it('setActiveChannel updates state', () => { + const store = useMessageStore() + store.setActiveChannel('general') + expect(store.activeChannel).toBe('general') + }) +}) diff --git a/web/src/__tests__/stores/tasks.test.ts b/web/src/__tests__/stores/tasks.test.ts new file mode 100644 index 0000000000..d51cf3cb0e --- /dev/null +++ b/web/src/__tests__/stores/tasks.test.ts @@ -0,0 +1,269 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useTaskStore } from '@/stores/tasks' +import type { Task, WsEvent } from '@/api/types' + +const mockListTasks = vi.fn() +const mockCreateTask = vi.fn() +const mockUpdateTask = vi.fn() +const mockTransitionTask = vi.fn() +const mockCancelTask = vi.fn() + +vi.mock('@/api/endpoints/tasks', () => ({ + listTasks: (...args: unknown[]) => mockListTasks(...args), + createTask: (...args: unknown[]) => mockCreateTask(...args), + updateTask: (...args: unknown[]) => mockUpdateTask(...args), + transitionTask: (...args: unknown[]) => mockTransitionTask(...args), + cancelTask: (...args: unknown[]) => mockCancelTask(...args), +})) + +const mockTask: Task = { + id: 'task-1', + title: 'Test Task', + description: 'A test task', + type: 'development', + status: 'created', + priority: 'medium', + project: 'test-project', + created_by: 'agent-1', + assigned_to: null, + reviewers: [], + dependencies: [], + artifacts_expected: [], + acceptance_criteria: [], + estimated_complexity: 'medium', + budget_limit: 10.0, + cost_usd: 0.0, + deadline: null, + max_retries: 3, + parent_task_id: null, + delegation_chain: [], + task_structure: null, + coordination_topology: 'auto', + version: 1, + created_at: '2026-03-12T10:00:00Z', + updated_at: '2026-03-12T10:00:00Z', +} + +describe('useTaskStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('initializes with empty state', () => { + const store = useTaskStore() + expect(store.tasks).toEqual([]) + expect(store.total).toBe(0) + expect(store.loading).toBe(false) + expect(store.error).toBeNull() + }) + + it('computes tasksByStatus correctly', () => { + const store = useTaskStore() + store.tasks = [mockTask, { ...mockTask, id: 'task-2', status: 'in_progress' }] + expect(store.tasksByStatus['created']).toHaveLength(1) + expect(store.tasksByStatus['in_progress']).toHaveLength(1) + }) + + describe('fetchTasks', () => { + it('fetches tasks and sets state', async () => { + mockListTasks.mockResolvedValue({ data: [mockTask], total: 1 }) + + const store = useTaskStore() + await store.fetchTasks({ limit: 10 }) + + expect(store.tasks).toEqual([mockTask]) + expect(store.total).toBe(1) + expect(store.loading).toBe(false) + expect(store.error).toBeNull() + }) + + it('sets error on failure', async () => { + mockListTasks.mockRejectedValue(new Error('Network error')) + + const store = useTaskStore() + await store.fetchTasks() + + expect(store.tasks).toEqual([]) + expect(store.error).toBe('Network error') + expect(store.loading).toBe(false) + }) + }) + + describe('createTask', () => { + it('appends task to list on success', async () => { + const newTask = { ...mockTask, id: 'task-new' } + mockCreateTask.mockResolvedValue(newTask) + + const store = useTaskStore() + store.total = 0 + const result = await store.createTask({ + title: 'New Task', + description: 'desc', + type: 'development', + project: 'test', + created_by: 'agent-1', + }) + + expect(result).toEqual(newTask) + expect(store.tasks).toHaveLength(1) + expect(store.total).toBe(1) + }) + + it('returns null and sets error on failure', async () => { + mockCreateTask.mockRejectedValue(new Error('Conflict')) + + const store = useTaskStore() + const result = await store.createTask({ + title: 'New', + description: 'desc', + type: 'development', + project: 'test', + created_by: 'agent-1', + }) + + expect(result).toBeNull() + expect(store.error).toBe('Conflict') + }) + }) + + describe('updateTask', () => { + it('replaces task in list on success', async () => { + const updated = { ...mockTask, title: 'Updated Title' } + mockUpdateTask.mockResolvedValue(updated) + + const store = useTaskStore() + store.tasks = [mockTask] + const result = await store.updateTask('task-1', { title: 'Updated Title' }) + + expect(result).toEqual(updated) + expect(store.tasks[0].title).toBe('Updated Title') + }) + + it('returns null on failure', async () => { + mockUpdateTask.mockRejectedValue(new Error('Not found')) + + const store = useTaskStore() + const result = await store.updateTask('task-1', { title: 'x' }) + + expect(result).toBeNull() + expect(store.error).toBe('Not found') + }) + }) + + describe('transitionTask', () => { + it('replaces task in list on success', async () => { + const transitioned = { ...mockTask, status: 'assigned' as const } + mockTransitionTask.mockResolvedValue(transitioned) + + const store = useTaskStore() + store.tasks = [mockTask] + const result = await store.transitionTask('task-1', { + target_status: 'assigned', + expected_version: 1, + }) + + expect(result).toEqual(transitioned) + expect(store.tasks[0].status).toBe('assigned') + }) + + it('returns null and sets error on failure', async () => { + mockTransitionTask.mockRejectedValue(new Error('Version conflict')) + + const store = useTaskStore() + store.tasks = [mockTask] + const result = await store.transitionTask('task-1', { + target_status: 'assigned', + expected_version: 1, + }) + + expect(result).toBeNull() + expect(store.error).toBe('Version conflict') + expect(store.tasks[0].status).toBe('created') + }) + }) + + describe('cancelTask', () => { + it('replaces task in list on success', async () => { + const cancelled = { ...mockTask, status: 'cancelled' as const } + mockCancelTask.mockResolvedValue(cancelled) + + const store = useTaskStore() + store.tasks = [mockTask] + const result = await store.cancelTask('task-1', { reason: 'done' }) + + expect(result).toEqual(cancelled) + expect(store.tasks[0].status).toBe('cancelled') + }) + + it('returns null and sets error on failure', async () => { + mockCancelTask.mockRejectedValue(new Error('Forbidden')) + + const store = useTaskStore() + store.tasks = [mockTask] + const result = await store.cancelTask('task-1', { reason: 'done' }) + + expect(result).toBeNull() + expect(store.error).toBe('Forbidden') + expect(store.tasks[0].status).toBe('created') + }) + }) + + describe('WS events', () => { + it('handles task.created WS event', () => { + const store = useTaskStore() + const event: WsEvent = { + event_type: 'task.created', + channel: 'tasks', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockTask }, + } + store.handleWsEvent(event) + expect(store.tasks).toHaveLength(1) + expect(store.total).toBe(1) + }) + + it('handles task.updated WS event', () => { + const store = useTaskStore() + store.tasks = [mockTask] + const event: WsEvent = { + event_type: 'task.updated', + channel: 'tasks', + timestamp: '2026-03-12T10:01:00Z', + payload: { id: 'task-1', title: 'Updated Title' }, + } + store.handleWsEvent(event) + expect(store.tasks[0].title).toBe('Updated Title') + }) + + it('does not duplicate tasks on repeated task.created events', () => { + const store = useTaskStore() + store.tasks = [mockTask] + store.total = 1 + const event: WsEvent = { + event_type: 'task.created', + channel: 'tasks', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockTask }, + } + store.handleWsEvent(event) + expect(store.tasks).toHaveLength(1) + }) + + it('skips task.created WS events when filters are active', () => { + const store = useTaskStore() + store.currentFilters = { status: 'in_progress' } + const event: WsEvent = { + event_type: 'task.created', + channel: 'tasks', + timestamp: '2026-03-12T10:00:00Z', + payload: { ...mockTask, id: 'task-new', status: 'created' }, + } + store.handleWsEvent(event) + // Should NOT append — filters are active, let next fetch sync the list + expect(store.tasks).toHaveLength(0) + expect(store.total).toBe(0) + }) + }) +}) diff --git a/web/src/__tests__/stores/websocket.test.ts b/web/src/__tests__/stores/websocket.test.ts new file mode 100644 index 0000000000..50622afee6 --- /dev/null +++ b/web/src/__tests__/stores/websocket.test.ts @@ -0,0 +1,368 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useWebSocketStore } from '@/stores/websocket' +import type { WsEvent } from '@/api/types' + +// Track all created MockWebSocket instances +let mockInstances: MockWebSocket[] = [] + +// Mock WebSocket +class MockWebSocket { + static CONNECTING = 0 + static OPEN = 1 + static CLOSING = 2 + static CLOSED = 3 + + readyState = MockWebSocket.CONNECTING + url: string + onopen: (() => void) | null = null + onclose: (() => void) | null = null + onmessage: ((event: { data: string }) => void) | null = null + onerror: ((event: unknown) => void) | null = null + send = vi.fn() + close = vi.fn() + + constructor(url: string) { + this.url = url + mockInstances.push(this) + // Schedule open event + setTimeout(() => { + this.readyState = MockWebSocket.OPEN + this.onopen?.() + }, 0) + } +} + +// Store original WebSocket +const OriginalWebSocket = globalThis.WebSocket + +beforeEach(() => { + mockInstances = [] + // @ts-expect-error -- mock WebSocket for testing + globalThis.WebSocket = MockWebSocket +}) + +afterEach(() => { + globalThis.WebSocket = OriginalWebSocket +}) + +describe('useWebSocketStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() + }) + + it('initializes with disconnected state', () => { + const store = useWebSocketStore() + expect(store.connected).toBe(false) + expect(store.reconnectExhausted).toBe(false) + expect(store.subscribedChannels).toEqual([]) + }) + + it('connects and sets connected to true', async () => { + const store = useWebSocketStore() + store.connect('test-token') + + await vi.advanceTimersByTimeAsync(0) + expect(store.connected).toBe(true) + }) + + it('does not create duplicate connections', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + expect(mockInstances).toHaveLength(1) + + store.connect('test-token') // should be no-op + expect(mockInstances).toHaveLength(1) // no new WebSocket created + }) + + it('queues subscriptions when not connected and does not call send', () => { + const store = useWebSocketStore() + // Don't connect first — subscribe while disconnected + store.subscribe(['tasks', 'agents']) + + // No WebSocket exists, so no send should have been called + expect(mockInstances).toHaveLength(0) + // Verify subscription is queued by connecting and checking send was called + store.connect('test-token') + }) + + it('replays pending subscriptions on connect', async () => { + const store = useWebSocketStore() + store.subscribe(['tasks']) + store.connect('test-token') + + await vi.advanceTimersByTimeAsync(0) + expect(store.connected).toBe(true) + // The pending subscription should have been sent on connect + const ws = mockInstances[0] + expect(ws.send).toHaveBeenCalledWith( + expect.stringContaining('"action":"subscribe"'), + ) + expect(ws.send).toHaveBeenCalledWith( + expect.stringContaining('"channels":["tasks"]'), + ) + }) + + it('deduplicates pending subscriptions', async () => { + const store = useWebSocketStore() + // Subscribe to same channels multiple times while disconnected + store.subscribe(['tasks', 'agents']) + store.subscribe(['tasks', 'agents']) + store.subscribe(['tasks', 'agents']) + + // Connect and verify only one subscribe message is sent (not three) + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const ws = mockInstances[0] + const subscribeCalls = ws.send.mock.calls.filter((call: unknown[]) => + String(call[0]).includes('"action":"subscribe"'), + ) + expect(subscribeCalls).toHaveLength(1) + }) + + it('disconnect sets state correctly', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + expect(store.connected).toBe(true) + + store.disconnect() + expect(store.connected).toBe(false) + expect(store.subscribedChannels).toEqual([]) + }) + + it('dispatches events to channel handlers via onmessage', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const handler = vi.fn() + store.onChannelEvent('tasks', handler) + + // Simulate incoming message via the mock WebSocket instance + const event: WsEvent = { + event_type: 'task.created', + channel: 'tasks', + timestamp: '2026-03-12T10:00:00Z', + payload: { id: 'task-1' }, + } + const ws = mockInstances[0] + ws.onmessage?.({ data: JSON.stringify(event) }) + + expect(handler).toHaveBeenCalledTimes(1) + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ event_type: 'task.created', channel: 'tasks' }), + ) + + // Remove handler and verify no more calls + store.offChannelEvent('tasks', handler) + ws.onmessage?.({ data: JSON.stringify(event) }) + expect(handler).toHaveBeenCalledTimes(1) // still 1, not 2 + }) + + it('wildcard handlers receive events from all channels', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const handler = vi.fn() + store.onChannelEvent('*', handler) + + const ws = mockInstances[0] + ws.onmessage?.({ + data: JSON.stringify({ + event_type: 'task.created', + channel: 'tasks', + timestamp: '2026-03-12T10:00:00Z', + payload: {}, + }), + }) + ws.onmessage?.({ + data: JSON.stringify({ + event_type: 'approval.submitted', + channel: 'approvals', + timestamp: '2026-03-12T10:00:00Z', + payload: {}, + }), + }) + + expect(handler).toHaveBeenCalledTimes(2) + store.offChannelEvent('*', handler) + }) + + it('handles malformed JSON messages gracefully', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const ws = mockInstances[0] + ws.onmessage?.({ data: 'not valid json{{{' }) + + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to parse WebSocket message:', + expect.any(SyntaxError), + ) + consoleSpy.mockRestore() + }) + + it('subscription ack updates subscribedChannels when array is valid', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const ws = mockInstances[0] + // Simulate subscription ack + ws.onmessage?.({ + data: JSON.stringify({ action: 'subscribed', channels: ['tasks', 'approvals'] }), + }) + expect(store.subscribedChannels).toEqual(['tasks', 'approvals']) + + // Non-array channels should not crash + ws.onmessage?.({ + data: JSON.stringify({ action: 'subscribed', channels: 'invalid' }), + }) + // Should still have the previous valid value + expect(store.subscribedChannels).toEqual(['tasks', 'approvals']) + }) + + it('scheduleReconnect stops after max attempts', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + expect(store.connected).toBe(true) + + // Replace with a WebSocket mock that immediately fails (never opens) + // @ts-expect-error -- override mock for this test + globalThis.WebSocket = class FailingWebSocket { + static CONNECTING = 0 + static OPEN = 1 + static CLOSING = 2 + static CLOSED = 3 + readyState = 0 + url: string + onopen: (() => void) | null = null + onclose: (() => void) | null = null + onmessage: ((event: { data: string }) => void) | null = null + onerror: ((event: unknown) => void) | null = null + send = vi.fn() + close = vi.fn() + constructor(url: string) { + this.url = url + // Simulate immediate connection failure — only fire onclose, never onopen + setTimeout(() => { + this.readyState = 3 // CLOSED + this.onclose?.() + }, 0) + } + } + + // Trigger initial disconnect + const ws = mockInstances[mockInstances.length - 1] + ws.readyState = MockWebSocket.CLOSED + ws.onclose?.() + + // Drive through all 20 reconnect attempts + for (let i = 0; i < 25; i++) { + await vi.advanceTimersByTimeAsync(120_000) + } + + expect(store.reconnectExhausted).toBe(true) + consoleSpy.mockRestore() + }) + + it('re-subscribes to active subscriptions on reconnect', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + // Subscribe while connected + store.subscribe(['tasks']) + const ws1 = mockInstances[0] + expect(ws1.send).toHaveBeenCalled() + + // Simulate disconnect and reconnect + ws1.readyState = MockWebSocket.CLOSED + ws1.onclose?.() + await vi.advanceTimersByTimeAsync(5_000) // trigger reconnect + + // New WebSocket instance should have been created + expect(mockInstances.length).toBeGreaterThan(1) + const ws2 = mockInstances[mockInstances.length - 1] + await vi.advanceTimersByTimeAsync(0) // trigger onopen + + // Active subscriptions should be re-sent automatically + expect(ws2.send).toHaveBeenCalledWith( + expect.stringContaining('"channels":["tasks"]'), + ) + }) + + it('unsubscribe removes channels from active subscriptions so reconnect does not re-subscribe', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + // Subscribe then unsubscribe + store.subscribe(['tasks']) + store.unsubscribe(['tasks']) + + // Simulate disconnect and reconnect + const ws1 = mockInstances[0] + ws1.readyState = MockWebSocket.CLOSED + ws1.onclose?.() + await vi.advanceTimersByTimeAsync(5_000) // trigger reconnect + + const ws2 = mockInstances[mockInstances.length - 1] + await vi.advanceTimersByTimeAsync(0) // trigger onopen + + // Should NOT re-subscribe to 'tasks' since it was unsubscribed + const subscribeCalls = ws2.send.mock.calls.filter((call: unknown[]) => + String(call[0]).includes('"channels":["tasks"]'), + ) + expect(subscribeCalls).toHaveLength(0) + }) + + it('sanitizes error messages from server', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const ws = mockInstances[0] + // Send a message with newlines (log injection attempt) + ws.onmessage?.({ + data: JSON.stringify({ error: 'bad\ninput\rwith newlines' }), + }) + + expect(consoleSpy).toHaveBeenCalledWith( + 'WebSocket error:', + 'bad input with newlines', + ) + consoleSpy.mockRestore() + }) + + it('send failures queue subscriptions for replay', async () => { + const store = useWebSocketStore() + store.connect('test-token') + await vi.advanceTimersByTimeAsync(0) + + const ws = mockInstances[0] + // Make send throw to simulate CLOSING state + ws.send.mockImplementation(() => { + throw new Error('WebSocket is in CLOSING state') + }) + + store.subscribe(['budget']) + // Should not throw — caught internally and queued for replay + expect(store.connected).toBe(true) + }) +}) diff --git a/web/src/__tests__/utils/constants.test.ts b/web/src/__tests__/utils/constants.test.ts new file mode 100644 index 0000000000..4465a75b33 --- /dev/null +++ b/web/src/__tests__/utils/constants.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest' +import { TASK_STATUS_ORDER, TERMINAL_STATUSES, VALID_TRANSITIONS, NAV_ITEMS } from '@/utils/constants' + +describe('TASK_STATUS_ORDER', () => { + it('contains all 9 statuses', () => { + expect(TASK_STATUS_ORDER).toHaveLength(9) + }) + + it('starts with created', () => { + expect(TASK_STATUS_ORDER[0]).toBe('created') + }) +}) + +describe('TERMINAL_STATUSES', () => { + it('contains exactly 2 terminal statuses', () => { + expect(TERMINAL_STATUSES.size).toBe(2) + }) + + it('contains completed and cancelled', () => { + expect(TERMINAL_STATUSES.has('completed')).toBe(true) + expect(TERMINAL_STATUSES.has('cancelled')).toBe(true) + }) + + it('does not contain in_progress', () => { + expect(TERMINAL_STATUSES.has('in_progress')).toBe(false) + }) +}) + +describe('VALID_TRANSITIONS', () => { + it('created can transition to assigned', () => { + expect(VALID_TRANSITIONS['created']).toContain('assigned') + }) + + it('completed has no transitions', () => { + expect(VALID_TRANSITIONS['completed']).toEqual([]) + }) + + it('in_progress can go to in_review', () => { + expect(VALID_TRANSITIONS['in_progress']).toContain('in_review') + }) +}) + +describe('NAV_ITEMS', () => { + it('has dashboard as first item', () => { + expect(NAV_ITEMS[0].label).toBe('Dashboard') + expect(NAV_ITEMS[0].to).toBe('/') + }) + + it('has settings as last item', () => { + expect(NAV_ITEMS[NAV_ITEMS.length - 1].label).toBe('Settings') + }) +}) diff --git a/web/src/__tests__/utils/errors.test.ts b/web/src/__tests__/utils/errors.test.ts new file mode 100644 index 0000000000..53154a1a57 --- /dev/null +++ b/web/src/__tests__/utils/errors.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect } from 'vitest' +import { getErrorMessage, isAxiosError } from '@/utils/errors' + +describe('isAxiosError', () => { + it('returns false for plain errors', () => { + expect(isAxiosError(new Error('test'))).toBe(false) + }) + + it('returns true for axios-like errors', () => { + const axiosError = { isAxiosError: true, response: { status: 400 } } + expect(isAxiosError(axiosError)).toBe(true) + }) +}) + +describe('getErrorMessage', () => { + it('extracts message from Error', () => { + expect(getErrorMessage(new Error('test message'))).toBe('test message') + }) + + it('returns generic message for unknown errors', () => { + expect(getErrorMessage(42)).toBe('An unexpected error occurred.') + }) + + it('extracts API error from axios response', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 400, + data: { error: 'Bad input', success: false, data: null }, + }, + } + expect(getErrorMessage(axiosError)).toBe('Bad input') + }) + + it('returns status-based message for 401', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 401, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('Authentication required. Please log in.') + }) + + it('returns status-based message for 403', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 403, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('You do not have permission to perform this action.') + }) + + it('returns status-based message for 409', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 409, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toContain('Conflict') + }) + + it('returns status-based message for 422', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 422, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('Validation error. Please check your input.') + }) + + it('returns status-based message for 429', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 429, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('Too many requests. Please try again in a moment.') + }) + + it('returns network error for no response', () => { + const axiosError = { + isAxiosError: true, + response: undefined, + } + expect(getErrorMessage(axiosError)).toBe('Network error. Please check your connection.') + }) + + it('returns generic message for 5xx responses (does not leak internals)', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 500, + data: { error: 'Internal: database pool exhausted at line 42', success: false }, + }, + } + expect(getErrorMessage(axiosError)).toBe('A server error occurred. Please try again later.') + }) + + it('returns status-based message for 404', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 404, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('The requested resource was not found.') + }) + + it('returns status-based message for 503', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 503, + data: {}, + }, + } + expect(getErrorMessage(axiosError)).toBe('Service temporarily unavailable. Please try again later.') + }) + + it('returns generic message for JSON-like Error.message', () => { + const err = new Error('{"stack":"at Object. (/app/server.js:15:7)"}') + expect(getErrorMessage(err)).toBe('An unexpected error occurred.') + }) +}) diff --git a/web/src/__tests__/utils/format.test.ts b/web/src/__tests__/utils/format.test.ts new file mode 100644 index 0000000000..b6a45dd282 --- /dev/null +++ b/web/src/__tests__/utils/format.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest' +import { + formatDate, + formatRelativeTime, + formatCurrency, + formatNumber, + formatUptime, + formatLabel, +} from '@/utils/format' + +describe('formatDate', () => { + it('returns dash for null', () => { + expect(formatDate(null)).toBe('—') + }) + + it('returns dash for undefined', () => { + expect(formatDate(undefined)).toBe('—') + }) + + it('formats valid ISO date', () => { + const result = formatDate('2026-03-12T10:30:00Z') + expect(result).toContain('2026') + // Use numeric month check to avoid locale sensitivity + expect(result).toMatch(/12|Mar/) + }) + + it('returns dash for invalid date string', () => { + expect(formatDate('not-a-date')).toBe('—') + }) +}) + +describe('formatRelativeTime', () => { + it('returns dash for null', () => { + expect(formatRelativeTime(null)).toBe('—') + }) + + it('returns "just now" for recent timestamps', () => { + const recent = new Date(Date.now() - 5_000).toISOString() + expect(formatRelativeTime(recent)).toBe('just now') + }) + + it('returns formatted date for future timestamps', () => { + const future = new Date(Date.now() + 60_000).toISOString() + // Future dates fall through to formatDate instead of 'just now' + const result = formatRelativeTime(future) + expect(result).not.toBe('just now') + expect(result).not.toBe('—') + }) + + it('returns dash for invalid date string', () => { + expect(formatRelativeTime('not-a-date')).toBe('—') + }) +}) + +describe('formatCurrency', () => { + it('formats zero', () => { + expect(formatCurrency(0)).toBe('$0.00') + }) + + it('formats positive value', () => { + const result = formatCurrency(123.4567) + expect(result).toContain('$') + expect(result).toContain('123') + }) + + it('formats negative value', () => { + const result = formatCurrency(-45.67) + expect(result).toContain('$') + expect(result).toContain('45') + }) +}) + +describe('formatNumber', () => { + it('formats integer', () => { + expect(formatNumber(1234)).toBe('1,234') + }) + + it('formats zero', () => { + expect(formatNumber(0)).toBe('0') + }) +}) + +describe('formatUptime', () => { + it('formats seconds to minutes', () => { + expect(formatUptime(120)).toBe('2m') + }) + + it('formats hours and minutes', () => { + expect(formatUptime(3720)).toBe('1h 2m') + }) + + it('formats round hours without trailing 0m', () => { + expect(formatUptime(3600)).toBe('1h') + }) + + it('formats days hours and minutes', () => { + expect(formatUptime(90060)).toBe('1d 1h 1m') + }) + + it('formats zero seconds as 0m', () => { + expect(formatUptime(0)).toBe('0m') + }) +}) + +describe('formatLabel', () => { + it('formats snake_case', () => { + expect(formatLabel('in_progress')).toBe('In Progress') + }) + + it('formats single word', () => { + expect(formatLabel('active')).toBe('Active') + }) +}) diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000000..47068d8658 --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,84 @@ +/** + * Axios client with JWT interceptor and ApiResponse envelope unwrapping. + */ + +import axios, { type AxiosError, type AxiosResponse } from 'axios' +import type { ApiResponse, PaginatedResponse } from './types' + +// Normalize: strip trailing slashes and any existing /api/v1 suffix +const RAW_BASE = (import.meta.env.VITE_API_BASE_URL as string) || '' +const BASE_URL = RAW_BASE.replace(/\/+$/, '').replace(/\/api\/v1\/?$/, '') + +export const apiClient = axios.create({ + baseURL: `${BASE_URL}/api/v1`, + headers: { 'Content-Type': 'application/json' }, + timeout: 30_000, +}) + +// ── Request interceptor: attach JWT ────────────────────────── + +apiClient.interceptors.request.use((config) => { + const token = localStorage.getItem('auth_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// ── Response interceptor: 401 redirect + error passthrough ── + +apiClient.interceptors.response.use( + (response: AxiosResponse) => response, + (error: AxiosError<{ error?: string; success?: boolean }>) => { + if (error.response?.status === 401) { + localStorage.removeItem('auth_token') + localStorage.removeItem('auth_token_expires_at') + // Sync Pinia auth state — dynamic import avoids circular dependency + import('@/stores/auth').then(({ useAuthStore }) => { + const auth = useAuthStore() + // clearAuth() also handles redirect to /login + auth.logout() + }).catch(() => { + // Fallback if store import fails: redirect directly + if (window.location.pathname !== '/login' && window.location.pathname !== '/setup') { + window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}` + } + }) + } + return Promise.reject(error) + }, +) + +/** + * Extract data from an ApiResponse envelope. + * Throws if the response indicates an error. + */ +export function unwrap(response: AxiosResponse>): T { + const body = response.data + if (!body.success || body.data === null || body.data === undefined) { + throw new Error(body.error ?? 'Unknown API error') + } + return body.data +} + +/** + * Extract data from a paginated response. + * Validates the response structure to avoid cryptic TypeErrors. + */ +export function unwrapPaginated( + response: AxiosResponse>, +): { data: T[]; total: number; offset: number; limit: number } { + const body = response.data + if (!body.success) { + throw new Error(body.error ?? 'Unknown API error') + } + if (!body.pagination || !Array.isArray(body.data)) { + throw new Error('Unexpected API response format') + } + return { + data: body.data, + total: body.pagination.total, + offset: body.pagination.offset, + limit: body.pagination.limit, + } +} diff --git a/web/src/api/endpoints/agents.ts b/web/src/api/endpoints/agents.ts new file mode 100644 index 0000000000..81dd5cbf8e --- /dev/null +++ b/web/src/api/endpoints/agents.ts @@ -0,0 +1,25 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { AgentConfig, ApiResponse, AutonomyLevelRequest, AutonomyLevelResponse, PaginatedResponse, PaginationParams } from '../types' + +export async function listAgents(params?: PaginationParams) { + const response = await apiClient.get>('/agents', { params }) + return unwrapPaginated(response) +} + +export async function getAgent(name: string): Promise { + const response = await apiClient.get>(`/agents/${encodeURIComponent(name)}`) + return unwrap(response) +} + +export async function getAutonomy(agentId: string): Promise { + const response = await apiClient.get>(`/agents/${encodeURIComponent(agentId)}/autonomy`) + return unwrap(response) +} + +export async function setAutonomy( + agentId: string, + data: AutonomyLevelRequest, +): Promise { + const response = await apiClient.post>(`/agents/${encodeURIComponent(agentId)}/autonomy`, data) + return unwrap(response) +} diff --git a/web/src/api/endpoints/analytics.ts b/web/src/api/endpoints/analytics.ts new file mode 100644 index 0000000000..a4ecb69c71 --- /dev/null +++ b/web/src/api/endpoints/analytics.ts @@ -0,0 +1,7 @@ +import { apiClient, unwrap } from '../client' +import type { ApiResponse, OverviewMetrics } from '../types' + +export async function getOverviewMetrics(): Promise { + const response = await apiClient.get>('/analytics/overview') + return unwrap(response) +} diff --git a/web/src/api/endpoints/approvals.ts b/web/src/api/endpoints/approvals.ts new file mode 100644 index 0000000000..9af3956121 --- /dev/null +++ b/web/src/api/endpoints/approvals.ts @@ -0,0 +1,35 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { + ApiResponse, + ApprovalFilters, + ApprovalItem, + ApproveRequest, + CreateApprovalRequest, + PaginatedResponse, + RejectRequest, +} from '../types' + +export async function listApprovals(filters?: ApprovalFilters) { + const response = await apiClient.get>('/approvals', { params: filters }) + return unwrapPaginated(response) +} + +export async function getApproval(id: string): Promise { + const response = await apiClient.get>(`/approvals/${encodeURIComponent(id)}`) + return unwrap(response) +} + +export async function createApproval(data: CreateApprovalRequest): Promise { + const response = await apiClient.post>('/approvals', data) + return unwrap(response) +} + +export async function approveApproval(id: string, data?: ApproveRequest): Promise { + const response = await apiClient.post>(`/approvals/${encodeURIComponent(id)}/approve`, data ?? {}) + return unwrap(response) +} + +export async function rejectApproval(id: string, data: RejectRequest): Promise { + const response = await apiClient.post>(`/approvals/${encodeURIComponent(id)}/reject`, data) + return unwrap(response) +} diff --git a/web/src/api/endpoints/auth.ts b/web/src/api/endpoints/auth.ts new file mode 100644 index 0000000000..00195f4395 --- /dev/null +++ b/web/src/api/endpoints/auth.ts @@ -0,0 +1,29 @@ +import { apiClient, unwrap } from '../client' +import type { + ApiResponse, + ChangePasswordRequest, + LoginRequest, + SetupRequest, + TokenResponse, + UserInfoResponse, +} from '../types' + +export async function setup(data: SetupRequest): Promise { + const response = await apiClient.post>('/auth/setup', data) + return unwrap(response) +} + +export async function login(data: LoginRequest): Promise { + const response = await apiClient.post>('/auth/login', data) + return unwrap(response) +} + +export async function changePassword(data: ChangePasswordRequest): Promise { + const response = await apiClient.post>('/auth/change-password', data) + return unwrap(response) +} + +export async function getMe(): Promise { + const response = await apiClient.get>('/auth/me') + return unwrap(response) +} diff --git a/web/src/api/endpoints/budget.ts b/web/src/api/endpoints/budget.ts new file mode 100644 index 0000000000..b05d422319 --- /dev/null +++ b/web/src/api/endpoints/budget.ts @@ -0,0 +1,19 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { AgentSpending, ApiResponse, BudgetConfig, CostRecord, PaginatedResponse, PaginationParams } from '../types' + +export async function getBudgetConfig(): Promise { + const response = await apiClient.get>('/budget/config') + return unwrap(response) +} + +export async function listCostRecords( + params?: PaginationParams & { agent_id?: string; task_id?: string }, +) { + const response = await apiClient.get>('/budget/records', { params }) + return unwrapPaginated(response) +} + +export async function getAgentSpending(agentId: string): Promise { + const response = await apiClient.get>(`/budget/agents/${encodeURIComponent(agentId)}`) + return unwrap(response) +} diff --git a/web/src/api/endpoints/company.ts b/web/src/api/endpoints/company.ts new file mode 100644 index 0000000000..ad1594365d --- /dev/null +++ b/web/src/api/endpoints/company.ts @@ -0,0 +1,17 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { ApiResponse, CompanyConfig, Department, PaginatedResponse, PaginationParams } from '../types' + +export async function getCompanyConfig(): Promise { + const response = await apiClient.get>('/company') + return unwrap(response) +} + +export async function listDepartments(params?: PaginationParams): Promise<{ data: Department[]; total: number; offset: number; limit: number }> { + const response = await apiClient.get>('/departments', { params }) + return unwrapPaginated(response) +} + +export async function getDepartment(name: string): Promise { + const response = await apiClient.get>(`/departments/${encodeURIComponent(name)}`) + return unwrap(response) +} diff --git a/web/src/api/endpoints/health.ts b/web/src/api/endpoints/health.ts new file mode 100644 index 0000000000..755eae6b5d --- /dev/null +++ b/web/src/api/endpoints/health.ts @@ -0,0 +1,7 @@ +import { apiClient, unwrap } from '../client' +import type { ApiResponse, HealthStatus } from '../types' + +export async function getHealth(): Promise { + const response = await apiClient.get>('/health') + return unwrap(response) +} diff --git a/web/src/api/endpoints/messages.ts b/web/src/api/endpoints/messages.ts new file mode 100644 index 0000000000..55c7dc8c13 --- /dev/null +++ b/web/src/api/endpoints/messages.ts @@ -0,0 +1,12 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { ApiResponse, Channel, Message, PaginatedResponse, PaginationParams } from '../types' + +export async function listMessages(params?: PaginationParams & { channel?: string }): Promise<{ data: Message[]; total: number; offset: number; limit: number }> { + const response = await apiClient.get>('/messages', { params }) + return unwrapPaginated(response) +} + +export async function listChannels(): Promise { + const response = await apiClient.get>('/messages/channels') + return unwrap(response) +} diff --git a/web/src/api/endpoints/providers.ts b/web/src/api/endpoints/providers.ts new file mode 100644 index 0000000000..3b5ee7e01e --- /dev/null +++ b/web/src/api/endpoints/providers.ts @@ -0,0 +1,30 @@ +import { apiClient, unwrap } from '../client' +import type { ApiResponse, ProviderConfig, ProviderModelConfig } from '../types' + +/** Strip api_key from a single provider config. */ +function stripSecrets(raw: ProviderConfig & { api_key?: unknown }): ProviderConfig { + const { api_key: _discarded, ...safe } = raw + return safe +} + +export async function listProviders(): Promise> { + const response = await apiClient.get>>('/providers') + const raw = unwrap>(response) + const result: Record = Object.create(null) as Record + for (const [key, provider] of Object.entries(raw)) { + // Skip prototype-polluting keys + if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue + result[key] = stripSecrets(provider) + } + return result +} + +export async function getProvider(name: string): Promise { + const response = await apiClient.get>(`/providers/${encodeURIComponent(name)}`) + return stripSecrets(unwrap(response)) +} + +export async function getProviderModels(name: string): Promise { + const response = await apiClient.get>(`/providers/${encodeURIComponent(name)}/models`) + return unwrap(response) +} diff --git a/web/src/api/endpoints/tasks.ts b/web/src/api/endpoints/tasks.ts new file mode 100644 index 0000000000..32044fee9a --- /dev/null +++ b/web/src/api/endpoints/tasks.ts @@ -0,0 +1,45 @@ +import { apiClient, unwrap, unwrapPaginated } from '../client' +import type { + ApiResponse, + CancelTaskRequest, + CreateTaskRequest, + PaginatedResponse, + Task, + TaskFilters, + TransitionTaskRequest, + UpdateTaskRequest, +} from '../types' + +export async function listTasks(filters?: TaskFilters) { + const response = await apiClient.get>('/tasks', { params: filters }) + return unwrapPaginated(response) +} + +export async function getTask(taskId: string): Promise { + const response = await apiClient.get>(`/tasks/${encodeURIComponent(taskId)}`) + return unwrap(response) +} + +export async function createTask(data: CreateTaskRequest): Promise { + const response = await apiClient.post>('/tasks', data) + return unwrap(response) +} + +export async function updateTask(taskId: string, data: UpdateTaskRequest): Promise { + const response = await apiClient.patch>(`/tasks/${encodeURIComponent(taskId)}`, data) + return unwrap(response) +} + +export async function transitionTask(taskId: string, data: TransitionTaskRequest): Promise { + const response = await apiClient.post>(`/tasks/${encodeURIComponent(taskId)}/transition`, data) + return unwrap(response) +} + +export async function cancelTask(taskId: string, data: CancelTaskRequest): Promise { + const response = await apiClient.post>(`/tasks/${encodeURIComponent(taskId)}/cancel`, data) + return unwrap(response) +} + +export async function deleteTask(taskId: string): Promise { + await apiClient.delete(`/tasks/${encodeURIComponent(taskId)}`) +} diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 0000000000..f23fa399d8 --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,555 @@ +/** TypeScript interfaces mirroring backend Pydantic DTOs and domain models. */ + +// ── Enums ──────────────────────────────────────────────────── + +export type TaskStatus = + | 'created' + | 'assigned' + | 'in_progress' + | 'in_review' + | 'completed' + | 'blocked' + | 'failed' + | 'interrupted' + | 'cancelled' + +export type TaskType = + | 'development' + | 'design' + | 'research' + | 'review' + | 'meeting' + | 'admin' + +export type Priority = 'critical' | 'high' | 'medium' | 'low' + +export type Complexity = 'simple' | 'medium' | 'complex' | 'epic' + +export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired' + +export type ApprovalRiskLevel = 'low' | 'medium' | 'high' | 'critical' + +export type SeniorityLevel = + | 'junior' + | 'mid' + | 'senior' + | 'lead' + | 'principal' + | 'director' + | 'vp' + | 'c_suite' + +export type AgentStatus = 'active' | 'onboarding' | 'on_leave' | 'terminated' + +export type AutonomyLevel = 'full' | 'semi' | 'supervised' | 'locked' + +export type HumanRole = + | 'ceo' + | 'manager' + | 'board_member' + | 'pair_programmer' + | 'observer' + +export type DepartmentName = + | 'executive' + | 'product' + | 'design' + | 'engineering' + | 'quality_assurance' + | 'data_analytics' + | 'operations' + | 'creative_marketing' + | 'security' + +export type ProjectStatus = + | 'planning' + | 'active' + | 'on_hold' + | 'completed' + | 'cancelled' + +export type RiskTolerance = 'low' | 'medium' | 'high' + +export type CreativityLevel = 'low' | 'medium' | 'high' + +export type DecisionMakingStyle = 'analytical' | 'intuitive' | 'consultative' | 'directive' + +export type CollaborationPreference = 'independent' | 'pair' | 'team' + +export type CommunicationVerbosity = 'terse' | 'balanced' | 'verbose' + +export type ConflictApproach = 'avoid' | 'accommodate' | 'compete' | 'compromise' | 'collaborate' + +export type TaskStructure = 'sequential' | 'parallel' | 'mixed' + +export type CoordinationTopology = 'sas' | 'centralized' | 'decentralized' | 'context_dependent' | 'auto' + +export type ToolAccessLevel = 'sandboxed' | 'restricted' | 'standard' | 'elevated' | 'custom' + +export type MemoryLevel = 'persistent' | 'project' | 'session' | 'none' + +// ── Response Envelopes ─────────────────────────────────────── + +/** Discriminated API response envelope. */ +export type ApiResponse = + | { data: T; error: null; success: true } + | { data: null; error: string; success: false } + +export interface PaginationMeta { + total: number + offset: number + limit: number +} + +/** Discriminated paginated response envelope. */ +export type PaginatedResponse = + | { data: T[]; error: null; success: true; pagination: PaginationMeta } + | { data: null; error: string; success: false; pagination: null } + +// ── Auth ───────────────────────────────────────────────────── + +export interface CredentialsRequest { + username: string + password: string +} + +/** Alias for setup endpoint. */ +export type SetupRequest = CredentialsRequest + +/** Alias for login endpoint. */ +export type LoginRequest = CredentialsRequest + +export interface ChangePasswordRequest { + current_password: string + new_password: string +} + +export interface TokenResponse { + token: string + expires_in: number + must_change_password: boolean +} + +export interface UserInfoResponse { + id: string + username: string + role: HumanRole + must_change_password: boolean +} + +// ── Tasks ──────────────────────────────────────────────────── + +export interface AcceptanceCriterion { + description: string + met: boolean +} + +export interface ExpectedArtifact { + name: string + type: string +} + +export interface Task { + id: string + title: string + description: string + type: TaskType + status: TaskStatus + priority: Priority + project: string + created_by: string + assigned_to: string | null + reviewers: string[] + dependencies: string[] + artifacts_expected: ExpectedArtifact[] + acceptance_criteria: AcceptanceCriterion[] + estimated_complexity: Complexity + budget_limit: number + cost_usd?: number + deadline: string | null + max_retries: number + parent_task_id: string | null + delegation_chain: string[] + task_structure: TaskStructure | null + coordination_topology: CoordinationTopology + version?: number + created_at?: string + updated_at?: string +} + +export interface CreateTaskRequest { + title: string + description: string + type: TaskType + priority?: Priority + project: string + created_by: string + assigned_to?: string | null + estimated_complexity?: Complexity + budget_limit?: number +} + +export interface UpdateTaskRequest { + title?: string + description?: string + priority?: Priority + assigned_to?: string | null + budget_limit?: number + expected_version?: number +} + +export interface TransitionTaskRequest { + target_status: TaskStatus + assigned_to?: string | null + expected_version?: number +} + +export interface CancelTaskRequest { + reason: string +} + +export interface TaskFilters { + status?: TaskStatus + assigned_to?: string + project?: string + offset?: number + limit?: number +} + +// ── Approvals ──────────────────────────────────────────────── + +export interface ApprovalItem { + id: string + action_type: string + title: string + description: string + requested_by: string + risk_level: ApprovalRiskLevel + status: ApprovalStatus + task_id: string | null + metadata: Record + decided_by: string | null + decision_reason: string | null + created_at: string + decided_at: string | null + expires_at: string | null +} + +export interface CreateApprovalRequest { + action_type: string + title: string + description: string + requested_by: string + risk_level: ApprovalRiskLevel + ttl_seconds?: number + task_id?: string + metadata?: Record +} + +export interface ApproveRequest { + comment?: string +} + +export interface RejectRequest { + reason: string +} + +export interface ApprovalFilters { + status?: ApprovalStatus + risk_level?: ApprovalRiskLevel + action_type?: string + offset?: number + limit?: number +} + +// ── Agents ─────────────────────────────────────────────────── + +export interface PersonalityConfig { + traits: string[] + communication_style: string + risk_tolerance: RiskTolerance + creativity: CreativityLevel + description: string + openness: number + conscientiousness: number + extraversion: number + agreeableness: number + stress_response: number + decision_making: DecisionMakingStyle + collaboration: CollaborationPreference + verbosity: CommunicationVerbosity + conflict_approach: ConflictApproach +} + +export interface ModelConfig { + provider: string + model_id: string + temperature: number + max_tokens: number + fallback_model: string | null +} + +export interface SkillSet { + primary: string[] + secondary: string[] +} + +export interface MemoryConfig { + type: MemoryLevel + retention_days: number | null +} + +export interface ToolPermissions { + access_level: ToolAccessLevel + allowed: string[] + denied: string[] +} + +/** + * Agent identity as returned by the API. + * Mirrors backend AgentIdentity with serialization adaptations + * (UUIDs as strings, dates as ISO strings, authority omitted from listing response). + */ +export interface AgentConfig { + id: string + name: string + role: string + department: string + level: SeniorityLevel + status: AgentStatus + personality: PersonalityConfig + model: ModelConfig + skills: SkillSet + memory: MemoryConfig + tools: ToolPermissions + autonomy_level: AutonomyLevel | null + hiring_date: string +} + +// ── Budget ─────────────────────────────────────────────────── + +export interface CostRecord { + agent_id: string + task_id: string + provider: string + model: string + input_tokens: number + output_tokens: number + cost_usd: number + timestamp: string + call_category: 'productive' | 'coordination' | 'system' | null +} + +export interface BudgetAlertConfig { + warn_at: number + critical_at: number + hard_stop_at: number +} + +export interface AutoDowngradeConfig { + enabled: boolean + threshold: number + downgrade_map: [string, string][] + boundary: 'task_assignment' +} + +export interface BudgetConfig { + total_monthly: number + alerts: BudgetAlertConfig + per_task_limit: number + per_agent_daily_limit: number + auto_downgrade: AutoDowngradeConfig + reset_day: number +} + +export interface AgentSpending { + agent_id: string + total_cost_usd: number +} + +// ── Analytics ──────────────────────────────────────────────── + +export interface OverviewMetrics { + total_tasks: number + tasks_by_status: Record + total_agents: number + total_cost_usd: number +} + +// ── Company / Organization ─────────────────────────────────── + +export interface Department { + name: DepartmentName + display_name: string + teams: TeamConfig[] +} + +export interface TeamConfig { + name: string + members: string[] +} + +export interface CompanyConfig { + company_name: string + agents: AgentConfig[] + departments: Department[] +} + +// ── Providers ──────────────────────────────────────────────── + +export interface ProviderModelConfig { + id: string + alias: string | null + cost_per_1k_input: number + cost_per_1k_output: number + max_context: number + estimated_latency_ms: number | null +} + +/** + * Provider configuration as returned by the listing endpoint. + * The backend MUST NOT serialize `api_key` to the frontend. + * If it does, the provider store strips it before storing. + */ +export interface ProviderConfig { + driver: string + base_url: string | null + models: ProviderModelConfig[] +} + +// ── Messages ───────────────────────────────────────────────── + +export type MessageType = + | 'task_update' + | 'question' + | 'announcement' + | 'status_report' + | 'escalation' + | 'delegation' + +export type MessagePriority = 'low' | 'normal' | 'high' | 'urgent' + +export type AttachmentType = 'artifact' | 'file' | 'link' + +export interface Attachment { + type: AttachmentType + ref: string +} + +export interface MessageMetadata { + task_id: string | null + project_id: string | null + tokens_used: number | null + cost_usd: number | null + extra: [string, string][] +} + +export interface Message { + id: string + timestamp: string + sender: string + to: string + type: MessageType + priority: MessagePriority + channel: string + content: string + attachments: Attachment[] + metadata: MessageMetadata +} + +export type ChannelType = 'topic' | 'direct' | 'broadcast' + +export interface Channel { + name: string + type: ChannelType + subscribers: string[] +} + +// ── Health ─────────────────────────────────────────────────── + +export interface HealthStatus { + status: 'ok' | 'degraded' | 'down' + persistence: boolean + message_bus: boolean + version: string + uptime_seconds: number +} + +// ── Autonomy ───────────────────────────────────────────────── + +export interface AutonomyLevelResponse { + agent_id: string + level: AutonomyLevel + promotion_pending: boolean +} + +export interface AutonomyLevelRequest { + level: AutonomyLevel +} + +// ── WebSocket ──────────────────────────────────────────────── + +export type WsChannel = + | 'tasks' + | 'agents' + | 'budget' + | 'messages' + | 'system' + | 'approvals' + +export type WsEventType = + | 'task.created' + | 'task.updated' + | 'task.status_changed' + | 'task.assigned' + | 'agent.hired' + | 'agent.fired' + | 'agent.status_changed' + | 'budget.record_added' + | 'budget.alert' + | 'message.sent' + | 'system.error' + | 'system.startup' + | 'system.shutdown' + | 'approval.submitted' + | 'approval.approved' + | 'approval.rejected' + | 'approval.expired' + +export interface WsEvent { + event_type: WsEventType + channel: WsChannel + timestamp: string + payload: Record +} + +export interface WsSubscribeMessage { + action: 'subscribe' + channels: WsChannel[] + filters?: Record +} + +export interface WsUnsubscribeMessage { + action: 'unsubscribe' + channels: WsChannel[] +} + +export interface WsAckMessage { + action: 'subscribed' | 'unsubscribed' + channels: WsChannel[] +} + +export interface WsErrorMessage { + error: string +} + +// ── Event handler type ────────────────────────────────────── + +export type WsEventHandler = (event: WsEvent) => void + +// ── Pagination helpers ─────────────────────────────────────── + +export interface PaginationParams { + offset?: number + limit?: number +} diff --git a/web/src/components/common/EmptyState.vue b/web/src/components/common/EmptyState.vue new file mode 100644 index 0000000000..be85873327 --- /dev/null +++ b/web/src/components/common/EmptyState.vue @@ -0,0 +1,22 @@ + + + diff --git a/web/src/components/common/ErrorBoundary.vue b/web/src/components/common/ErrorBoundary.vue new file mode 100644 index 0000000000..565c9ee05f --- /dev/null +++ b/web/src/components/common/ErrorBoundary.vue @@ -0,0 +1,34 @@ + + + + diff --git a/web/src/components/common/LoadingSkeleton.vue b/web/src/components/common/LoadingSkeleton.vue new file mode 100644 index 0000000000..d638586ed5 --- /dev/null +++ b/web/src/components/common/LoadingSkeleton.vue @@ -0,0 +1,13 @@ + + + diff --git a/web/src/components/common/PageHeader.vue b/web/src/components/common/PageHeader.vue new file mode 100644 index 0000000000..191daafc02 --- /dev/null +++ b/web/src/components/common/PageHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/web/src/components/common/StatusBadge.vue b/web/src/components/common/StatusBadge.vue new file mode 100644 index 0000000000..bb7de7e0f6 --- /dev/null +++ b/web/src/components/common/StatusBadge.vue @@ -0,0 +1,29 @@ + + + diff --git a/web/src/components/layout/AppShell.vue b/web/src/components/layout/AppShell.vue new file mode 100644 index 0000000000..aaa3a71e53 --- /dev/null +++ b/web/src/components/layout/AppShell.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/layout/ConnectionStatus.vue b/web/src/components/layout/ConnectionStatus.vue new file mode 100644 index 0000000000..263f83eac0 --- /dev/null +++ b/web/src/components/layout/ConnectionStatus.vue @@ -0,0 +1,65 @@ + + + diff --git a/web/src/components/layout/Sidebar.vue b/web/src/components/layout/Sidebar.vue new file mode 100644 index 0000000000..6e3ad5961d --- /dev/null +++ b/web/src/components/layout/Sidebar.vue @@ -0,0 +1,73 @@ + + + diff --git a/web/src/components/layout/Topbar.vue b/web/src/components/layout/Topbar.vue new file mode 100644 index 0000000000..c6ad3dd505 --- /dev/null +++ b/web/src/components/layout/Topbar.vue @@ -0,0 +1,72 @@ + + + diff --git a/web/src/composables/useAuth.ts b/web/src/composables/useAuth.ts new file mode 100644 index 0000000000..82de151c31 --- /dev/null +++ b/web/src/composables/useAuth.ts @@ -0,0 +1,26 @@ +import { computed } from 'vue' +import { useAuthStore } from '@/stores/auth' +import { WRITE_ROLES } from '@/utils/constants' + +/** Auth state helpers for components. */ +export function useAuth() { + const store = useAuthStore() + + const isAuthenticated = computed(() => store.isAuthenticated) + const user = computed(() => store.user) + const userRole = computed(() => store.userRole) + const mustChangePassword = computed(() => store.mustChangePassword) + + const canWrite = computed(() => { + const role = userRole.value + return role !== null && (WRITE_ROLES as ReadonlyArray).includes(role) + }) + + return { + isAuthenticated, + user, + userRole, + mustChangePassword, + canWrite, + } +} diff --git a/web/src/composables/useLoginLockout.ts b/web/src/composables/useLoginLockout.ts new file mode 100644 index 0000000000..43528deb81 --- /dev/null +++ b/web/src/composables/useLoginLockout.ts @@ -0,0 +1,60 @@ +/** + * Shared client-side lockout logic for Login and Setup pages. + * This is a UX hint only — real brute-force protection is server-side. + */ + +import { ref, computed, onUnmounted } from 'vue' +import { isAxiosError } from '@/utils/errors' +import { LOGIN_MAX_ATTEMPTS, LOGIN_LOCKOUT_MS } from '@/utils/constants' + +export function useLoginLockout() { + const attempts = ref(0) + const lockedUntil = ref(null) + + // Reactive clock so `locked` re-evaluates when lockout expires + const now = ref(Date.now()) + const clockTimer = setInterval(() => { now.value = Date.now() }, 1000) + onUnmounted(() => clearInterval(clockTimer)) + + const locked = computed(() => !!(lockedUntil.value && now.value < lockedUntil.value)) + + /** Clear expired lockout. Returns true if still locked. */ + function checkAndClearLockout(): boolean { + if (lockedUntil.value && Date.now() >= lockedUntil.value) { + lockedUntil.value = null + attempts.value = 0 + } + return locked.value + } + + /** + * Record a failed attempt. Uses HTTP status code (not error message strings) + * to distinguish credential errors (4xx) from transient failures (network/5xx). + * Returns a lockout error message if the user just got locked out, or null. + */ + function recordFailure(err: unknown): string | null { + // Only count credential failures (4xx) toward lockout + const isCredentialError = isAxiosError(err) && + err.response !== undefined && + err.response.status >= 400 && + err.response.status < 500 + + if (isCredentialError) { + attempts.value++ + if (attempts.value >= LOGIN_MAX_ATTEMPTS) { + lockedUntil.value = Date.now() + LOGIN_LOCKOUT_MS + attempts.value = 0 + return `Too many failed attempts. Please wait ${LOGIN_LOCKOUT_MS / 1000} seconds.` + } + } + return null + } + + /** Reset attempts on successful auth. */ + function reset() { + attempts.value = 0 + lockedUntil.value = null + } + + return { locked, checkAndClearLockout, recordFailure, reset } +} diff --git a/web/src/composables/useOptimisticUpdate.ts b/web/src/composables/useOptimisticUpdate.ts new file mode 100644 index 0000000000..321a4ba58f --- /dev/null +++ b/web/src/composables/useOptimisticUpdate.ts @@ -0,0 +1,59 @@ +import { ref } from 'vue' +import { getErrorMessage } from '@/utils/errors' + +/** + * Perform an optimistic UI update with rollback on failure. + * + * Returns an `execute(applyOptimistic, serverAction)` function where + * `applyOptimistic` applies the optimistic state and returns a rollback function, + * and `serverAction` is the actual server request. + * + * A `null` return means one of two things: + * - **Server error**: `error.value` is set with a message. The optimistic state was rolled back. + * - **Already in-flight**: `pending.value` was true, so the call was a no-op. `error.value` is + * unchanged from its previous value (may be `null`). + * + * Callers should check `error.value` to distinguish server errors from no-op returns. + */ +export function useOptimisticUpdate() { + const pending = ref(false) + const error = ref(null) + + async function execute( + applyOptimistic: () => () => void, + serverAction: () => Promise, + ): Promise { + if (pending.value) return null + pending.value = true + error.value = null + + // Capture rollback before any mutation so a partial throw is still reversible + let rollback: (() => void) | null = null + try { + rollback = applyOptimistic() + } catch (prepareErr) { + pending.value = false + error.value = getErrorMessage(prepareErr) + console.error('Optimistic prepare failed:', error.value) + return null + } + + try { + const result = await serverAction() + return result + } catch (err) { + try { + rollback() + } catch (rollbackErr) { + console.error('Rollback failed:', getErrorMessage(rollbackErr)) + } + error.value = getErrorMessage(err) + console.error('Optimistic update failed:', error.value) + return null + } finally { + pending.value = false + } + } + + return { pending, error, execute } +} diff --git a/web/src/composables/usePolling.ts b/web/src/composables/usePolling.ts new file mode 100644 index 0000000000..f07cd11161 --- /dev/null +++ b/web/src/composables/usePolling.ts @@ -0,0 +1,56 @@ +import { ref, onUnmounted } from 'vue' + +const MIN_POLL_INTERVAL = 100 + +/** + * Poll a function at a fixed interval with cleanup on unmount. + * Uses setTimeout-based scheduling to prevent overlapping async calls. + */ +export function usePolling(fn: () => Promise, intervalMs: number) { + if (!Number.isFinite(intervalMs) || intervalMs < MIN_POLL_INTERVAL) { + throw new Error(`usePolling: intervalMs must be a finite number >= ${MIN_POLL_INTERVAL}, got ${intervalMs}`) + } + const active = ref(false) + let timer: ReturnType | null = null + + const scheduleTick = () => { + if (!active.value) return + timer = setTimeout(async () => { + if (!active.value) return + try { + await fn() + } catch (err) { + console.error('Polling error:', err) + } + scheduleTick() + }, intervalMs) + } + + function start() { + if (active.value) return + active.value = true + // Fetch immediately on start, then schedule subsequent ticks + const immediate = async () => { + if (!active.value) return + try { + await fn() + } catch (err) { + console.error('Polling error:', err) + } + scheduleTick() + } + immediate() + } + + function stop() { + active.value = false + if (timer) { + clearTimeout(timer) + timer = null + } + } + + onUnmounted(stop) + + return { active, start, stop } +} diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000000..2e0282ed00 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,32 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import PrimeVue from 'primevue/config' +import ToastService from 'primevue/toastservice' +import ConfirmationService from 'primevue/confirmationservice' + +import App from './App.vue' +import { router } from './router' +import { primeVueOptions } from './primevue-preset' +import { sanitizeForLog } from './utils/logging' +import './styles/global.css' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(PrimeVue, primeVueOptions) +app.use(ToastService) +app.use(ConfirmationService) + +// Global error handler for unhandled errors in components +app.config.errorHandler = (err, _instance, info) => { + console.error('Unhandled Vue error:', sanitizeForLog(err), 'Info:', sanitizeForLog(info)) +} + +// Catch unhandled promise rejections — log but don't preventDefault() so the +// browser's default handler and error-monitoring integrations still fire. +window.addEventListener('unhandledrejection', (event) => { + console.error('Unhandled promise rejection:', sanitizeForLog(event.reason)) +}) + +app.mount('#app') diff --git a/web/src/primevue-preset.ts b/web/src/primevue-preset.ts new file mode 100644 index 0000000000..2b06dd5724 --- /dev/null +++ b/web/src/primevue-preset.ts @@ -0,0 +1,12 @@ +import Aura from '@primevue/themes/aura' +import type { PrimeVueConfiguration } from 'primevue' + +export const primeVueOptions: PrimeVueConfiguration = { + theme: { + preset: Aura, + options: { + darkModeSelector: '.dark', + cssLayer: false, + }, + }, +} diff --git a/web/src/router/guards.ts b/web/src/router/guards.ts new file mode 100644 index 0000000000..5db61af036 --- /dev/null +++ b/web/src/router/guards.ts @@ -0,0 +1,40 @@ +import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +/** + * Navigation guard that redirects unauthenticated users to /login. + * Uses route.meta.requiresAuth to determine access control: + * - Routes with requiresAuth: false are public (login, setup) + * - All other routes require authentication + * Redirects authenticated users away from public auth pages. + * Preserves intended destination via `redirect` query param. + */ +export function authGuard( + to: RouteLocationNormalized, + _from: RouteLocationNormalized, + next: NavigationGuardNext, +): void { + const auth = useAuthStore() + + if (to.meta.requiresAuth === false) { + // If already authenticated, redirect away from login/setup + if (auth.isAuthenticated) { + next('/') + return + } + next() + return + } + + if (!auth.isAuthenticated) { + const redirect = to.fullPath !== '/' ? to.fullPath : undefined + next({ path: '/login', query: redirect ? { redirect } : undefined }) + return + } + + // TODO(#346): Enforce mustChangePassword redirect when + // /change-password route and page are added in the page-views PR. + // Currently only surfaced as a toast in LoginPage.vue. + + next() +} diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100644 index 0000000000..70d91cd0be --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,34 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { authGuard } from './guards' +import PlaceholderHome from '@/views/PlaceholderHome.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('@/views/LoginPage.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/setup', + name: 'setup', + component: () => import('@/views/SetupPage.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/', + name: 'home', + component: PlaceholderHome, + }, + { + path: '/:pathMatch(.*)*', + redirect: '/', + }, + ], +}) + +router.beforeEach(authGuard) + +export { router } diff --git a/web/src/stores/agents.ts b/web/src/stores/agents.ts new file mode 100644 index 0000000000..72c8adb6ba --- /dev/null +++ b/web/src/stores/agents.ts @@ -0,0 +1,81 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as agentsApi from '@/api/endpoints/agents' +import { getErrorMessage } from '@/utils/errors' +import { MAX_PAGE_SIZE } from '@/utils/constants' +import type { AgentConfig, WsEvent } from '@/api/types' + +export const useAgentStore = defineStore('agents', () => { + const agents = ref([]) + const total = ref(0) + const loading = ref(false) + const error = ref(null) + + async function fetchAgents() { + loading.value = true + error.value = null + try { + const result = await agentsApi.listAgents({ limit: MAX_PAGE_SIZE }) + agents.value = result.data + total.value = result.total + } catch (err) { + error.value = getErrorMessage(err) + } finally { + loading.value = false + } + } + + async function fetchAgent(name: string): Promise { + error.value = null + try { + return await agentsApi.getAgent(name) + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + /** Runtime check for minimum required AgentConfig fields. */ + function isValidAgentPayload(p: Record): boolean { + return ( + typeof p.id === 'string' && p.id !== '' && + typeof p.name === 'string' && p.name !== '' && + typeof p.role === 'string' && p.role !== '' && + typeof p.department === 'string' && p.department !== '' + ) + } + + function handleWsEvent(event: WsEvent) { + const payload = event.payload as Record | null + if (!payload || typeof payload !== 'object') return + switch (event.event_type) { + case 'agent.hired': + if ( + isValidAgentPayload(payload) && + !agents.value.some((a) => a.name === payload.name) + ) { + agents.value = [...agents.value, payload as unknown as AgentConfig] + total.value++ + } + break + case 'agent.fired': + if (typeof payload.name === 'string' && payload.name) { + const prevLength = agents.value.length + agents.value = agents.value.filter((a) => a.name !== payload.name) + if (agents.value.length < prevLength) { + total.value-- + } + } + break + case 'agent.status_changed': + if (typeof payload.name === 'string' && payload.name) { + agents.value = agents.value.map((a) => + a.name === payload.name ? { ...a, ...(payload as Partial) } : a, + ) + } + break + } + } + + return { agents, total, loading, error, fetchAgents, fetchAgent, handleWsEvent } +}) diff --git a/web/src/stores/analytics.ts b/web/src/stores/analytics.ts new file mode 100644 index 0000000000..f2857a318e --- /dev/null +++ b/web/src/stores/analytics.ts @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as analyticsApi from '@/api/endpoints/analytics' +import { getErrorMessage } from '@/utils/errors' +import type { OverviewMetrics } from '@/api/types' + +export const useAnalyticsStore = defineStore('analytics', () => { + const metrics = ref(null) + const loading = ref(false) + const error = ref(null) + let fetchGeneration = 0 + + async function fetchMetrics() { + const gen = ++fetchGeneration + loading.value = true + error.value = null + try { + const result = await analyticsApi.getOverviewMetrics() + // Only apply if this is still the latest fetch (prevents stale overwrites) + if (gen === fetchGeneration) { + metrics.value = result + } + } catch (err) { + if (gen === fetchGeneration) { + error.value = getErrorMessage(err) + } + } finally { + if (gen === fetchGeneration) { + loading.value = false + } + } + } + + return { metrics, loading, error, fetchMetrics } +}) diff --git a/web/src/stores/approvals.ts b/web/src/stores/approvals.ts new file mode 100644 index 0000000000..f6c2b826b8 --- /dev/null +++ b/web/src/stores/approvals.ts @@ -0,0 +1,107 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import * as approvalsApi from '@/api/endpoints/approvals' +import { getErrorMessage } from '@/utils/errors' +import type { ApprovalItem, ApprovalFilters, ApproveRequest, RejectRequest, WsEvent } from '@/api/types' + +export const useApprovalStore = defineStore('approvals', () => { + const approvals = ref([]) + const total = ref(0) + const loading = ref(false) + const error = ref(null) + const activeFilters = ref(undefined) + + const pendingCount = computed(() => approvals.value.filter((a) => a.status === 'pending').length) + + async function fetchApprovals(filters?: ApprovalFilters) { + loading.value = true + error.value = null + activeFilters.value = filters ? { ...filters } : undefined + try { + const result = await approvalsApi.listApprovals(filters) + approvals.value = result.data + total.value = result.total + } catch (err) { + error.value = getErrorMessage(err) + } finally { + loading.value = false + } + } + + async function approve(id: string, data?: ApproveRequest): Promise { + error.value = null + try { + const updated = await approvalsApi.approveApproval(id, data) + approvals.value = approvals.value.map((a) => (a.id === id ? updated : a)) + return updated + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + async function reject(id: string, data: RejectRequest): Promise { + error.value = null + try { + const updated = await approvalsApi.rejectApproval(id, data) + approvals.value = approvals.value.map((a) => (a.id === id ? updated : a)) + return updated + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + /** Runtime check for required ApprovalItem fields before insertion. */ + function isValidApprovalPayload(p: Record): boolean { + return ( + typeof p.id === 'string' && p.id !== '' && + typeof p.action_type === 'string' && + typeof p.title === 'string' && + typeof p.status === 'string' && + typeof p.requested_by === 'string' && + typeof p.risk_level === 'string' && + typeof p.created_at === 'string' + ) + } + + function handleWsEvent(event: WsEvent) { + const payload = event.payload as Record | null + if (!payload || typeof payload !== 'object') return + switch (event.event_type) { + case 'approval.submitted': + if ( + isValidApprovalPayload(payload) && + !approvals.value.some((a) => a.id === payload.id) + ) { + // Only insert + count into unfiltered views to keep list consistent + if (!activeFilters.value) { + approvals.value = [payload as unknown as ApprovalItem, ...approvals.value] + total.value++ + } + } + break + case 'approval.approved': + case 'approval.rejected': + case 'approval.expired': + if (typeof payload.id === 'string' && payload.id) { + approvals.value = approvals.value.map((a) => + a.id === payload.id ? { ...a, ...(payload as Partial) } : a, + ) + } + break + } + } + + return { + approvals, + total, + loading, + error, + pendingCount, + fetchApprovals, + approve, + reject, + handleWsEvent, + } +}) diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts new file mode 100644 index 0000000000..9dca5cee9a --- /dev/null +++ b/web/src/stores/auth.ts @@ -0,0 +1,174 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import * as authApi from '@/api/endpoints/auth' +import { getErrorMessage, isAxiosError } from '@/utils/errors' +import { router } from '@/router' +import type { HumanRole, UserInfoResponse } from '@/api/types' + +export const useAuthStore = defineStore('auth', () => { + // Restore token only if not expired + const storedToken = localStorage.getItem('auth_token') + const expiresAt = Number(localStorage.getItem('auth_token_expires_at') ?? 0) + const initialToken = storedToken && Date.now() < expiresAt ? storedToken : null + if (!initialToken) { + localStorage.removeItem('auth_token') + localStorage.removeItem('auth_token_expires_at') + } + + const token = ref(initialToken) + const user = ref(null) + const loading = ref(false) + + let expiryTimer: ReturnType | null = null + + // Schedule expiry cleanup for restored token + if (initialToken && expiresAt > Date.now()) { + expiryTimer = setTimeout(() => { + clearAuth() + }, expiresAt - Date.now()) + } + + // Clean up timer during HMR to avoid stale timers on dev reloads + if (import.meta.hot) { + import.meta.hot.dispose(() => { + if (expiryTimer) { + clearTimeout(expiryTimer) + expiryTimer = null + } + }) + } + + const isAuthenticated = computed(() => !!token.value) + const mustChangePassword = computed(() => user.value?.must_change_password ?? false) + const userRole = computed(() => user.value?.role ?? null) + + function setToken(newToken: string, expiresIn: number) { + if (expiresIn <= 0) { + console.error('setToken: invalid expiresIn', expiresIn) + return + } + // Clear any existing expiry timer to prevent stale timer from killing new session + if (expiryTimer) { + clearTimeout(expiryTimer) + expiryTimer = null + } + + token.value = newToken + const expiresAtMs = Date.now() + expiresIn * 1000 + localStorage.setItem('auth_token', newToken) + localStorage.setItem('auth_token_expires_at', String(expiresAtMs)) + + // Schedule token cleanup + expiryTimer = setTimeout(() => { + clearAuth() + }, expiresIn * 1000) + } + + function clearAuth() { + if (expiryTimer) { + clearTimeout(expiryTimer) + expiryTimer = null + } + token.value = null + user.value = null + localStorage.removeItem('auth_token') + localStorage.removeItem('auth_token_expires_at') + // Redirect to login if not already there + if (router.currentRoute.value.path !== '/login' && router.currentRoute.value.path !== '/setup') { + router.push('/login') + } + } + + /** Common post-auth flow: set token, fetch user profile, handle failures. */ + async function performAuthFlow( + authFn: () => Promise<{ token: string; expires_in: number }>, + flowName: string, + ) { + loading.value = true + try { + const result = await authFn() + setToken(result.token, result.expires_in) + try { + await fetchUser() + } catch (fetchErr) { + // fetchUser already clears auth on 401 (invalid token) and doesn't throw. + // If we get here, it's a transient error (network/5xx) — the token may be + // valid but we can't load the profile. Clear auth since the app can't + // function without user data, but use a distinct error message. + if (isAxiosError(fetchErr) && fetchErr.response?.status === 401) { + // 401 during fetchUser means the just-issued token is already invalid + clearAuth() + throw new Error(`${flowName} failed: invalid session. Please try again.`) + } + clearAuth() + throw new Error(`${flowName} succeeded but failed to load user profile. Please check your connection and try again.`) + } + // If fetchUser silently cleared auth (e.g. 401), the flow should not succeed + if (!user.value) { + clearAuth() + throw new Error(`${flowName} succeeded but failed to load user profile. Please try again.`) + } + return result + } finally { + loading.value = false + } + } + + async function setup(username: string, password: string) { + return performAuthFlow(() => authApi.setup({ username, password }), 'Setup') + } + + async function login(username: string, password: string) { + return performAuthFlow(() => authApi.login({ username, password }), 'Login') + } + + async function fetchUser() { + if (!token.value) return + try { + user.value = await authApi.getMe() + } catch (err) { + // Only clear auth on 401 (invalid/expired token) + // Transient errors (network, 500) should NOT log the user out + if (isAxiosError(err) && err.response?.status === 401) { + clearAuth() + } else { + console.error('Failed to fetch user profile:', getErrorMessage(err)) + throw err + } + } + } + + async function changePassword(currentPassword: string, newPassword: string) { + loading.value = true + try { + const result = await authApi.changePassword({ + current_password: currentPassword, + new_password: newPassword, + }) + user.value = result + return result + } catch (err) { + throw new Error(getErrorMessage(err)) + } finally { + loading.value = false + } + } + + function logout() { + clearAuth() + } + + return { + token, + user, + loading, + isAuthenticated, + mustChangePassword, + userRole, + setup, + login, + fetchUser, + changePassword, + logout, + } +}) diff --git a/web/src/stores/budget.ts b/web/src/stores/budget.ts new file mode 100644 index 0000000000..b6642a2503 --- /dev/null +++ b/web/src/stores/budget.ts @@ -0,0 +1,109 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as budgetApi from '@/api/endpoints/budget' +import { getErrorMessage } from '@/utils/errors' +import type { BudgetConfig, CostRecord, AgentSpending, WsEvent } from '@/api/types' + +const MAX_WS_RECORDS = 500 + +/** Runtime type guard for CostRecord-shaped payloads — validates all required fields. */ +function isCostRecord(payload: unknown): payload is CostRecord { + if (typeof payload !== 'object' || payload === null) return false + const p = payload as Record + return ( + typeof p.agent_id === 'string' && + typeof p.task_id === 'string' && + typeof p.provider === 'string' && + typeof p.model === 'string' && + typeof p.cost_usd === 'number' && + typeof p.input_tokens === 'number' && + typeof p.output_tokens === 'number' && + typeof p.timestamp === 'string' + ) +} + +export const useBudgetStore = defineStore('budget', () => { + const config = ref(null) + const records = ref([]) + const totalRecords = ref(0) + const configLoading = ref(false) + const recordsLoading = ref(false) + const spendingLoading = ref(false) + const loading = ref(false) + const error = ref(null) + let lastFetchParams: { agent_id?: string; task_id?: string; limit?: number } | undefined + + async function fetchConfig() { + configLoading.value = true + loading.value = true + error.value = null + try { + config.value = await budgetApi.getBudgetConfig() + } catch (err) { + error.value = getErrorMessage(err) + } finally { + configLoading.value = false + if (!recordsLoading.value && !spendingLoading.value) loading.value = false + } + } + + async function fetchRecords(params?: { agent_id?: string; task_id?: string; limit?: number }) { + recordsLoading.value = true + loading.value = true + error.value = null + lastFetchParams = params ? { ...params } : undefined + try { + const result = await budgetApi.listCostRecords(params) + records.value = result.data + totalRecords.value = result.total + } catch (err) { + error.value = getErrorMessage(err) + } finally { + recordsLoading.value = false + if (!configLoading.value && !spendingLoading.value) loading.value = false + } + } + + async function fetchAgentSpending(agentId: string): Promise { + spendingLoading.value = true + loading.value = true + error.value = null + try { + return await budgetApi.getAgentSpending(agentId) + } catch (err) { + error.value = getErrorMessage(err) + return null + } finally { + spendingLoading.value = false + if (!configLoading.value && !recordsLoading.value) loading.value = false + } + } + + function handleWsEvent(event: WsEvent) { + if (event.event_type === 'budget.record_added') { + if (isCostRecord(event.payload)) { + // Skip if active filters don't match this record + if (lastFetchParams?.agent_id && event.payload.agent_id !== lastFetchParams.agent_id) return + if (lastFetchParams?.task_id && event.payload.task_id !== lastFetchParams.task_id) return + const limit = lastFetchParams?.limit ?? MAX_WS_RECORDS + records.value = [event.payload, ...records.value].slice(0, limit) + totalRecords.value++ + } + } + } + + return { + config, + records, + totalRecords, + configLoading, + recordsLoading, + spendingLoading, + loading, + error, + fetchConfig, + fetchRecords, + fetchAgentSpending, + handleWsEvent, + } +}) diff --git a/web/src/stores/company.ts b/web/src/stores/company.ts new file mode 100644 index 0000000000..1e4ab6a6ec --- /dev/null +++ b/web/src/stores/company.ts @@ -0,0 +1,78 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as companyApi from '@/api/endpoints/company' +import { getErrorMessage } from '@/utils/errors' +import { MAX_PAGE_SIZE } from '@/utils/constants' +import type { CompanyConfig, Department } from '@/api/types' + +export const useCompanyStore = defineStore('company', () => { + const config = ref(null) + const departments = ref([]) + const loading = ref(false) + const departmentsLoading = ref(false) + const configError = ref(null) + const departmentsError = ref(null) + + let configGen = 0 + let departmentsGen = 0 + + async function fetchConfig() { + const gen = ++configGen + loading.value = true + configError.value = null + try { + const result = await companyApi.getCompanyConfig() + if (gen === configGen) { + config.value = result + } + } catch (err) { + if (gen === configGen) { + configError.value = getErrorMessage(err) + } + } finally { + if (gen === configGen) { + loading.value = false + } + } + } + + async function fetchDepartments() { + const gen = ++departmentsGen + departmentsLoading.value = true + departmentsError.value = null + try { + let allDepts: Department[] = [] + let offset = 0 + // Paginate until all departments are fetched + while (true) { + const result = await companyApi.listDepartments({ limit: MAX_PAGE_SIZE, offset }) + if (gen !== departmentsGen) return // Stale request — abort + allDepts = [...allDepts, ...result.data] + if (result.data.length === 0 || allDepts.length >= result.total) break + offset += MAX_PAGE_SIZE + } + if (gen === departmentsGen) { + departments.value = allDepts + } + } catch (err) { + if (gen === departmentsGen) { + departmentsError.value = getErrorMessage(err) + } + } finally { + if (gen === departmentsGen) { + departmentsLoading.value = false + } + } + } + + return { + config, + departments, + loading, + departmentsLoading, + configError, + departmentsError, + fetchConfig, + fetchDepartments, + } +}) diff --git a/web/src/stores/messages.ts b/web/src/stores/messages.ts new file mode 100644 index 0000000000..0ec06f6694 --- /dev/null +++ b/web/src/stores/messages.ts @@ -0,0 +1,101 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as messagesApi from '@/api/endpoints/messages' +import { getErrorMessage } from '@/utils/errors' +import type { Channel, Message, WsEvent } from '@/api/types' + +const MAX_WS_MESSAGES = 500 + +/** Runtime check for minimum required Message fields on a WS payload. */ +function isValidMessagePayload(p: Record): boolean { + return ( + typeof p.id === 'string' && p.id !== '' && + typeof p.channel === 'string' && + typeof p.sender === 'string' && + typeof p.content === 'string' && + typeof p.timestamp === 'string' + ) +} + +export const useMessageStore = defineStore('messages', () => { + const messages = ref([]) + const channels = ref([]) + const total = ref(0) + const activeChannel = ref(null) + const loading = ref(false) + const channelsLoading = ref(false) + const error = ref(null) + const channelsError = ref(null) + let fetchRequestId = 0 + + async function fetchChannels() { + channelsLoading.value = true + channelsError.value = null + try { + channels.value = await messagesApi.listChannels() + } catch (err) { + channelsError.value = getErrorMessage(err) + } finally { + channelsLoading.value = false + } + } + + async function fetchMessages(channel?: string) { + const requestId = ++fetchRequestId + loading.value = true + error.value = null + // Sync the WS filter so handleWsEvent matches the fetched channel + activeChannel.value = channel ?? null + try { + const params = channel ? { channel, limit: 100 } : { limit: 100 } + const result = await messagesApi.listMessages(params) + // Only commit if this is still the latest request (prevent stale overwrites) + if (requestId === fetchRequestId) { + messages.value = result.data + total.value = result.total + } + } catch (err) { + if (requestId === fetchRequestId) { + error.value = getErrorMessage(err) + } + } finally { + if (requestId === fetchRequestId) { + loading.value = false + } + } + } + + function setActiveChannel(channel: string | null) { + activeChannel.value = channel + } + + function handleWsEvent(event: WsEvent) { + if (event.event_type === 'message.sent') { + const payload = event.payload as Record | null + if (!payload || typeof payload !== 'object') return + if (!isValidMessagePayload(payload)) return + const message = payload as unknown as Message + // Only append if message matches active channel (or no filter is set) + if (!activeChannel.value || message.channel === activeChannel.value) { + if (!messages.value.some((m) => m.id === message.id)) { + messages.value = [...messages.value, message].slice(-MAX_WS_MESSAGES) + } + } + } + } + + return { + messages, + channels, + total, + activeChannel, + loading, + channelsLoading, + error, + channelsError, + fetchChannels, + fetchMessages, + setActiveChannel, + handleWsEvent, + } +}) diff --git a/web/src/stores/providers.ts b/web/src/stores/providers.ts new file mode 100644 index 0000000000..374a69ed2e --- /dev/null +++ b/web/src/stores/providers.ts @@ -0,0 +1,40 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import * as providersApi from '@/api/endpoints/providers' +import { getErrorMessage } from '@/utils/errors' +import type { ProviderConfig } from '@/api/types' + +const UNSAFE_KEYS = new Set(['__proto__', 'prototype', 'constructor']) + +/** Strip any accidentally-serialized secrets before storing in reactive state. */ +function sanitizeProviders(raw: Record): Record { + const result = Object.create(null) as Record + for (const [key, provider] of Object.entries(raw)) { + if (UNSAFE_KEYS.has(key)) continue + // Destructure to omit api_key if the backend accidentally includes it + const { api_key: _discarded, ...safe } = provider as ProviderConfig & { api_key?: unknown } + result[key] = safe + } + return result +} + +export const useProviderStore = defineStore('providers', () => { + const providers = ref>({}) + const loading = ref(false) + const error = ref(null) + + async function fetchProviders() { + loading.value = true + error.value = null + try { + const raw = await providersApi.listProviders() + providers.value = sanitizeProviders(raw) + } catch (err) { + error.value = getErrorMessage(err) + } finally { + loading.value = false + } + } + + return { providers, loading, error, fetchProviders } +}) diff --git a/web/src/stores/tasks.ts b/web/src/stores/tasks.ts new file mode 100644 index 0000000000..f44900cc5f --- /dev/null +++ b/web/src/stores/tasks.ts @@ -0,0 +1,170 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import * as tasksApi from '@/api/endpoints/tasks' +import { getErrorMessage } from '@/utils/errors' +import type { + Task, + TaskFilters, + CreateTaskRequest, + UpdateTaskRequest, + TransitionTaskRequest, + CancelTaskRequest, + WsEvent, +} from '@/api/types' + +export const useTaskStore = defineStore('tasks', () => { + const tasks = ref([]) + const total = ref(0) + const loading = ref(false) + const error = ref(null) + const currentFilters = ref({}) + + const tasksByStatus = computed(() => { + const grouped: Record = {} + for (const task of tasks.value) { + const list = grouped[task.status] + if (list) { + list.push(task) + } else { + grouped[task.status] = [task] + } + } + return grouped + }) + + /** Check whether any non-trivial filters are currently active. */ + function hasActiveFilters(): boolean { + return Object.values(currentFilters.value).some( + (v) => v !== undefined && v !== null && !(typeof v === 'string' && v.trim() === ''), + ) + } + + async function fetchTasks(filters?: TaskFilters) { + loading.value = true + error.value = null + // Always update filters — passing undefined clears previous filters + currentFilters.value = filters ? { ...filters } : {} + try { + const result = await tasksApi.listTasks(currentFilters.value) + tasks.value = result.data + total.value = result.total + } catch (err) { + error.value = getErrorMessage(err) + } finally { + loading.value = false + } + } + + async function createTask(data: CreateTaskRequest): Promise { + error.value = null + try { + const task = await tasksApi.createTask(data) + // Only append to local list if no filters are active (filtered views are kept + // accurate by REST fetches) and guard against race with WS task.created event + if (!hasActiveFilters() && !tasks.value.some((t) => t.id === task.id)) { + tasks.value = [...tasks.value, task] + total.value++ + } + return task + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + async function updateTask(taskId: string, data: UpdateTaskRequest): Promise { + error.value = null + try { + const updated = await tasksApi.updateTask(taskId, data) + tasks.value = tasks.value.map((t) => (t.id === taskId ? updated : t)) + return updated + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + async function transitionTask( + taskId: string, + data: TransitionTaskRequest, + ): Promise { + error.value = null + try { + const updated = await tasksApi.transitionTask(taskId, data) + tasks.value = tasks.value.map((t) => (t.id === taskId ? updated : t)) + return updated + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + async function cancelTask(taskId: string, data: CancelTaskRequest): Promise { + error.value = null + try { + const updated = await tasksApi.cancelTask(taskId, data) + tasks.value = tasks.value.map((t) => (t.id === taskId ? updated : t)) + return updated + } catch (err) { + error.value = getErrorMessage(err) + return null + } + } + + /** Runtime check for required Task fields before insertion. */ + function isValidTaskPayload(p: Record): boolean { + return ( + typeof p.id === 'string' && p.id !== '' && + typeof p.title === 'string' && + typeof p.status === 'string' && + typeof p.type === 'string' && + typeof p.priority === 'string' && + typeof p.created_by === 'string' + ) + } + + function handleWsEvent(event: WsEvent) { + const payload = event.payload as Record | null + if (!payload || typeof payload !== 'object') return + switch (event.event_type) { + case 'task.created': + if ( + isValidTaskPayload(payload) && + !tasks.value.some((t) => t.id === payload.id) + ) { + // Only append if no active filters — filtered views are kept accurate by REST fetches + if (!hasActiveFilters()) { + tasks.value = [...tasks.value, payload as unknown as Task] + total.value++ + } + } + break + case 'task.updated': + case 'task.status_changed': + case 'task.assigned': + if (typeof payload.id === 'string' && payload.id) { + // Only update tasks already in the list — if filters are active, + // tasks that no longer match will be cleaned up on next REST fetch + tasks.value = tasks.value.map((t) => + t.id === payload.id ? { ...t, ...(payload as Partial) } : t, + ) + } + break + } + } + + return { + tasks, + total, + loading, + error, + currentFilters, + tasksByStatus, + fetchTasks, + createTask, + updateTask, + transitionTask, + cancelTask, + handleWsEvent, + } +}) diff --git a/web/src/stores/websocket.ts b/web/src/stores/websocket.ts new file mode 100644 index 0000000000..ae9e28bd6e --- /dev/null +++ b/web/src/stores/websocket.ts @@ -0,0 +1,234 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { WsChannel, WsEvent, WsEventHandler } from '@/api/types' +import { WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY, WS_MAX_RECONNECT_ATTEMPTS } from '@/utils/constants' +import { sanitizeForLog } from '@/utils/logging' + +/** Build a stable deduplication key for a subscription (sorted channels + sorted filter keys). */ +function subscriptionKey(channels: WsChannel[], filters?: Record): string { + const sortedChannels = [...channels].sort() + const sortedFilters: Record = {} + if (filters) { + for (const key of Object.keys(filters).sort()) { + sortedFilters[key] = filters[key] + } + } + return JSON.stringify({ channels: sortedChannels, filters: sortedFilters }) +} + +export const useWebSocketStore = defineStore('websocket', () => { + const connected = ref(false) + const reconnectExhausted = ref(false) + const subscribedChannels = ref([]) + + let socket: WebSocket | null = null + let reconnectAttempts = 0 + let reconnectTimer: ReturnType | null = null + let intentionalClose = false + let currentToken: string | null = null + const channelHandlers = new Map>() + let pendingSubscriptions: { channels: WsChannel[]; filters?: Record }[] = [] + // Track active subscriptions so reconnect can re-subscribe automatically + const activeSubscriptions: { channels: WsChannel[]; filters?: Record }[] = [] + + function getWsUrl(): string { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + return `${protocol}//${host}/api/v1/ws` + } + + function connect(token: string) { + if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) return + reconnectExhausted.value = false + + currentToken = token + intentionalClose = false + // TODO(#343): Replace with one-time WS ticket endpoint for production security. + // Currently passes JWT as query param which is logged in server/proxy/browser. + const url = `${getWsUrl()}?token=${encodeURIComponent(token)}` + socket = new WebSocket(url) + + socket.onopen = () => { + connected.value = true + reconnectAttempts = 0 + // Clear pending queue — activeSubscriptions is the single source of truth. + // Anything queued while disconnected was already added to activeSubscriptions + // by subscribe(), so replaying pendingSubscriptions would cause duplicate sends. + pendingSubscriptions = [] + // Re-subscribe to all active subscriptions (covers both reconnect and first-connect) + for (const sub of activeSubscriptions) { + try { + socket!.send(JSON.stringify({ action: 'subscribe', channels: sub.channels, filters: sub.filters })) + } catch { + // Will be retried on next reconnect + } + } + } + + socket.onmessage = (event: MessageEvent) => { + let data: unknown + try { + data = JSON.parse(event.data) + } catch (parseErr) { + console.error('Failed to parse WebSocket message:', parseErr) + return + } + + const msg = data as Record + + if (msg.action === 'subscribed' || msg.action === 'unsubscribed') { + if (Array.isArray(msg.channels)) { + subscribedChannels.value = [...(msg.channels as WsChannel[])] + } + return + } + + if (msg.error) { + console.error('WebSocket error:', sanitizeForLog(msg.error)) + return + } + + if (msg.event_type && msg.channel) { + try { + dispatchEvent(msg as unknown as WsEvent) + } catch (handlerErr) { + console.error('WebSocket event handler error:', sanitizeForLog(handlerErr), 'Event type:', sanitizeForLog(msg.event_type, 100)) + } + } + } + + socket.onclose = () => { + connected.value = false + socket = null + if (!intentionalClose && currentToken) { + scheduleReconnect() + } + } + + socket.onerror = (event) => { + console.error('WebSocket connection error:', event) + // onclose fires after onerror, reconnect is handled there + } + } + + function scheduleReconnect() { + if (reconnectTimer) clearTimeout(reconnectTimer) + if (reconnectAttempts >= WS_MAX_RECONNECT_ATTEMPTS) { + console.error('WebSocket: max reconnection attempts reached') + reconnectExhausted.value = true + return + } + const delay = Math.min( + WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts), + WS_RECONNECT_MAX_DELAY, + ) + reconnectAttempts++ + reconnectTimer = setTimeout(() => { + if (currentToken) connect(currentToken) + }, delay) + } + + function disconnect() { + intentionalClose = true + currentToken = null + reconnectAttempts = 0 + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + if (socket) { + socket.close() + socket = null + } + connected.value = false + subscribedChannels.value = [] + pendingSubscriptions = [] + activeSubscriptions.length = 0 + channelHandlers.clear() + } + + function subscribe(channels: WsChannel[], filters?: Record) { + // Track as active subscription for auto-re-subscribe on reconnect + const key = subscriptionKey(channels, filters) + if (!activeSubscriptions.some((s) => subscriptionKey(s.channels, s.filters) === key)) { + activeSubscriptions.push({ channels: [...channels], filters: filters ? { ...filters } : undefined }) + } + + if (!socket || socket.readyState !== WebSocket.OPEN) { + // Queue for replay when connection opens, with deduplication + if (!pendingSubscriptions.some((s) => subscriptionKey(s.channels, s.filters) === key)) { + pendingSubscriptions.push({ channels, filters }) + } + return + } + try { + socket.send(JSON.stringify({ action: 'subscribe', channels, filters })) + } catch { + // Socket may have transitioned to CLOSING — queue for replay + if (!pendingSubscriptions.some((s) => subscriptionKey(s.channels, s.filters) === key)) { + pendingSubscriptions.push({ channels, filters }) + } + } + } + + function unsubscribe(channels: WsChannel[]) { + // Remove from tracked subscriptions so reconnect won't re-subscribe. + // Uses every() — only removes subscriptions whose channels are fully + // covered by the unsubscribe set. Partial overlap is intentionally kept. + const channelSet = new Set(channels) + for (let i = activeSubscriptions.length - 1; i >= 0; i--) { + if (activeSubscriptions[i].channels.every((c) => channelSet.has(c))) { + activeSubscriptions.splice(i, 1) + } + } + for (let i = pendingSubscriptions.length - 1; i >= 0; i--) { + if (pendingSubscriptions[i].channels.every((c) => channelSet.has(c))) { + pendingSubscriptions.splice(i, 1) + } + } + + if (!socket || socket.readyState !== WebSocket.OPEN) return + try { + socket.send(JSON.stringify({ action: 'unsubscribe', channels })) + } catch { + // Socket transitioned to CLOSING — unsubscribe will happen on reconnect + } + } + + function onChannelEvent(channel: string, handler: WsEventHandler) { + if (!channelHandlers.has(channel)) { + channelHandlers.set(channel, new Set()) + } + channelHandlers.get(channel)!.add(handler) + } + + function offChannelEvent(channel: string, handler: WsEventHandler) { + channelHandlers.get(channel)?.delete(handler) + } + + function dispatchEvent(event: WsEvent) { + // Wrap each handler in try/catch so one failing handler doesn't block others + channelHandlers.get(event.channel)?.forEach((h) => { + try { h(event) } catch (err) { + console.error('WebSocket channel handler error:', sanitizeForLog(err)) + } + }) + channelHandlers.get('*')?.forEach((h) => { + try { h(event) } catch (err) { + console.error('WebSocket wildcard handler error:', sanitizeForLog(err)) + } + }) + } + + return { + connected, + reconnectExhausted, + subscribedChannels, + connect, + disconnect, + subscribe, + unsubscribe, + onChannelEvent, + offChannelEvent, + } +}) diff --git a/web/src/styles/global.css b/web/src/styles/global.css new file mode 100644 index 0000000000..93640c1cdc --- /dev/null +++ b/web/src/styles/global.css @@ -0,0 +1,59 @@ +@import "tailwindcss"; + +@theme { + --color-brand-50: #eff6ff; + --color-brand-100: #dbeafe; + --color-brand-200: #bfdbfe; + --color-brand-300: #93c5fd; + --color-brand-400: #60a5fa; + --color-brand-500: #3b82f6; + --color-brand-600: #2563eb; + --color-brand-700: #1d4ed8; + --color-brand-800: #1e40af; + --color-brand-900: #1e3a8a; + --color-brand-950: #172554; + + --color-surface-0: #020617; + --color-surface-50: #0f172a; + --color-surface-100: #1e293b; + --color-surface-200: #334155; + --color-surface-300: #475569; + --color-surface-400: #64748b; + --color-surface-500: #94a3b8; + --color-surface-600: #cbd5e1; + --color-surface-700: #e2e8f0; + --color-surface-800: #f1f5f9; + --color-surface-900: #f8fafc; + + --color-danger-500: #ef4444; + --color-danger-600: #dc2626; + --color-warning-500: #f59e0b; + --color-warning-600: #d97706; + --color-success-500: #22c55e; + --color-success-600: #16a34a; +} + +/* Scrollbar styling — Firefox */ +html { + scrollbar-width: thin; + scrollbar-color: var(--color-surface-300) var(--color-surface-50); +} + +/* Scrollbar styling — WebKit/Blink */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: var(--color-surface-50); +} + +::-webkit-scrollbar-thumb { + background: var(--color-surface-300); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-surface-400); +} diff --git a/web/src/styles/theme.ts b/web/src/styles/theme.ts new file mode 100644 index 0000000000..e0afb4166d --- /dev/null +++ b/web/src/styles/theme.ts @@ -0,0 +1,70 @@ +/** Dark theme color tokens used throughout the application. */ + +export const colors = { + brand: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', + }, + surface: { + 0: '#020617', + 50: '#0f172a', + 100: '#1e293b', + 200: '#334155', + 300: '#475569', + 400: '#64748b', + 500: '#94a3b8', + 600: '#cbd5e1', + 700: '#e2e8f0', + 800: '#f1f5f9', + 900: '#f8fafc', + }, + danger: { 500: '#ef4444', 600: '#dc2626' }, + warning: { 500: '#f59e0b', 600: '#d97706' }, + success: { 500: '#22c55e', 600: '#16a34a' }, +} as const + +export type Status = 'created' | 'assigned' | 'in_progress' | 'in_review' | 'completed' | 'blocked' | 'failed' | 'interrupted' | 'cancelled' | 'pending' | 'approved' | 'rejected' | 'expired' +export type Priority = 'critical' | 'high' | 'medium' | 'low' +export type RiskLevel = 'critical' | 'high' | 'medium' | 'low' + +/** Status color mapping for task/approval badges. */ +export const statusColors: Record = { + created: 'bg-slate-600 text-slate-200', + assigned: 'bg-blue-600 text-blue-100', + in_progress: 'bg-amber-600 text-amber-100', + in_review: 'bg-purple-600 text-purple-100', + completed: 'bg-green-600 text-green-100', + blocked: 'bg-red-600 text-red-100', + failed: 'bg-red-700 text-red-100', + interrupted: 'bg-orange-600 text-orange-100', + cancelled: 'bg-gray-600 text-gray-200', + pending: 'bg-amber-600 text-amber-100', + approved: 'bg-green-600 text-green-100', + rejected: 'bg-red-600 text-red-100', + expired: 'bg-gray-500 text-gray-200', +} + +/** Priority color mapping. */ +export const priorityColors: Record = { + critical: 'bg-red-600 text-red-100', + high: 'bg-orange-600 text-orange-100', + medium: 'bg-yellow-600 text-yellow-100', + low: 'bg-slate-600 text-slate-200', +} + +/** Risk level color mapping. */ +export const riskColors: Record = { + critical: 'bg-red-600 text-red-100', + high: 'bg-orange-600 text-orange-100', + medium: 'bg-yellow-600 text-yellow-100', + low: 'bg-green-600 text-green-100', +} diff --git a/web/src/utils/constants.ts b/web/src/utils/constants.ts new file mode 100644 index 0000000000..6918b9fd55 --- /dev/null +++ b/web/src/utils/constants.ts @@ -0,0 +1,73 @@ +/** Application-wide constants. */ + +import type { TaskStatus } from '@/api/types' + +export const APP_NAME = 'SynthOrg' + +export const WS_RECONNECT_BASE_DELAY = 1000 +export const WS_RECONNECT_MAX_DELAY = 30000 +export const WS_MAX_RECONNECT_ATTEMPTS = 20 +export const WS_MAX_MESSAGE_SIZE = 4096 + +export const HEALTH_POLL_INTERVAL = 15000 + +export const DEFAULT_PAGE_SIZE = 50 +export const MAX_PAGE_SIZE = 200 + +export const MIN_PASSWORD_LENGTH = 12 + +export const LOGIN_MAX_ATTEMPTS = 5 +export const LOGIN_LOCKOUT_MS = 60_000 + +/** Ordered task statuses for Kanban columns. */ +export const TASK_STATUS_ORDER: readonly TaskStatus[] = [ + 'created', + 'assigned', + 'in_progress', + 'in_review', + 'blocked', + 'completed', + 'failed', + 'interrupted', + 'cancelled', +] as const + +/** Terminal task statuses that cannot transition further. */ +export const TERMINAL_STATUSES = new Set(['completed', 'cancelled']) + +/** Task status transitions map. */ +export const VALID_TRANSITIONS: Readonly> = { + created: ['assigned'], + assigned: ['in_progress', 'blocked', 'cancelled', 'failed', 'interrupted'], + in_progress: ['in_review', 'blocked', 'cancelled', 'failed', 'interrupted'], + in_review: ['completed', 'in_progress', 'blocked', 'cancelled'], + blocked: ['assigned'], + failed: ['assigned'], + interrupted: ['assigned'], + completed: [], + cancelled: [], +} + +/** Write-capable human roles. */ +export const WRITE_ROLES = ['ceo', 'manager', 'board_member', 'pair_programmer'] as const + +/** + * Sidebar navigation items. + * Routes below '/' are registered in the page-views PR (PR 2). + * Until then the catch-all redirect handles unregistered paths. + */ +export const NAV_ITEMS = [ + { label: 'Dashboard', icon: 'pi pi-home', to: '/' }, + { label: 'Org Chart', icon: 'pi pi-sitemap', to: '/org-chart' }, + { label: 'Tasks', icon: 'pi pi-check-square', to: '/tasks' }, + { label: 'Messages', icon: 'pi pi-comments', to: '/messages' }, + { label: 'Approvals', icon: 'pi pi-shield', to: '/approvals' }, + { label: 'Agents', icon: 'pi pi-users', to: '/agents' }, + { label: 'Budget', icon: 'pi pi-chart-bar', to: '/budget' }, + { label: 'Meetings', icon: 'pi pi-video', to: '/meetings' }, + { label: 'Artifacts', icon: 'pi pi-file', to: '/artifacts' }, + { label: 'Settings', icon: 'pi pi-cog', to: '/settings' }, +] as const + +/** Type of a single navigation item derived from NAV_ITEMS. */ +export type NavItem = (typeof NAV_ITEMS)[number] diff --git a/web/src/utils/errors.ts b/web/src/utils/errors.ts new file mode 100644 index 0000000000..24a371e628 --- /dev/null +++ b/web/src/utils/errors.ts @@ -0,0 +1,66 @@ +/** Error utilities and user-friendly messages. */ + +import axios, { type AxiosError } from 'axios' + +/** + * Check if an error is an Axios error. + */ +export function isAxiosError(error: unknown): error is AxiosError { + return axios.isAxiosError(error) +} + +/** + * Extract a user-friendly error message from any error. + * Filters raw 5xx backend error strings to prevent leaking internal details. + */ +export function getErrorMessage(error: unknown): string { + if (isAxiosError(error)) { + const status = error.response?.status + const data = error.response?.data as { error?: string; success?: boolean } | undefined + + // For 4xx errors, surface the backend's validation message + if (data?.error && typeof data.error === 'string' && status !== undefined && status < 500) { + return data.error + } + + switch (status) { + case 400: + return 'Invalid request. Please check your input.' + case 401: + return 'Authentication required. Please log in.' + case 403: + return 'You do not have permission to perform this action.' + case 404: + return 'The requested resource was not found.' + case 409: + return 'Conflict: the resource was modified by another user. Please refresh and try again.' + case 422: + return 'Validation error. Please check your input.' + case 429: + return 'Too many requests. Please try again in a moment.' + case 503: + return 'Service temporarily unavailable. Please try again later.' + default: + break + } + + if (!error.response) { + return 'Network error. Please check your connection.' + } + + // For 5xx, use generic message instead of leaking server internals + return 'A server error occurred. Please try again later.' + } + + if (error instanceof Error) { + // Only surface messages from errors explicitly thrown by our own code. + // Errors from unknown sources could contain backend internals. + const msg = error.message + if (msg && msg.length < 200 && !/^\{/.test(msg)) { + return msg + } + return 'An unexpected error occurred.' + } + + return 'An unexpected error occurred.' +} diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts new file mode 100644 index 0000000000..94f4ed4310 --- /dev/null +++ b/web/src/utils/format.ts @@ -0,0 +1,80 @@ +/** Formatting utilities for dates, currency, and numbers. */ + +/** + * Format an ISO 8601 date string to a human-readable locale string. + */ +export function formatDate(iso: string | null | undefined): string { + if (!iso) return '—' + const date = new Date(iso) + if (Number.isNaN(date.getTime())) return '—' + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +/** + * Format a date as relative time (e.g., "2 hours ago"). + */ +export function formatRelativeTime(iso: string | null | undefined): string { + if (!iso) return '—' + const date = new Date(iso) + if (Number.isNaN(date.getTime())) return '—' + const now = new Date() + const diffMs = now.getTime() - date.getTime() + if (diffMs < 0) return formatDate(iso) + const diffSec = Math.floor(diffMs / 1000) + + if (diffSec < 60) return 'just now' + if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago` + if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago` + if (diffSec < 604800) return `${Math.floor(diffSec / 86400)}d ago` + return formatDate(iso) +} + +/** + * Format USD currency value. + */ +export function formatCurrency(value: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 4, + }).format(value) +} + +/** + * Format a number with locale-appropriate separators. + */ +export function formatNumber(value: number): string { + return new Intl.NumberFormat('en-US').format(value) +} + +/** + * Format seconds as a human-readable uptime string. + */ +export function formatUptime(seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) seconds = 0 + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const mins = Math.floor((seconds % 3600) / 60) + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (mins > 0 || parts.length === 0) parts.push(`${mins}m`) + return parts.join(' ') +} + +/** + * Capitalize and format a snake_case string for display. + */ +export function formatLabel(value: string): string { + return value + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') +} diff --git a/web/src/utils/logging.ts b/web/src/utils/logging.ts new file mode 100644 index 0000000000..a1e701ac9d --- /dev/null +++ b/web/src/utils/logging.ts @@ -0,0 +1,11 @@ +/** Sanitize a value for safe logging (strip control chars, truncate). */ +export function sanitizeForLog(value: unknown, maxLen = 500): string { + const raw = value instanceof Error ? value.message : String(value) + let result = '' + for (const ch of raw) { + const code = ch.charCodeAt(0) + result += (code >= 0x20 && code !== 0x7f) ? ch : ' ' + if (result.length >= maxLen) break + } + return result +} diff --git a/web/src/views/LoginPage.vue b/web/src/views/LoginPage.vue new file mode 100644 index 0000000000..d26de03902 --- /dev/null +++ b/web/src/views/LoginPage.vue @@ -0,0 +1,109 @@ + + + diff --git a/web/src/views/PlaceholderHome.vue b/web/src/views/PlaceholderHome.vue new file mode 100644 index 0000000000..78e12699f9 --- /dev/null +++ b/web/src/views/PlaceholderHome.vue @@ -0,0 +1,5 @@ + diff --git a/web/src/views/SetupPage.vue b/web/src/views/SetupPage.vue new file mode 100644 index 0000000000..5910948a01 --- /dev/null +++ b/web/src/views/SetupPage.vue @@ -0,0 +1,117 @@ + + + diff --git a/web/style.css b/web/style.css deleted file mode 100644 index 6e005b11e8..0000000000 --- a/web/style.css +++ /dev/null @@ -1,50 +0,0 @@ -* { margin: 0; padding: 0; box-sizing: border-box; } -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif; - background: #0f172a; - color: #e2e8f0; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} -.container { - text-align: center; - max-width: 480px; - padding: 2rem; -} -h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 0.5rem; - background: linear-gradient(135deg, #60a5fa, #a78bfa); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} -.subtitle { - font-size: 1.1rem; - color: #94a3b8; - margin-bottom: 2rem; -} -.status { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; -} -.status-checking { background: #1e293b; color: #94a3b8; } -.status-checking .status-dot { background: #94a3b8; } -.status-connected { background: #064e3b; color: #6ee7b7; } -.status-connected .status-dot { background: #34d399; } -.status-disconnected { background: #450a0a; color: #fca5a5; } -.status-disconnected .status-dot { background: #f87171; } diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000000..93a023d7f1 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": ["vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "env.d.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000000..0883ce3bd7 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "composite": true, + "types": ["node"] + }, + "include": ["vite.config.ts", "vitest.config.ts", "eslint.config.js"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000000..9e73cfc69e --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + ws: true, + }, + }, + }, +}) diff --git a/web/vitest.config.ts b/web/vitest.config.ts new file mode 100644 index 0000000000..8d78690761 --- /dev/null +++ b/web/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + globals: true, + environment: 'jsdom', + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,vue}'], + exclude: ['src/**/*.d.ts', 'src/main.ts', 'src/__tests__/**'], + }, + }, +})