diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cafbae..2a9380b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,3 +88,35 @@ jobs: SERVICE_CMD=${{ matrix.service.cmd }} cache-from: type=gha cache-to: type=gha,mode=max + + build-dashboard: + needs: lint-and-test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push dashboard + uses: docker/build-push-action@v6 + with: + context: frontend + file: frontend/Dockerfile + push: true + tags: | + ${{ env.IMAGE_BASE }}/dashboard:${{ github.sha }} + ${{ env.IMAGE_BASE }}/dashboard:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.kiro/specs/stonks-oracle/tasks.md b/.kiro/specs/stonks-oracle/tasks.md index 1793329..559035b 100644 --- a/.kiro/specs/stonks-oracle/tasks.md +++ b/.kiro/specs/stonks-oracle/tasks.md @@ -184,8 +184,8 @@ ## Phase 16 - Web Dashboard Frontend -- [ ] 16. Build React web dashboard for full platform control and analytics -- [ ] 16.1 Scaffold React project with Vite, TypeScript, Tailwind, and routing +- [x] 16. Build React web dashboard for full platform control and analytics +- [x] 16.1 Scaffold React project with Vite, TypeScript, Tailwind, and routing - Initialize `frontend/` directory with `npm create vite@latest` using the React-TS template - Install core dependencies: `@tanstack/react-router`, `@tanstack/react-query`, `tailwindcss`, `recharts`, `@monaco-editor/react`, `lucide-react` - Configure Tailwind with a dark-mode-friendly color palette @@ -194,13 +194,13 @@ - Create a shared API client module that targets the Query API, Symbol Registry, and Risk Engine base URLs (configurable via env vars) - _Requirements: 13.1_ -- [ ] 16.2 Build the API client layer and shared components +- [x] 16.2 Build the API client layer and shared components - Create typed API hooks using TanStack Query for each API domain: companies, documents, trends, recommendations, orders, positions, admin/trading, admin/sources, ops endpoints - Build reusable UI components: DataTable (sortable, filterable, paginated), StatusBadge, ConfidenceBar, TrendArrow, DateRangeSelector, TickerFilter, LoadingSpinner, ErrorBoundary - Build a shared layout component with sidebar, breadcrumbs, and top-bar health indicator - _Requirements: 13.1, 13.2_ -- [ ] 16.3 Implement company and source management pages +- [x] 16.3 Implement company and source management pages - Build `/companies` list page with searchable, sortable table showing ticker, name, sector, active status, source count - Build `/companies/:id` detail page with editable fields (sector, industry, market cap, active toggle), tabs for aliases, sources, and document history - Build source add/edit form with source type selector, config JSON editor, credibility slider, retention days, access policy dropdown @@ -208,19 +208,19 @@ - Wire watchlist CRUD pages at `/watchlists` with member management - _Requirements: 13.2, 1.1, 1.2, 1.3_ -- [ ] 16.4 Implement document timeline and intelligence drill-down pages +- [x] 16.4 Implement document timeline and intelligence drill-down pages - Build `/documents` list page with filterable timeline: title, type, source, ticker mentions, published date, parse quality badge, extraction status - Build `/documents/:id` detail page showing full intelligence extraction, company impacts with sentiment/score, key facts, risks, macro themes, and links to raw MinIO artifacts - Add evidence chain visualization showing document → intelligence → impact records - _Requirements: 13.3, 11.1, 11.2_ -- [ ] 16.5 Implement trend summary and evidence chain pages +- [x] 16.5 Implement trend summary and evidence chain pages - Build `/trends` list page with company trend cards showing direction indicator, strength bar, confidence score, contradiction score, and window selector - Build `/trends/:id` detail page with full evidence drill-down: contributing documents, intelligence objects, rank scores, weight breakdowns - Add expandable evidence list on trend cards for quick preview - _Requirements: 13.3, 6.5, 10.4_ -- [ ] 16.6 Implement recommendation review and order tracking pages +- [x] 16.6 Implement recommendation review and order tracking pages - Build `/recommendations` list page with filterable table: ticker, action badge, mode, confidence bar, thesis preview, timestamp - Build `/recommendations/:id` detail page with full evidence drill-down, risk evaluation display, and linked orders - Build `/orders` list page with status badges, fill info, and expandable audit trail @@ -228,21 +228,21 @@ - Build `/positions` page with current positions table showing unrealized/realized PnL, entry/current prices - _Requirements: 13.4, 11.1, 11.2, 11.3_ -- [ ] 16.7 Implement trading controls and risk management pages +- [x] 16.7 Implement trading controls and risk management pages - Build `/trading` page with trading mode toggle (paper/live/disabled) with confirmation dialog - Build pending approvals queue with approve/reject buttons and review note input - Build risk configuration editor form for max position size, daily loss cap, sector exposure, cooldown periods - Build active lockouts display with type, reason, and expiration countdown - _Requirements: 13.5, 8.1, 8.2_ -- [ ] 16.8 Implement DevOps monitoring dashboards +- [x] 16.8 Implement DevOps monitoring dashboards - Build `/ops/pipeline` page with pipeline health summary: document stage counts, parsing quality distribution, extraction validation rates, trend generation stats - Build `/ops/ingestion` page with time-series charts (Recharts) for ingestion throughput, success/failure rates by source type, configurable time bucket selector - Build `/ops/model` page with model performance metrics: success rate gauge, latency percentile chart, retry rate, confidence distribution histogram, recent failures table - Build `/ops/coverage` page with company × source type coverage matrix, stale source indicators, and coverage gap alerts - _Requirements: 13.6, 12.1, 12.2, 12.3_ -- [ ] 16.9 Implement SQL query explorer (Athena-like) +- [x] 16.9 Implement SQL query explorer (Athena-like) - Add a `/api/analytics/query` proxy endpoint to the Query API that forwards SQL to Trino, enforces row limits, and returns structured `{columns, rows, row_count, elapsed_ms}` results - Add a `/api/analytics/schema` endpoint that returns Trino catalog/schema/table/column metadata for the schema browser - Build `/analytics/query` page with Monaco Editor SQL input, schema browser sidebar, execute button, and results table with virtual scrolling @@ -250,7 +250,7 @@ - Add saved queries: persist to PostgreSQL via a new `/api/analytics/saved-queries` CRUD endpoint, display saved query list with load/delete - _Requirements: 13.7, 10.1, 10.3_ -- [ ] 16.10 Implement pre-built analytical dashboards (QuickSight-like) +- [x] 16.10 Implement pre-built analytical dashboards (QuickSight-like) - Build `/analytics/dashboards` gallery page listing available dashboards with preview thumbnails - Build Symbol Overview dashboard: company card grid with trend direction, latest recommendation, position status, sourced from API data - Build Sentiment Heatmap dashboard: sector × time matrix colored by aggregated sentiment, sourced from Trino query @@ -260,13 +260,13 @@ - Add date range selector and ticker filter controls shared across all dashboards - _Requirements: 13.8, 10.2, 10.4_ -- [ ] 16.11 Build home/overview page +- [x] 16.11 Build home/overview page - Build `/` home page with system health summary card, recent activity feed, key metrics (active companies, documents today, recommendations today, pipeline status) - Add quick-nav cards linking to each major section - Add alert banner for critical issues (source failures, pipeline bottlenecks) - _Requirements: 13.1, 13.6_ -- [ ] 16.12 Add Dockerfile, CI build, Helm template, and deploy to cluster +- [x] 16.12 Add Dockerfile, CI build, Helm template, and deploy to cluster - Create `frontend/Dockerfile` using multi-stage build: node for build, nginx for serve - Add `dashboard` service to `.github/workflows/build.yml` matrix - Add `dashboard` deployment, service, and ingress to Helm chart values and templates diff --git a/Makefile b/Makefile index 8b520d2..aa3c174 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,11 @@ build: -t $(GHCR)/$$svc:latest \ -f docker/Dockerfile . || exit 1; \ done + @echo "Building dashboard..." + docker build \ + -t $(GHCR)/dashboard:$(SHA) \ + -t $(GHCR)/dashboard:latest \ + -f frontend/Dockerfile frontend/ || exit 1 push: @for svc in $(SERVICES); do \ diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..1a741cb --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +ARG VITE_QUERY_API_URL="" +ARG VITE_SYMBOL_REGISTRY_URL="" +ARG VITE_RISK_ENGINE_URL="" +RUN npm run build + +# Stage 2: Serve +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d715e83 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Stonks Oracle + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..9374ae3 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA fallback — all routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the query-api service + location /api/ { + proxy_pass http://query-api:8000; + 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; + } + + # Cache static assets + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..01cdffd --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3872 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-query": "^5.99.0", + "@tanstack/react-router": "^1.168.18", + "lucide-react": "^1.8.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "recharts": "^3.8.1", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/eslintrc/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/@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/@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/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "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.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/history": { + "version": "1.161.6", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", + "integrity": "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.0.tgz", + "integrity": "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.0.tgz", + "integrity": "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.99.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.168.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.18.tgz", + "integrity": "sha512-RmBptS3/qtkGhvG/u41JWOgxz1FIWybBz7iBTgLUIoFkqOj6NE4XlhUOsP2fabxACtbZdJnpvCWcJFWpWGIngw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.168.14", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.168.14", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.14.tgz", + "integrity": "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "cookie-es": "^3.0.0", + "seroval": "^1.5.0", + "seroval-plugins": "^1.5.0" + }, + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "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==", + "dev": true, + "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": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" + }, + "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.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "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.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.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.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" + }, + "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.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "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-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "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/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/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/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/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "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-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-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "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-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "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/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-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "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/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/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.335", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", + "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "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/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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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-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/isbot": { + "version": "5.1.37", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.37.tgz", + "integrity": "sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "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/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-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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.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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "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/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.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/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "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/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/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/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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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/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/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/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "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/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz", + "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.2.tgz", + "integrity": "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "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/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/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "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/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/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "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/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" + }, + "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.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/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/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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9005a61 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-query": "^5.99.0", + "@tanstack/react-router": "^1.168.18", + "lucide-react": "^1.8.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "recharts": "^3.8.1", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..564edbc --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,79 @@ +/** + * Shared API client targeting Query API, Symbol Registry, and Risk Engine. + * Base URLs are configurable via env vars (VITE_*). + */ + +const QUERY_API_BASE = import.meta.env.VITE_QUERY_API_URL ?? ''; +const SYMBOL_REGISTRY_BASE = import.meta.env.VITE_SYMBOL_REGISTRY_URL ?? ''; +const RISK_ENGINE_BASE = import.meta.env.VITE_RISK_ENGINE_URL ?? ''; + +export type ApiBase = 'query' | 'registry' | 'risk'; + +function baseUrl(api: ApiBase): string { + switch (api) { + case 'query': + return QUERY_API_BASE; + case 'registry': + return SYMBOL_REGISTRY_BASE; + case 'risk': + return RISK_ENGINE_BASE; + } +} + +export class ApiError extends Error { + status: number; + body: unknown; + + constructor(status: number, body: unknown) { + super(`API error ${status}`); + this.name = 'ApiError'; + this.status = status; + this.body = body; + } +} + +export async function apiFetch( + api: ApiBase, + path: string, + init?: RequestInit, +): Promise { + const url = `${baseUrl(api)}${path}`; + const res = await fetch(url, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...init?.headers, + }, + }); + if (!res.ok) { + const body = await res.json().catch(() => null); + throw new ApiError(res.status, body); + } + return res.json() as Promise; +} + +/** Convenience GET */ +export function apiGet(api: ApiBase, path: string): Promise { + return apiFetch(api, path); +} + +/** Convenience POST */ +export function apiPost(api: ApiBase, path: string, body: unknown): Promise { + return apiFetch(api, path, { + method: 'POST', + body: JSON.stringify(body), + }); +} + +/** Convenience PUT */ +export function apiPut(api: ApiBase, path: string, body: unknown): Promise { + return apiFetch(api, path, { + method: 'PUT', + body: JSON.stringify(body), + }); +} + +/** Convenience DELETE */ +export function apiDelete(api: ApiBase, path: string): Promise { + return apiFetch(api, path, { method: 'DELETE' }); +} diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts new file mode 100644 index 0000000..fcf5e52 --- /dev/null +++ b/frontend/src/api/hooks.ts @@ -0,0 +1,476 @@ +/** + * Typed TanStack Query hooks for all API domains. + * Requirements: 13.1, 13.2 + */ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiGet, apiPost, apiPut } from './client'; +import type { ApiBase } from './client'; + +// --------------------------------------------------------------------------- +// Generic helpers +// --------------------------------------------------------------------------- + +function useGet(key: unknown[], api: ApiBase, path: string, enabled = true) { + return useQuery({ queryKey: key, queryFn: () => apiGet(api, path), enabled }); +} + +// --------------------------------------------------------------------------- +// Companies (Query API) +// --------------------------------------------------------------------------- + +export interface Company { + id: string; + ticker: string; + legal_name: string; + exchange: string | null; + sector: string | null; + industry: string | null; + market_cap_bucket: string | null; + active: boolean; + created_at?: string; + updated_at?: string; + aliases?: Alias[]; + active_source_count?: number; +} + +export interface Alias { + id: string; + alias: string; + alias_type: string; +} + +export interface Source { + id: string; + source_type: string; + source_name: string; + config?: Record; + credibility_score: number; + retention_days?: number; + access_policy?: string; + active: boolean; +} + +export function useCompanies(params?: { active?: boolean; sector?: string; ticker?: string }) { + const qs = new URLSearchParams(); + if (params?.active !== undefined) qs.set('active', String(params.active)); + if (params?.sector) qs.set('sector', params.sector); + if (params?.ticker) qs.set('ticker', params.ticker); + const path = `/api/companies${qs.toString() ? '?' + qs : ''}`; + return useGet(['companies', params], 'query', path); +} + +export function useCompany(id: string | undefined) { + return useGet(['company', id], 'query', `/api/companies/${id}`, !!id); +} + +export function useCompanySources(companyId: string | undefined) { + return useGet(['company-sources', companyId], 'query', `/api/companies/${companyId}/sources`, !!companyId); +} + +// --------------------------------------------------------------------------- +// Symbol Registry (CRUD) +// --------------------------------------------------------------------------- + +export function useRegistryCompanies(active = true) { + return useGet(['registry-companies', active], 'registry', `/companies?active=${active}`); +} + +export function useCreateCompany() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { ticker: string; legal_name: string; sector?: string; industry?: string; exchange?: string; market_cap_bucket?: string }) => + apiPost('registry', '/companies', body), + onSuccess: () => qc.invalidateQueries({ queryKey: ['companies'] }), + }); +} + +export function useCreateSource(companyId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { source_type: string; source_name: string; config?: Record; credibility_score?: number; retention_days?: number; access_policy?: string }) => + apiPost('registry', `/companies/${companyId}/sources`, body), + onSuccess: () => qc.invalidateQueries({ queryKey: ['company-sources', companyId] }), + }); +} + +export function useCreateAlias(companyId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { alias: string; alias_type?: string }) => + apiPost('registry', `/companies/${companyId}/aliases`, body), + onSuccess: () => qc.invalidateQueries({ queryKey: ['company', companyId] }), + }); +} + +// --------------------------------------------------------------------------- +// Watchlists +// --------------------------------------------------------------------------- + +export interface Watchlist { + id: string; + name: string; + description: string | null; + active: boolean; +} + +export function useWatchlists() { + return useGet(['watchlists'], 'registry', '/watchlists'); +} + +export function useCreateWatchlist() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { name: string; description?: string }) => + apiPost('registry', '/watchlists', body), + onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlists'] }), + }); +} + +export function useWatchlistMembers(watchlistId: string | undefined) { + return useGet(['watchlist-members', watchlistId], 'registry', `/watchlists/${watchlistId}/members`, !!watchlistId); +} + +// --------------------------------------------------------------------------- +// Documents +// --------------------------------------------------------------------------- + +export interface Document { + id: string; + document_type: string; + source_type: string; + publisher: string | null; + url: string | null; + title: string | null; + published_at: string | null; + retrieved_at: string | null; + language: string | null; + content_hash: string | null; + parse_quality_score: number | null; + parse_confidence: string | null; + status: string; + created_at: string; +} + +export interface DocumentDetail extends Document { + canonical_url: string | null; + raw_storage_ref: string | null; + normalized_storage_ref: string | null; + company_mentions: Array<{ company_id: string; ticker: string; mention_type: string; confidence: number; legal_name: string }>; + intelligence: DocumentIntelligence | null; +} + +export interface DocumentIntelligence { + id: string; + summary: string | null; + macro_themes: string[] | null; + novelty_score: number | null; + source_credibility: number | null; + extraction_warnings: string[] | null; + confidence: number | null; + model_provider: string | null; + model_name: string | null; + prompt_version: string | null; + schema_version: string | null; + validation_status: string | null; + company_impacts?: CompanyImpact[]; +} + +export interface CompanyImpact { + company_id: string; + ticker: string; + legal_name: string; + relevance: number; + sentiment: string; + impact_score: number; + impact_horizon: string; + catalyst_type: string; + key_facts: string[] | null; + risks: string[] | null; + evidence_spans: string[] | null; +} + +export function useDocuments(params?: { ticker?: string; document_type?: string; status?: string; since?: string; limit?: number; offset?: number }) { + const qs = new URLSearchParams(); + if (params?.ticker) qs.set('ticker', params.ticker); + if (params?.document_type) qs.set('document_type', params.document_type); + if (params?.status) qs.set('status', params.status); + if (params?.since) qs.set('since', params.since); + if (params?.limit) qs.set('limit', String(params.limit)); + if (params?.offset) qs.set('offset', String(params.offset)); + const path = `/api/documents${qs.toString() ? '?' + qs : ''}`; + return useGet(['documents', params], 'query', path); +} + +export function useDocument(id: string | undefined) { + return useGet(['document', id], 'query', `/api/documents/${id}`, !!id); +} + +// --------------------------------------------------------------------------- +// Trends +// --------------------------------------------------------------------------- + +export interface TrendSummary { + id: string; + entity_type: string; + entity_id: string; + window: string; + trend_direction: string; + trend_strength: number; + confidence: number; + top_supporting_evidence: string[] | null; + top_opposing_evidence: string[] | null; + dominant_catalysts: string[] | null; + material_risks: string[] | null; + contradiction_score: number; + market_context: Record | null; + generated_at: string; +} + +export function useTrends(params?: { ticker?: string; window?: string; limit?: number; offset?: number }) { + const qs = new URLSearchParams(); + if (params?.ticker) qs.set('ticker', params.ticker); + if (params?.window) qs.set('window', params.window); + if (params?.limit) qs.set('limit', String(params.limit)); + if (params?.offset) qs.set('offset', String(params.offset)); + const path = `/api/trends${qs.toString() ? '?' + qs : ''}`; + return useGet(['trends', params], 'query', path); +} + +export function useTrend(id: string | undefined) { + return useGet(['trend', id], 'query', `/api/trends/${id}`, !!id); +} + +export function useTrendEvidence(id: string | undefined) { + return useGet<{ trend: TrendSummary; evidence: unknown[] }>(['trend-evidence', id], 'query', `/api/trends/${id}/evidence`, !!id); +} + +// --------------------------------------------------------------------------- +// Recommendations +// --------------------------------------------------------------------------- + +export interface Recommendation { + id: string; + ticker: string; + action: string; + mode: string; + confidence: number; + time_horizon: string; + thesis: string | null; + invalidation_conditions: string[] | null; + portfolio_pct: number | null; + max_loss_pct: number | null; + model_version: string | null; + risk_classification: string | null; + generated_at: string; +} + +export interface RecommendationDetail extends Recommendation { + company_id: string | null; + evidence: Array<{ id: string; document_id: string; intelligence_id: string; evidence_type: string; weight: number; title: string; document_type: string; source_type: string; publisher: string; url: string; published_at: string }>; + risk_evaluation: { id: string; eligible: boolean; allowed_mode: string; rejection_reasons: string[] | null; risk_checks: Record | null; evaluated_at: string } | null; +} + +export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; limit?: number; offset?: number }) { + const qs = new URLSearchParams(); + if (params?.ticker) qs.set('ticker', params.ticker); + if (params?.action) qs.set('action', params.action); + if (params?.mode) qs.set('mode', params.mode); + if (params?.since) qs.set('since', params.since); + if (params?.limit) qs.set('limit', String(params.limit)); + if (params?.offset) qs.set('offset', String(params.offset)); + const path = `/api/recommendations${qs.toString() ? '?' + qs : ''}`; + return useGet(['recommendations', params], 'query', path); +} + +export function useRecommendation(id: string | undefined) { + return useGet(['recommendation', id], 'query', `/api/recommendations/${id}`, !!id); +} + +// --------------------------------------------------------------------------- +// Orders +// --------------------------------------------------------------------------- + +export interface Order { + id: string; + recommendation_id: string | null; + broker_account_id: string | null; + ticker: string; + side: string; + order_type: string; + quantity: number; + limit_price: number | null; + stop_price: number | null; + status: string; + broker_order_id: string | null; + submitted_at: string | null; + fill_price: number | null; + fill_quantity: number | null; + created_at: string; +} + +export interface OrderDetail extends Order { + idempotency_key: string | null; + decision_trace: Record | null; + events: Array<{ id: string; event_type: string; data: unknown; broker_timestamp: string | null; created_at: string }>; + audit_trail: unknown[]; +} + +export function useOrders(params?: { ticker?: string; status?: string; side?: string; since?: string; limit?: number; offset?: number }) { + const qs = new URLSearchParams(); + if (params?.ticker) qs.set('ticker', params.ticker); + if (params?.status) qs.set('status', params.status); + if (params?.side) qs.set('side', params.side); + if (params?.since) qs.set('since', params.since); + if (params?.limit) qs.set('limit', String(params.limit)); + if (params?.offset) qs.set('offset', String(params.offset)); + const path = `/api/orders${qs.toString() ? '?' + qs : ''}`; + return useGet(['orders', params], 'query', path); +} + +export function useOrder(id: string | undefined) { + return useGet(['order', id], 'query', `/api/orders/${id}`, !!id); +} + +// --------------------------------------------------------------------------- +// Positions +// --------------------------------------------------------------------------- + +export interface Position { + id: string; + broker_account_id: string | null; + ticker: string; + quantity: number; + avg_entry_price: number; + current_price: number | null; + unrealized_pnl: number | null; + realized_pnl: number | null; + updated_at: string; +} + +export function usePositions(ticker?: string) { + const path = ticker ? `/api/positions?ticker=${ticker}` : '/api/positions'; + return useGet(['positions', ticker], 'query', path); +} + +// --------------------------------------------------------------------------- +// Admin: Trading +// --------------------------------------------------------------------------- + +export interface TradingConfig { + id?: string; + name?: string; + trading_mode: string; + config: Record; +} + +export interface Approval { + id: string; + order_job: unknown; + recommendation_id: string | null; + ticker: string; + side: string; + quantity: number; + estimated_value: number | null; + status: string; + requested_at: string; + expires_at: string | null; +} + +export interface Lockout { + id: string; + ticker: string; + lockout_type: string; + reason: string; + expires_at: string; + created_at: string; +} + +export function useTradingConfig() { + return useGet(['trading-config'], 'query', '/api/admin/trading/config'); +} + +export function useSetTradingMode() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (mode: string) => apiPut('query', `/api/admin/trading/mode?mode=${mode}`, {}), + onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }), + }); +} + +export function usePendingApprovals() { + return useGet(['pending-approvals'], 'query', '/api/admin/trading/approvals'); +} + +export function useReviewApproval() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, approved, review_note }: { id: string; approved: boolean; review_note?: string }) => + apiPut('query', `/api/admin/trading/approvals/${id}?approved=${approved}&reviewed_by=operator&review_note=${encodeURIComponent(review_note ?? '')}`, {}), + onSuccess: () => qc.invalidateQueries({ queryKey: ['pending-approvals'] }), + }); +} + +export function useActiveLockouts() { + return useGet(['lockouts'], 'query', '/api/admin/trading/lockouts'); +} + +// --------------------------------------------------------------------------- +// Admin: Sources +// --------------------------------------------------------------------------- + +export interface SourceHealth { + source_id: string; + source_type: string; + source_name: string; + credibility_score: number; + active: boolean; + ticker: string; + legal_name: string; + company_id: string; + last_run_status: string | null; + last_run_at: string | null; + last_error: string | null; + total_runs_24h: number; + failed_runs_24h: number; + total_items_24h: number; +} + +export function useSourceHealth(params?: { source_type?: string; company_id?: string }) { + const qs = new URLSearchParams(); + if (params?.source_type) qs.set('source_type', params.source_type); + if (params?.company_id) qs.set('company_id', params.company_id); + const path = `/api/admin/sources/health${qs.toString() ? '?' + qs : ''}`; + return useGet(['source-health', params], 'query', path); +} + +// --------------------------------------------------------------------------- +// Ops: Pipeline, Ingestion, Model, Coverage +// --------------------------------------------------------------------------- + +export function usePipelineHealth(hours = 24) { + return useGet>(['pipeline-health', hours], 'query', `/api/ops/pipeline/health?hours=${hours}`); +} + +export function useIngestionSummary(hours = 24) { + return useGet>(['ingestion-summary', hours], 'query', `/api/ops/ingestion/summary?hours=${hours}`); +} + +export function useIngestionThroughput(hours = 24, bucket = '1h') { + return useGet(['ingestion-throughput', hours, bucket], 'query', `/api/ops/ingestion/throughput?hours=${hours}&bucket=${bucket}`); +} + +export function useModelPerformance(hours = 24) { + return useGet>(['model-performance', hours], 'query', `/api/ops/model/performance?hours=${hours}`); +} + +export function useModelFailures(hours = 24) { + return useGet(['model-failures', hours], 'query', `/api/ops/model/failures?hours=${hours}`); +} + +export function useCoverageGaps() { + return useGet<{ missing_source_types: unknown[]; stale_sources: unknown[] }>(['coverage-gaps'], 'query', '/api/ops/sources/coverage-gaps'); +} + +export function useSymbolCoverage() { + return useGet(['symbol-coverage'], 'query', '/api/admin/companies/coverage'); +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx new file mode 100644 index 0000000..2b2855c --- /dev/null +++ b/frontend/src/components/AppLayout.tsx @@ -0,0 +1,105 @@ +import type { ReactNode } from 'react'; +import { Link, useRouterState } from '@tanstack/react-router'; +import { + Home, + Building2, + FileText, + TrendingUp, + Lightbulb, + ShoppingCart, + Wallet, + ShieldCheck, + Activity, + Download, + Cpu, + Radar, + Terminal, + LayoutDashboard, + List, +} from 'lucide-react'; + +interface NavItem { + to: string; + label: string; + icon: ReactNode; + group?: string; +} + +const navItems: NavItem[] = [ + { to: '/', label: 'Home', icon: }, + { to: '/companies', label: 'Companies', icon: , group: 'Data' }, + { to: '/watchlists', label: 'Watchlists', icon: , group: 'Data' }, + { to: '/documents', label: 'Documents', icon: , group: 'Data' }, + { to: '/trends', label: 'Trends', icon: , group: 'Intelligence' }, + { to: '/recommendations', label: 'Recommendations', icon: , group: 'Intelligence' }, + { to: '/orders', label: 'Orders', icon: , group: 'Trading' }, + { to: '/positions', label: 'Positions', icon: , group: 'Trading' }, + { to: '/trading', label: 'Trading Controls', icon: , group: 'Trading' }, + { to: '/ops/pipeline', label: 'Pipeline', icon: , group: 'Ops' }, + { to: '/ops/ingestion', label: 'Ingestion', icon: , group: 'Ops' }, + { to: '/ops/model', label: 'Model Perf', icon: , group: 'Ops' }, + { to: '/ops/coverage', label: 'Coverage', icon: , group: 'Ops' }, + { to: '/analytics/query', label: 'SQL Explorer', icon: , group: 'Analytics' }, + { to: '/analytics/dashboards', label: 'Dashboards', icon: , group: 'Analytics' }, +]; + +export function AppLayout({ children }: { children: ReactNode }) { + const routerState = useRouterState(); + const currentPath = routerState.location.pathname; + + let lastGroup: string | undefined; + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx new file mode 100644 index 0000000..8274c48 --- /dev/null +++ b/frontend/src/components/DataTable.tsx @@ -0,0 +1,144 @@ +import { useState, useMemo, type ReactNode } from 'react'; +import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'; + +export interface Column { + key: string; + header: string; + render?: (row: T) => ReactNode; + sortable?: boolean; + className?: string; +} + +interface Props { + data: T[]; + columns: Column[]; + keyField: keyof T & string; + pageSize?: number; + onRowClick?: (row: T) => void; + filterFn?: (row: T, query: string) => boolean; + emptyMessage?: string; +} + +export function DataTable({ + data, + columns, + keyField, + pageSize = 25, + onRowClick, + filterFn, + emptyMessage = 'No data', +}: Props) { + const [sortKey, setSortKey] = useState(null); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); + const [page, setPage] = useState(0); + const [filter, setFilter] = useState(''); + + const filtered = useMemo(() => { + if (!filter || !filterFn) return data; + return data.filter((row) => filterFn(row, filter)); + }, [data, filter, filterFn]); + + const sorted = useMemo(() => { + if (!sortKey) return filtered; + return [...filtered].sort((a, b) => { + const av = (a as Record)[sortKey]; + const bv = (b as Record)[sortKey]; + if (av == null && bv == null) return 0; + if (av == null) return 1; + if (bv == null) return -1; + const cmp = av < bv ? -1 : av > bv ? 1 : 0; + return sortDir === 'asc' ? cmp : -cmp; + }); + }, [filtered, sortKey, sortDir]); + + const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); + const paged = sorted.slice(page * pageSize, (page + 1) * pageSize); + + function toggleSort(key: string) { + if (sortKey === key) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortKey(key); + setSortDir('asc'); + } + } + + return ( +
+ {filterFn && ( + { setFilter(e.target.value); setPage(0); }} + className="mb-3 w-full max-w-xs rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" + aria-label="Filter table" + /> + )} + +
+ + + + {columns.map((col) => ( + + ))} + + + + {paged.length === 0 ? ( + + + + ) : ( + paged.map((row) => ( + )[keyField])} + className={`border-b border-surface-700/50 ${onRowClick ? 'cursor-pointer hover:bg-surface-800/50' : ''}`} + onClick={() => onRowClick?.(row)} + > + {columns.map((col) => ( + + ))} + + )) + )} + +
col.sortable !== false && toggleSort(col.key)} + aria-sort={sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined} + > + + {col.header} + {col.sortable !== false && ( + sortKey === col.key + ? (sortDir === 'asc' ? : ) + : + )} + +
+ {emptyMessage} +
+ {col.render ? col.render(row) : String((row as Record)[col.key] ?? '—')} +
+
+ + {totalPages > 1 && ( +
+ {sorted.length} rows +
+ + {page + 1} / {totalPages} + +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/ui.tsx b/frontend/src/components/ui.tsx new file mode 100644 index 0000000..71740e8 --- /dev/null +++ b/frontend/src/components/ui.tsx @@ -0,0 +1,166 @@ +/** + * Shared reusable UI components: StatusBadge, ConfidenceBar, TrendArrow, + * DateRangeSelector, TickerFilter, LoadingSpinner, ErrorBoundary. + * Requirements: 13.1, 13.2 + */ +import { Component, type ReactNode, type ChangeEvent } from 'react'; +import { TrendingUp, TrendingDown, Minus, Loader2 } from 'lucide-react'; + +// --------------------------------------------------------------------------- +// StatusBadge +// --------------------------------------------------------------------------- + +const statusColors: Record = { + completed: 'bg-green-900/40 text-green-400 border-green-700/50', + success: 'bg-green-900/40 text-green-400 border-green-700/50', + valid: 'bg-green-900/40 text-green-400 border-green-700/50', + active: 'bg-green-900/40 text-green-400 border-green-700/50', + approved: 'bg-green-900/40 text-green-400 border-green-700/50', + filled: 'bg-green-900/40 text-green-400 border-green-700/50', + running: 'bg-blue-900/40 text-blue-400 border-blue-700/50', + pending: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', + failed: 'bg-red-900/40 text-red-400 border-red-700/50', + rejected: 'bg-red-900/40 text-red-400 border-red-700/50', + cancelled: 'bg-gray-800/40 text-gray-400 border-gray-700/50', + disabled: 'bg-gray-800/40 text-gray-400 border-gray-700/50', + paper: 'bg-purple-900/40 text-purple-400 border-purple-700/50', + live: 'bg-orange-900/40 text-orange-400 border-orange-700/50', + buy: 'bg-green-900/40 text-green-400 border-green-700/50', + sell: 'bg-red-900/40 text-red-400 border-red-700/50', + hold: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', + watch: 'bg-blue-900/40 text-blue-400 border-blue-700/50', +}; + +export function StatusBadge({ status }: { status: string | null | undefined }) { + const s = (status ?? 'unknown').toLowerCase(); + const cls = statusColors[s] ?? 'bg-gray-800/40 text-gray-400 border-gray-700/50'; + return ( + + {s} + + ); +} + +// --------------------------------------------------------------------------- +// ConfidenceBar +// --------------------------------------------------------------------------- + +export function ConfidenceBar({ value, className = '' }: { value: number | null | undefined; className?: string }) { + const pct = Math.round((value ?? 0) * 100); + const color = pct >= 70 ? 'bg-green-500' : pct >= 40 ? 'bg-yellow-500' : 'bg-red-500'; + return ( +
+
+
+
+ {pct}% +
+ ); +} + +// --------------------------------------------------------------------------- +// TrendArrow +// --------------------------------------------------------------------------- + +export function TrendArrow({ direction }: { direction: string | null | undefined }) { + const d = (direction ?? 'neutral').toLowerCase(); + if (d === 'bullish') return ; + if (d === 'bearish') return ; + return ; +} + +// --------------------------------------------------------------------------- +// DateRangeSelector +// --------------------------------------------------------------------------- + +export function DateRangeSelector({ value, onChange }: { value: number; onChange: (hours: number) => void }) { + const options = [ + { label: '1h', hours: 1 }, + { label: '6h', hours: 6 }, + { label: '24h', hours: 24 }, + { label: '7d', hours: 168 }, + ]; + return ( +
+ {options.map((opt) => ( + + ))} +
+ ); +} + +// --------------------------------------------------------------------------- +// TickerFilter +// --------------------------------------------------------------------------- + +export function TickerFilter({ value, onChange }: { value: string; onChange: (v: string) => void }) { + return ( + ) => onChange(e.target.value.toUpperCase())} + className="w-24 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-xs text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" + aria-label="Filter by ticker" + /> + ); +} + +// --------------------------------------------------------------------------- +// LoadingSpinner +// --------------------------------------------------------------------------- + +export function LoadingSpinner({ className = '' }: { className?: string }) { + return ( +
+ +
+ ); +} + +// --------------------------------------------------------------------------- +// ErrorBoundary +// --------------------------------------------------------------------------- + +interface EBProps { children: ReactNode; fallback?: ReactNode } +interface EBState { error: Error | null } + +export class ErrorBoundary extends Component { + state: EBState = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + render() { + if (this.state.error) { + return this.props.fallback ?? ( +
+ Something went wrong: {this.state.error.message} +
+ ); + } + return this.props.children; + } +} + +// --------------------------------------------------------------------------- +// Card — generic container +// --------------------------------------------------------------------------- + +export function Card({ children, className = '' }: { children: ReactNode; className?: string }) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..98cd355 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,26 @@ +@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-surface-50: #f8fafc; + --color-surface-100: #f1f5f9; + --color-surface-700: #334155; + --color-surface-800: #1e293b; + --color-surface-850: #172033; + --color-surface-900: #0f172a; + --color-surface-950: #020617; + + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-danger: #ef4444; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..3d66ffd --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,25 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { RouterProvider } from '@tanstack/react-router'; +import { router } from './routes'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + gcTime: 5 * 60_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/frontend/src/pages/Companies.tsx b/frontend/src/pages/Companies.tsx new file mode 100644 index 0000000..88f62a8 --- /dev/null +++ b/frontend/src/pages/Companies.tsx @@ -0,0 +1,49 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useCompanies } from '../api/hooks'; +import { DataTable, type Column } from '../components/DataTable'; +import { StatusBadge, LoadingSpinner } from '../components/ui'; +import type { Company } from '../api/hooks'; + +const columns: Column[] = [ + { key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' }, + { key: 'legal_name', header: 'Name' }, + { key: 'sector', header: 'Sector' }, + { + key: 'active', + header: 'Status', + render: (r) => , + }, + { + key: 'active_source_count', + header: 'Sources', + render: (r) => {r.active_source_count ?? '—'}, + }, +]; + +export function CompaniesPage() { + const navigate = useNavigate(); + const { data, isLoading, error } = useCompanies(); + + if (isLoading) return ; + if (error) return
Failed to load companies
; + + return ( +
+

Companies

+ + data={data ?? []} + columns={columns} + keyField="id" + onRowClick={(row) => navigate({ to: '/companies/$id', params: { id: row.id } })} + filterFn={(row, q) => { + const lq = q.toLowerCase(); + return ( + row.ticker.toLowerCase().includes(lq) || + (row.legal_name ?? '').toLowerCase().includes(lq) || + (row.sector ?? '').toLowerCase().includes(lq) + ); + }} + /> +
+ ); +} diff --git a/frontend/src/pages/CompanyDetail.tsx b/frontend/src/pages/CompanyDetail.tsx new file mode 100644 index 0000000..c186476 --- /dev/null +++ b/frontend/src/pages/CompanyDetail.tsx @@ -0,0 +1,190 @@ +import { useParams } from '@tanstack/react-router'; +import { useState } from 'react'; +import { useCompany, useCompanySources, useCreateAlias, useCreateSource } from '../api/hooks'; +import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; +import { DataTable, type Column } from '../components/DataTable'; +import type { Source } from '../api/hooks'; +import type { Alias } from '../api/hooks'; + +const sourceCols: Column[] = [ + { key: 'source_type', header: 'Type' }, + { key: 'source_name', header: 'Name' }, + { key: 'credibility_score', header: 'Credibility', render: (r) => {(r.credibility_score * 100).toFixed(0)}% }, + { key: 'active', header: 'Status', render: (r) => }, +]; + +export function CompanyDetailPage() { + const { id } = useParams({ from: '/companies/$id' }); + const { data: company, isLoading } = useCompany(id); + const { data: sources } = useCompanySources(id); + const [tab, setTab] = useState<'aliases' | 'sources'>('sources'); + + if (isLoading || !company) return ; + + return ( +
+
+

{company.ticker}

+ +
+ + +
+
Name
{company.legal_name}
+
Exchange
{company.exchange ?? '—'}
+
Sector
{company.sector ?? '—'}
+
Industry
{company.industry ?? '—'}
+
Market Cap
{company.market_cap_bucket ?? '—'}
+
Sources
{company.active_source_count ?? 0}
+
+
+ + {/* Tabs */} +
+ {(['sources', 'aliases'] as const).map((t) => ( + + ))} +
+ + {tab === 'sources' && ( +
+ data={sources ?? []} columns={sourceCols} keyField="id" /> + +
+ )} + + {tab === 'aliases' && ( +
+ + +
+ )} +
+ ); +} + +function AliasesList({ aliases }: { aliases: Alias[] }) { + if (aliases.length === 0) return

No aliases configured

; + return ( +
+ {aliases.map((a) => ( + + {a.alias} ({a.alias_type}) + + ))} +
+ ); +} + +function AddAliasForm({ companyId }: { companyId: string }) { + const [alias, setAlias] = useState(''); + const mutation = useCreateAlias(companyId); + + return ( +
{ + e.preventDefault(); + if (alias.trim()) mutation.mutate({ alias: alias.trim() }, { onSuccess: () => setAlias('') }); + }} + > + setAlias(e.target.value)} + className="rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" + aria-label="New alias" + /> + +
+ ); +} + +function AddSourceForm({ companyId }: { companyId: string }) { + const [open, setOpen] = useState(false); + const [sourceType, setSourceType] = useState('market_api'); + const [sourceName, setSourceName] = useState(''); + const [credibility, setCredibility] = useState(0.5); + const mutation = useCreateSource(companyId); + + if (!open) { + return ( + + ); + } + + return ( + +
{ + e.preventDefault(); + mutation.mutate( + { source_type: sourceType, source_name: sourceName, credibility_score: credibility }, + { onSuccess: () => { setOpen(false); setSourceName(''); } }, + ); + }} + > +
+
+ + +
+
+ + setSourceName(e.target.value)} + className="w-full rounded-md border border-surface-700 bg-surface-900 px-2 py-1.5 text-sm text-gray-200 placeholder-gray-500" + placeholder="Source name" + required + /> +
+
+
+ + setCredibility(Number(e.target.value))} + className="w-full" + /> +
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/Dashboards.tsx b/frontend/src/pages/Dashboards.tsx new file mode 100644 index 0000000..98e3f6f --- /dev/null +++ b/frontend/src/pages/Dashboards.tsx @@ -0,0 +1,315 @@ +import { useState } from 'react'; +import { useCompanies, useTrends, useRecommendations, usePositions } from '../api/hooks'; +import { useQuery } from '@tanstack/react-query'; +import { apiPost } from '../api/client'; +import { TrendArrow, StatusBadge, ConfidenceBar, LoadingSpinner, DateRangeSelector, TickerFilter, Card } from '../components/ui'; +import { + LineChart, Line, BarChart, Bar, ScatterChart, Scatter, + XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, +} from 'recharts'; + +type DashboardId = 'gallery' | 'symbol-overview' | 'sentiment-heatmap' | 'prediction-accuracy' | 'paper-pnl' | 'model-quality'; + +const dashboards: Array<{ id: DashboardId; title: string; description: string }> = [ + { id: 'symbol-overview', title: 'Symbol Overview', description: 'Company cards with trend direction, latest recommendation, position status' }, + { id: 'sentiment-heatmap', title: 'Sentiment Heatmap', description: 'Sector × time matrix colored by aggregated sentiment' }, + { id: 'prediction-accuracy', title: 'Prediction Accuracy', description: 'Predicted confidence vs realized price move' }, + { id: 'paper-pnl', title: 'Paper Trading PnL', description: 'Equity curve, daily PnL bars, win rate metrics' }, + { id: 'model-quality', title: 'Model Quality', description: 'Extraction success rate, latency distribution, retry rate' }, +]; + +export function DashboardsPage() { + const [active, setActive] = useState('gallery'); + const [hours, setHours] = useState(168); + const [ticker, setTicker] = useState(''); + + return ( +
+
+

Dashboards

+
+ + +
+
+ + {/* Gallery / nav */} + {active === 'gallery' ? ( +
+ {dashboards.map((d) => ( + + + + ))} +
+ ) : ( +
+ + {active === 'symbol-overview' && } + {active === 'sentiment-heatmap' && } + {active === 'prediction-accuracy' && } + {active === 'paper-pnl' && } + {active === 'model-quality' && } +
+ )} +
+ ); +} + +// --------------------------------------------------------------------------- +// Symbol Overview +// --------------------------------------------------------------------------- + +function SymbolOverview({ ticker }: { ticker: string }) { + const { data: companies, isLoading: cLoading } = useCompanies({ ticker: ticker || undefined }); + const { data: trends } = useTrends({ ticker: ticker || undefined, window: '7d', limit: 100 }); + const { data: recs } = useRecommendations({ ticker: ticker || undefined, limit: 100 }); + const { data: positions } = usePositions(ticker || undefined); + + if (cLoading) return ; + + const trendMap = new Map((trends ?? []).map((t) => [t.entity_id, t])); + const recMap = new Map((recs ?? []).map((r) => [r.ticker, r])); + const posMap = new Map((positions ?? []).map((p) => [p.ticker, p])); + + return ( +
+ {(companies ?? []).map((c) => { + const trend = trendMap.get(c.ticker); + const rec = recMap.get(c.ticker); + const pos = posMap.get(c.ticker); + return ( + +
+ {c.ticker} + {trend && } +
+
{c.legal_name}
+ {trend && ( +
+ Strength + +
+ )} + {rec && ( +
+ + +
+ )} + {pos && ( +
+ Position: + {pos.quantity} @ ${pos.avg_entry_price.toFixed(2)} + {pos.unrealized_pnl != null && ( + = 0 ? 'ml-2 text-green-400' : 'ml-2 text-red-400'}> + {pos.unrealized_pnl >= 0 ? '+' : ''}{pos.unrealized_pnl.toFixed(2)} + + )} +
+ )} +
+ ); + })} +
+ ); +} + +// --------------------------------------------------------------------------- +// Trino-backed dashboards +// --------------------------------------------------------------------------- + +function useTrinoQuery(sql: string, enabled = true) { + return useQuery({ + queryKey: ['trino-dashboard', sql], + queryFn: () => apiPost<{ columns: Array<{ name: string }>; rows: unknown[][] }>('query', '/api/analytics/query', { sql, limit: 5000 }), + enabled, + staleTime: 60_000, + }); +} + +function SentimentHeatmap({ hours }: { hours: number }) { + const days = Math.ceil(hours / 24); + const { data, isLoading } = useTrinoQuery( + `SELECT entity_id AS ticker, window, trend_direction, trend_strength, confidence, generated_at FROM trend_windows WHERE entity_type = 'company' AND generated_at >= current_timestamp - interval '${days}' day ORDER BY generated_at DESC LIMIT 500` + ); + + if (isLoading) return ; + if (!data?.rows.length) return

No sentiment data available

; + + const chartData = data.rows.map((r) => ({ + ticker: String(r[0]), + window: String(r[1]), + strength: Number(r[3]) || 0, + direction: String(r[2]), + })); + + return ( + +

Sentiment by Symbol

+ + + + + + + + + +
+ ); +} + +function PredictionAccuracy({ hours }: { hours: number }) { + const days = Math.ceil(hours / 24); + const { data, isLoading } = useTrinoQuery( + `SELECT predicted_confidence, realized_move_pct, ticker, prediction_date FROM prediction_vs_outcome WHERE prediction_date >= current_date - interval '${days}' day ORDER BY prediction_date DESC LIMIT 1000` + ); + + if (isLoading) return ; + if (!data?.rows.length) return

No prediction data available

; + + const chartData = data.rows.map((r) => ({ + confidence: Number(r[0]) || 0, + realized: Number(r[1]) || 0, + ticker: String(r[2]), + })); + + return ( + +

Predicted Confidence vs Realized Move

+ + + + + + + + + +
+ ); +} + +function PaperPnl({ hours }: { hours: number }) { + const days = Math.ceil(hours / 24); + const { data, isLoading } = useTrinoQuery( + `SELECT dt, daily_pnl, cumulative_pnl, win_count, loss_count FROM pnl_daily WHERE dt >= current_date - interval '${days}' day ORDER BY dt ASC LIMIT 365` + ); + + if (isLoading) return ; + if (!data?.rows.length) return

No PnL data available

; + + const chartData = data.rows.map((r) => ({ + date: String(r[0]), + daily: Number(r[1]) || 0, + cumulative: Number(r[2]) || 0, + wins: Number(r[3]) || 0, + losses: Number(r[4]) || 0, + })); + + const totalWins = chartData.reduce((s, d) => s + d.wins, 0); + const totalLosses = chartData.reduce((s, d) => s + d.losses, 0); + const winRate = totalWins + totalLosses > 0 ? ((totalWins / (totalWins + totalLosses)) * 100).toFixed(1) : '—'; + + return ( +
+
+ +
{winRate}%
+
Win Rate
+
+ +
{totalWins}
+
Wins
+
+ +
{totalLosses}
+
Losses
+
+
+ + +

Equity Curve

+ + + + + + + + + +
+ + +

Daily PnL

+ + + + + + + + + +
+
+ ); +} + +function ModelQuality({ hours }: { hours: number }) { + const days = Math.ceil(hours / 24); + const { data, isLoading } = useTrinoQuery( + `SELECT date_trunc('hour', recorded_at) AS hour, count(*) AS total, count(*) filter (where success = true) AS successes, avg(total_duration_ms) AS avg_latency, avg(retry_count) AS avg_retries FROM model_performance_metrics WHERE recorded_at >= current_timestamp - interval '${days}' day GROUP BY 1 ORDER BY 1 ASC LIMIT 500` + ); + + if (isLoading) return ; + if (!data?.rows.length) return

No model metrics available

; + + const chartData = data.rows.map((r) => ({ + hour: String(r[0]).slice(11, 16), + total: Number(r[1]) || 0, + successes: Number(r[2]) || 0, + rate: Number(r[1]) > 0 ? ((Number(r[2]) / Number(r[1])) * 100) : 0, + latency: Math.round(Number(r[3]) || 0), + retries: Number(r[4]) || 0, + })); + + return ( +
+ +

Success Rate Over Time

+ + + + + + + + + +
+ + +

Latency & Retries

+ + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/DocumentDetail.tsx b/frontend/src/pages/DocumentDetail.tsx new file mode 100644 index 0000000..99d29c8 --- /dev/null +++ b/frontend/src/pages/DocumentDetail.tsx @@ -0,0 +1,152 @@ +import { useParams } from '@tanstack/react-router'; +import { useDocument } from '../api/hooks'; +import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui'; + +export function DocumentDetailPage() { + const { id } = useParams({ from: '/documents/$id' }); + const { data: doc, isLoading } = useDocument(id); + + if (isLoading || !doc) return ; + + return ( +
+
+

{doc.title ?? 'Untitled Document'}

+
+ + {doc.document_type} + {doc.source_type} + {doc.publisher && {doc.publisher}} + {doc.published_at && {new Date(doc.published_at).toLocaleString()}} +
+
+ + {/* Metadata */} + +

Metadata

+
+
URL
{doc.url ? {doc.url} : '—'}
+
Language
{doc.language ?? '—'}
+
Content Hash
{doc.content_hash ?? '—'}
+
Parse Quality
{doc.parse_quality_score?.toFixed(2) ?? '—'}
+
Parse Confidence
+
Retrieved
{doc.retrieved_at ? new Date(doc.retrieved_at).toLocaleString() : '—'}
+
+
+ + {/* Company Mentions */} + {doc.company_mentions.length > 0 && ( + +

Company Mentions

+
+ {doc.company_mentions.map((m, i) => ( + + {m.ticker} {m.legal_name} + ({m.mention_type}, {(m.confidence * 100).toFixed(0)}%) + + ))} +
+
+ )} + + {/* Intelligence Extraction */} + {doc.intelligence ? ( + +

Intelligence Extraction

+
+
+
Summary
+

{doc.intelligence.summary ?? '—'}

+
+ +
+
+
Confidence
+ +
+
+
Validation
+ +
+
+
Model
+ {doc.intelligence.model_name ?? '—'} ({doc.intelligence.prompt_version}) +
+
+ + {doc.intelligence.macro_themes && doc.intelligence.macro_themes.length > 0 && ( +
+
Macro Themes
+
+ {doc.intelligence.macro_themes.map((t, i) => ( + {t} + ))} +
+
+ )} + + {doc.intelligence.extraction_warnings && doc.intelligence.extraction_warnings.length > 0 && ( +
+
Warnings
+
+ {doc.intelligence.extraction_warnings.map((w, i) => ( + {w} + ))} +
+
+ )} + + {/* Company Impacts */} + {doc.intelligence.company_impacts && doc.intelligence.company_impacts.length > 0 && ( +
+
Company Impacts
+
+ {doc.intelligence.company_impacts.map((imp, i) => ( +
+
+ {imp.ticker} + + + {imp.catalyst_type} + {imp.impact_horizon} +
+ {imp.key_facts && imp.key_facts.length > 0 && ( +
+
Key Facts
+
    + {imp.key_facts.map((f, j) =>
  • {f}
  • )} +
+
+ )} + {imp.risks && imp.risks.length > 0 && ( +
+
Risks
+
    + {imp.risks.map((r, j) =>
  • {r}
  • )} +
+
+ )} +
+ ))} +
+
+ )} +
+
+ ) : ( + +

No intelligence extraction available

+
+ )} + + {/* Storage References */} + +

Storage References

+
+
Raw:
{doc.raw_storage_ref ?? '—'}
+
Normalized:
{doc.normalized_storage_ref ?? '—'}
+
+
+
+ ); +} diff --git a/frontend/src/pages/Documents.tsx b/frontend/src/pages/Documents.tsx new file mode 100644 index 0000000..ebf5543 --- /dev/null +++ b/frontend/src/pages/Documents.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useDocuments } from '../api/hooks'; +import { DataTable, type Column } from '../components/DataTable'; +import { StatusBadge, LoadingSpinner, TickerFilter } from '../components/ui'; +import type { Document } from '../api/hooks'; + +export function DocumentsPage() { + const navigate = useNavigate(); + const [ticker, setTicker] = useState(''); + const { data, isLoading, error } = useDocuments({ ticker: ticker || undefined, limit: 100 }); + + const columns: Column[] = [ + { key: 'title', header: 'Title', render: (r) => {r.title ?? '—'} }, + { key: 'document_type', header: 'Type' }, + { key: 'source_type', header: 'Source' }, + { key: 'published_at', header: 'Published', render: (r) => {r.published_at ? new Date(r.published_at).toLocaleDateString() : '—'} }, + { key: 'parse_confidence', header: 'Parse Quality', render: (r) => }, + { key: 'status', header: 'Status', render: (r) => }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load documents
; + + return ( +
+
+

Documents

+ +
+ + data={data ?? []} + columns={columns} + keyField="id" + onRowClick={(row) => navigate({ to: '/documents/$id', params: { id: row.id } })} + filterFn={(row, q) => { + const lq = q.toLowerCase(); + return (row.title ?? '').toLowerCase().includes(lq) || row.document_type.toLowerCase().includes(lq); + }} + /> +
+ ); +} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..322f160 --- /dev/null +++ b/frontend/src/pages/Home.tsx @@ -0,0 +1,121 @@ +import { Link } from '@tanstack/react-router'; +import { usePipelineHealth, useIngestionSummary, useRecommendations, useCompanies, useCoverageGaps } from '../api/hooks'; +import { LoadingSpinner, Card } from '../components/ui'; +import { + Building2, FileText, TrendingUp, Lightbulb, ShoppingCart, + Wallet, ShieldCheck, Activity, Download, Cpu, Radar, + Terminal, LayoutDashboard, AlertTriangle, +} from 'lucide-react'; + +const quickNav = [ + { to: '/companies', label: 'Companies', icon: , color: 'text-blue-400' }, + { to: '/documents', label: 'Documents', icon: , color: 'text-green-400' }, + { to: '/trends', label: 'Trends', icon: , color: 'text-purple-400' }, + { to: '/recommendations', label: 'Recommendations', icon: , color: 'text-yellow-400' }, + { to: '/orders', label: 'Orders', icon: , color: 'text-orange-400' }, + { to: '/positions', label: 'Positions', icon: , color: 'text-cyan-400' }, + { to: '/trading', label: 'Trading', icon: , color: 'text-red-400' }, + { to: '/ops/pipeline', label: 'Pipeline', icon: , color: 'text-emerald-400' }, + { to: '/ops/ingestion', label: 'Ingestion', icon: , color: 'text-teal-400' }, + { to: '/ops/model', label: 'Model Perf', icon: , color: 'text-indigo-400' }, + { to: '/ops/coverage', label: 'Coverage', icon: , color: 'text-pink-400' }, + { to: '/analytics/query', label: 'SQL Explorer', icon: , color: 'text-amber-400' }, + { to: '/analytics/dashboards', label: 'Dashboards', icon: , color: 'text-violet-400' }, +]; + +export function HomePage() { + const { data: pipeline, isLoading: pLoading } = usePipelineHealth(24); + const { data: ingestion } = useIngestionSummary(24); + const { data: recs } = useRecommendations({ limit: 5 }); + const { data: companies } = useCompanies(); + const { data: gaps } = useCoverageGaps(); + + if (pLoading) return ; + + const ing = (ingestion ?? {}) as Record; + const staleCount = (gaps?.stale_sources?.length ?? 0) + (gaps?.missing_source_types?.length ?? 0); + + return ( +
+

Stonks Oracle

+ + {/* Alert banner */} + {staleCount > 0 && ( +
+ + + {staleCount} coverage issue{staleCount > 1 ? 's' : ''} detected — check{' '} + Source Coverage + +
+ )} + + {/* Key metrics */} +
+ + + + +
+ + {/* Pipeline status */} + {pipeline && ( + +

Pipeline Status (24h)

+
+ {((pipeline.document_stages ?? []) as Array<{ status: string; doc_count: number }>).map((s) => ( +
+
{s.doc_count}
+
{s.status}
+
+ ))} +
+
+ )} + + {/* Recent activity */} + {recs && recs.length > 0 && ( + +

Recent Recommendations

+
+ {recs.map((r) => ( + +
+ {r.ticker} + + {r.action} + + {r.thesis} +
+ {new Date(r.generated_at).toLocaleString()} + + ))} +
+
+ )} + + {/* Quick nav */} +
+ {quickNav.map((item) => ( + + {item.icon} + {item.label} + + ))} +
+
+ ); +} + +function MetricCard({ label, value }: { label: string; value: unknown }) { + return ( + +
{value != null ? String(value) : '—'}
+
{label}
+
+ ); +} diff --git a/frontend/src/pages/OpsCoverage.tsx b/frontend/src/pages/OpsCoverage.tsx new file mode 100644 index 0000000..17beda5 --- /dev/null +++ b/frontend/src/pages/OpsCoverage.tsx @@ -0,0 +1,104 @@ +import { useCoverageGaps, useSymbolCoverage } from '../api/hooks'; +import { LoadingSpinner, StatusBadge, Card } from '../components/ui'; + +export function OpsCoveragePage() { + const { data: gaps, isLoading: gapsLoading } = useCoverageGaps(); + const { data: coverage, isLoading: covLoading } = useSymbolCoverage(); + + if (gapsLoading || covLoading) return ; + + const missing = (gaps?.missing_source_types ?? []) as Array>; + const stale = (gaps?.stale_sources ?? []) as Array>; + const matrix = (coverage ?? []) as Array>; + + return ( +
+

Source Coverage

+ + {/* Coverage Matrix */} + +

Company × Source Type Matrix

+
+ + + + + + + + + + + + + + + {matrix.map((row, i) => ( + + + + + + + + + + + ))} + +
TickerNameMarketNewsFilingsWebBrokerTotal
{row.ticker as string}{row.legal_name as string}{String(row.active_sources)}
+
+
+ + {/* Missing Source Types */} + {missing.length > 0 && ( + +

Missing Source Types ({missing.length})

+
+ {missing.map((m, i) => { + const activeTypes = (m.active_types as string[]) ?? []; + const expected = (m.expected_types as string[]) ?? []; + const missingTypes = expected.filter((t) => !activeTypes.includes(t)); + return ( +
+ {m.ticker as string} + missing: + {missingTypes.map((t) => ( + + ))} +
+ ); + })} +
+
+ )} + + {/* Stale Sources */} + {stale.length > 0 && ( + +

Stale Sources ({stale.length})

+
+ {stale.map((s, i) => ( +
+
+ {s.ticker as string} + + {s.source_name as string} +
+
+ Last success: {s.last_success ? new Date(s.last_success as string).toLocaleString() : 'never'} + {s.recent_failures ? ` | ${s.recent_failures} failures (24h)` : ''} +
+
+ ))} +
+
+ )} +
+ ); +} + +function CoverageCell({ count }: { count: number }) { + const color = count > 0 ? 'text-green-400' : 'text-red-400'; + return {count}; +} diff --git a/frontend/src/pages/OpsIngestion.tsx b/frontend/src/pages/OpsIngestion.tsx new file mode 100644 index 0000000..2014d1a --- /dev/null +++ b/frontend/src/pages/OpsIngestion.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { useIngestionThroughput, useIngestionSummary } from '../api/hooks'; +import { LoadingSpinner, DateRangeSelector, Card } from '../components/ui'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; + +export function OpsIngestionPage() { + const [hours, setHours] = useState(24); + const [bucket, setBucket] = useState('1h'); + const { data: throughput, isLoading: tpLoading } = useIngestionThroughput(hours, bucket); + const { data: summary } = useIngestionSummary(hours); + + if (tpLoading) return ; + + const chartData = (throughput ?? []).map((row: unknown) => { + const r = row as Record; + return { + time: r.bucket_start ? new Date(r.bucket_start as string).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '', + completed: Number(r.completed ?? 0), + failed: Number(r.failed ?? 0), + items: Number(r.items_fetched ?? 0), + }; + }); + + const s = (summary ?? {}) as Record; + + return ( +
+
+

Ingestion Monitor

+
+ +
+ {['15m', '1h', '6h', '1d'].map((b) => ( + + ))} +
+
+
+ + {/* Summary stats */} +
+ + + + + +
+ + {/* Throughput chart */} + +

Throughput

+ + + + + + + + + + +
+ + {/* By source type */} + {s.by_source_type && ( + +

By Source Type

+
+ + + + + + + + + + + + {(s.by_source_type as Array>).map((row, i) => ( + + + + + + + + ))} + +
TypeRunsCompletedFailedItems
{row.source_type as string}{String(row.runs)}{String(row.completed)}{String(row.failed)}{String(row.items_fetched)}
+
+
+ )} +
+ ); +} + +function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: unknown; color?: string }) { + return ( + +
{value != null ? String(value) : '—'}
+
{label}
+
+ ); +} diff --git a/frontend/src/pages/OpsModel.tsx b/frontend/src/pages/OpsModel.tsx new file mode 100644 index 0000000..e00973f --- /dev/null +++ b/frontend/src/pages/OpsModel.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { useModelPerformance, useModelFailures } from '../api/hooks'; +import { LoadingSpinner, DateRangeSelector, StatusBadge, Card } from '../components/ui'; + +export function OpsModelPage() { + const [hours, setHours] = useState(24); + const { data: perf, isLoading } = useModelPerformance(hours); + const { data: failures } = useModelFailures(hours); + + if (isLoading) return ; + + const p = (perf ?? {}) as Record; + + return ( +
+
+

Model Performance

+ +
+ + {/* Key metrics */} +
+ + + + + +
+ + {/* Recent Failures */} + +

+ Recent Failures ({(failures as unknown[])?.length ?? 0}) +

+ {!(failures as unknown[])?.length ? ( +

No recent failures

+ ) : ( +
+ {(failures as Array>).map((f, i) => ( +
+
+
+ {f.ticker as string} + + {f.model_name as string} +
+ {f.recorded_at ? new Date(f.recorded_at as string).toLocaleString() : ''} +
+
+ {f.document_title as string} ({f.document_type as string}) +
+ {f.validation_errors && ( +
+ {JSON.stringify(f.validation_errors)} +
+ )} +
+ ))} +
+ )} +
+
+ ); +} + +function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: unknown; color?: string }) { + return ( + +
{value != null ? String(value) : '—'}
+
{label}
+
+ ); +} diff --git a/frontend/src/pages/OpsPipeline.tsx b/frontend/src/pages/OpsPipeline.tsx new file mode 100644 index 0000000..0a77a4d --- /dev/null +++ b/frontend/src/pages/OpsPipeline.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react'; +import { usePipelineHealth } from '../api/hooks'; +import { LoadingSpinner, DateRangeSelector, Card } from '../components/ui'; + +export function OpsPipelinePage() { + const [hours, setHours] = useState(24); + const { data, isLoading } = usePipelineHealth(hours); + + if (isLoading) return ; + + const stages = (data?.document_stages as Array<{ status: string; doc_count: number }>) ?? []; + const parsing = (data?.parsing ?? {}) as Record; + const extraction = (data?.extraction ?? {}) as Record; + const aggregation = (data?.aggregation ?? {}) as Record; + + return ( +
+
+

Pipeline Health

+ +
+ + {/* Document Stage Counts */} + +

Document Stages

+
+ {stages.map((s) => ( +
+
{s.doc_count}
+
{s.status}
+
+ ))} +
+
+ + {/* Parsing Quality */} + +

Parsing Quality

+
+ + + + + +
+
+ + {/* Extraction Stats */} + +

Extraction Validation

+
+ + + + + +
+
+ + {/* Aggregation */} + +

Trend Generation

+
+ + + + +
+
+
+ ); +} + +function Stat({ label, value, color = 'text-gray-100' }: { label: string; value: unknown; color?: string }) { + return ( +
+
{value != null ? String(value) : '—'}
+
{label}
+
+ ); +} diff --git a/frontend/src/pages/OrderDetail.tsx b/frontend/src/pages/OrderDetail.tsx new file mode 100644 index 0000000..3e22824 --- /dev/null +++ b/frontend/src/pages/OrderDetail.tsx @@ -0,0 +1,81 @@ +import { useParams } from '@tanstack/react-router'; +import { useOrder } from '../api/hooks'; +import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; + +export function OrderDetailPage() { + const { id } = useParams({ from: '/orders/$id' }); + const { data: order, isLoading } = useOrder(id); + + if (isLoading || !order) return ; + + return ( +
+
+

{order.ticker}

+ + +
+ + +
+
Type
{order.order_type}
+
Quantity
{order.quantity}
+
Limit Price
{order.limit_price != null ? `$${order.limit_price.toFixed(2)}` : '—'}
+
Fill Price
{order.fill_price != null ? `$${order.fill_price.toFixed(2)}` : '—'}
+
Fill Qty
{order.fill_quantity ?? '—'}
+
Broker Order
{order.broker_order_id ?? '—'}
+
Idempotency Key
{order.idempotency_key ?? '—'}
+
Created
{new Date(order.created_at).toLocaleString()}
+
+
+ + {/* Decision Trace */} + {order.decision_trace && Object.keys(order.decision_trace).length > 0 && ( + +

Decision Trace

+
+            {JSON.stringify(order.decision_trace, null, 2)}
+          
+
+ )} + + {/* Order Events Timeline */} + +

Events ({order.events.length})

+ {order.events.length === 0 ? ( +

No events

+ ) : ( +
+ {order.events.map((ev) => ( +
+ +
+
{new Date(ev.created_at).toLocaleString()}
+ {ev.data && ( +
{JSON.stringify(ev.data, null, 2)}
+ )} +
+
+ ))} +
+ )} +
+ + {/* Audit Trail */} + {order.audit_trail && (order.audit_trail as unknown[]).length > 0 && ( + +

Audit Trail

+
+ {(order.audit_trail as Array>).map((entry, i) => ( +
+ {entry.created_at ? new Date(entry.created_at as string).toLocaleString() : ''} + {entry.event_type as string} + {entry.description as string} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx new file mode 100644 index 0000000..66e5cb3 --- /dev/null +++ b/frontend/src/pages/Orders.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useOrders } from '../api/hooks'; +import { DataTable, type Column } from '../components/DataTable'; +import { StatusBadge, LoadingSpinner, TickerFilter } from '../components/ui'; +import type { Order } from '../api/hooks'; + +export function OrdersPage() { + const navigate = useNavigate(); + const [ticker, setTicker] = useState(''); + const { data, isLoading } = useOrders({ ticker: ticker || undefined, limit: 100 }); + + const columns: Column[] = [ + { key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' }, + { key: 'side', header: 'Side', render: (r) => }, + { key: 'order_type', header: 'Type' }, + { key: 'quantity', header: 'Qty' }, + { key: 'status', header: 'Status', render: (r) => }, + { key: 'fill_price', header: 'Fill Price', render: (r) => {r.fill_price != null ? `$${r.fill_price.toFixed(2)}` : '—'} }, + { key: 'created_at', header: 'Created', render: (r) => {new Date(r.created_at).toLocaleString()} }, + ]; + + if (isLoading) return ; + + return ( +
+
+

Orders

+ +
+ + data={data ?? []} + columns={columns} + keyField="id" + onRowClick={(row) => navigate({ to: '/orders/$id', params: { id: row.id } })} + /> +
+ ); +} diff --git a/frontend/src/pages/Placeholder.tsx b/frontend/src/pages/Placeholder.tsx new file mode 100644 index 0000000..3fee7f3 --- /dev/null +++ b/frontend/src/pages/Placeholder.tsx @@ -0,0 +1,10 @@ +export function PlaceholderPage({ title }: { title: string }) { + return ( +
+
+

{title}

+

Coming soon

+
+
+ ); +} diff --git a/frontend/src/pages/Positions.tsx b/frontend/src/pages/Positions.tsx new file mode 100644 index 0000000..c1102c6 --- /dev/null +++ b/frontend/src/pages/Positions.tsx @@ -0,0 +1,41 @@ +import { usePositions } from '../api/hooks'; +import { DataTable, type Column } from '../components/DataTable'; +import { LoadingSpinner } from '../components/ui'; +import type { Position } from '../api/hooks'; + +function fmtUsd(v: number | null | undefined) { + if (v == null) return '—'; + return `$${v.toFixed(2)}`; +} + +function pnlColor(v: number | null | undefined) { + if (v == null) return 'text-gray-400'; + return v >= 0 ? 'text-green-400' : 'text-red-400'; +} + +const columns: Column[] = [ + { key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' }, + { key: 'quantity', header: 'Qty' }, + { key: 'avg_entry_price', header: 'Entry', render: (r) => {fmtUsd(r.avg_entry_price)} }, + { key: 'current_price', header: 'Current', render: (r) => {fmtUsd(r.current_price)} }, + { key: 'unrealized_pnl', header: 'Unrealized P&L', render: (r) => {fmtUsd(r.unrealized_pnl)} }, + { key: 'realized_pnl', header: 'Realized P&L', render: (r) => {fmtUsd(r.realized_pnl)} }, + { key: 'updated_at', header: 'Updated', render: (r) => {new Date(r.updated_at).toLocaleString()} }, +]; + +export function PositionsPage() { + const { data, isLoading } = usePositions(); + + if (isLoading) return ; + + return ( +
+

Positions

+ + data={data ?? []} + columns={columns} + keyField="id" + /> +
+ ); +} diff --git a/frontend/src/pages/RecommendationDetail.tsx b/frontend/src/pages/RecommendationDetail.tsx new file mode 100644 index 0000000..64c9478 --- /dev/null +++ b/frontend/src/pages/RecommendationDetail.tsx @@ -0,0 +1,92 @@ +import { useParams } from '@tanstack/react-router'; +import { useRecommendation } from '../api/hooks'; +import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui'; + +export function RecommendationDetailPage() { + const { id } = useParams({ from: '/recommendations/$id' }); + const { data: rec, isLoading } = useRecommendation(id); + + if (isLoading || !rec) return ; + + return ( +
+
+

{rec.ticker}

+ + +
+ + +
+
Confidence
+
Horizon
{rec.time_horizon}
+
Risk
+
Generated
{new Date(rec.generated_at).toLocaleString()}
+
Portfolio %
{rec.portfolio_pct != null ? `${(rec.portfolio_pct * 100).toFixed(1)}%` : '—'}
+
Max Loss %
{rec.max_loss_pct != null ? `${(rec.max_loss_pct * 100).toFixed(2)}%` : '—'}
+
Model
{rec.model_version ?? '—'}
+
+
+ + {rec.thesis && ( + +

Thesis

+

{rec.thesis}

+
+ )} + + {rec.invalidation_conditions && rec.invalidation_conditions.length > 0 && ( + +

Invalidation Conditions

+
    + {rec.invalidation_conditions.map((c, i) =>
  • {c}
  • )} +
+
+ )} + + {/* Risk Evaluation */} + {rec.risk_evaluation && ( + +

Risk Evaluation

+
+ + Allowed mode: {rec.risk_evaluation.allowed_mode} +
+ {rec.risk_evaluation.rejection_reasons && rec.risk_evaluation.rejection_reasons.length > 0 && ( +
    + {rec.risk_evaluation.rejection_reasons.map((r, i) =>
  • {r}
  • )} +
+ )} +
+ )} + + {/* Evidence */} + +

Evidence ({rec.evidence.length})

+ {rec.evidence.length === 0 ? ( +

No evidence linked

+ ) : ( +
+ {rec.evidence.map((ev) => ( +
+
+
+ + {ev.title ?? 'Untitled'} +
+ weight: {ev.weight.toFixed(3)} +
+
+ {ev.document_type} + {ev.source_type} + {ev.publisher && {ev.publisher}} + {ev.published_at && {new Date(ev.published_at).toLocaleDateString()}} +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/Recommendations.tsx b/frontend/src/pages/Recommendations.tsx new file mode 100644 index 0000000..01247f7 --- /dev/null +++ b/frontend/src/pages/Recommendations.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useRecommendations } from '../api/hooks'; +import { DataTable, type Column } from '../components/DataTable'; +import { StatusBadge, ConfidenceBar, LoadingSpinner, TickerFilter } from '../components/ui'; +import type { Recommendation } from '../api/hooks'; + +export function RecommendationsPage() { + const navigate = useNavigate(); + const [ticker, setTicker] = useState(''); + const { data, isLoading } = useRecommendations({ ticker: ticker || undefined, limit: 100 }); + + const columns: Column[] = [ + { key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' }, + { key: 'action', header: 'Action', render: (r) => }, + { key: 'mode', header: 'Mode', render: (r) => }, + { key: 'confidence', header: 'Confidence', render: (r) => }, + { key: 'thesis', header: 'Thesis', render: (r) => {r.thesis ?? '—'} }, + { key: 'generated_at', header: 'Generated', render: (r) => {new Date(r.generated_at).toLocaleString()} }, + ]; + + if (isLoading) return ; + + return ( +
+
+

Recommendations

+ +
+ + data={data ?? []} + columns={columns} + keyField="id" + onRowClick={(row) => navigate({ to: '/recommendations/$id', params: { id: row.id } })} + /> +
+ ); +} diff --git a/frontend/src/pages/SqlExplorer.tsx b/frontend/src/pages/SqlExplorer.tsx new file mode 100644 index 0000000..98bcd35 --- /dev/null +++ b/frontend/src/pages/SqlExplorer.tsx @@ -0,0 +1,257 @@ +import { useState, useCallback } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import Editor from '@monaco-editor/react'; +import { apiGet, apiPost, apiDelete } from '../api/client'; +import { LoadingSpinner, Card } from '../components/ui'; +import { + BarChart, Bar, LineChart, Line, ScatterChart, Scatter, + XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, +} from 'recharts'; + +interface QueryResult { + columns: Array<{ name: string; type: string }>; + rows: unknown[][]; + row_count: number; + elapsed_ms: number; +} + +interface SavedQuery { + id: string; + name: string; + description: string; + sql_text: string; + created_at: string; +} + +interface SchemaInfo { + catalog: string; + schema: string; + tables: Array<{ name: string; columns: Array<{ name: string; type: string }> }>; +} + +type ChartType = 'none' | 'bar' | 'line' | 'scatter'; + +export function SqlExplorerPage() { + const qc = useQueryClient(); + const [sql, setSql] = useState('SELECT 1 AS test'); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [chartType, setChartType] = useState('none'); + const [xCol, setXCol] = useState(0); + const [yCol, setYCol] = useState(1); + + const { data: schema } = useQuery({ + queryKey: ['trino-schema'], + queryFn: () => apiGet('query', '/api/analytics/schema'), + }); + + const { data: savedQueries } = useQuery({ + queryKey: ['saved-queries'], + queryFn: () => apiGet('query', '/api/analytics/saved-queries'), + }); + + const executeMutation = useMutation({ + mutationFn: (sqlText: string) => apiPost('query', '/api/analytics/query', { sql: sqlText, limit: 1000 }), + onSuccess: (data) => { setResult(data); setError(null); }, + onError: (err: Error) => { setError(err.message); setResult(null); }, + }); + + const saveMutation = useMutation({ + mutationFn: (body: { name: string; sql_text: string }) => apiPost('query', '/api/analytics/saved-queries', body), + onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-queries'] }), + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => apiDelete('query', `/api/analytics/saved-queries/${id}`), + onSuccess: () => qc.invalidateQueries({ queryKey: ['saved-queries'] }), + }); + + const handleExecute = useCallback(() => { + if (sql.trim()) executeMutation.mutate(sql); + }, [sql, executeMutation]); + + const handleSave = useCallback(() => { + const name = prompt('Query name:'); + if (name) saveMutation.mutate({ name, sql_text: sql }); + }, [sql, saveMutation]); + + // Build chart data from result + const chartData = result && result.columns.length >= 2 + ? result.rows.map((row) => ({ + x: row[xCol], + y: Number(row[yCol]) || 0, + label: String(row[xCol]), + })) + : []; + + return ( +
+ {/* Schema browser sidebar */} +
+

Schema

+ {schema?.tables.map((t) => ( +
+ +
+ {t.columns.map((c) => ( +
+ {c.name} + {c.type} +
+ ))} +
+
+ ))} + + {/* Saved queries */} +

Saved Queries

+ {(savedQueries ?? []).map((sq) => ( +
+ + +
+ ))} +
+ + {/* Main area */} +
+ {/* Editor */} +
+ setSql(v ?? '')} + theme="vs-dark" + options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: 'on', scrollBeyondLastLine: false }} + /> +
+ + {/* Controls */} +
+ + + {result && ( + + {result.row_count} rows in {result.elapsed_ms}ms + + )} + {error && {error}} +
+ + {/* Results */} + {executeMutation.isPending && } + + {result && ( +
+ + + + {result.columns.map((col, i) => ( + + ))} + + + + {result.rows.map((row, ri) => ( + + {row.map((cell, ci) => ( + + ))} + + ))} + +
+ {col.name} ({col.type}) +
+ {cell == null ? NULL : String(cell)} +
+
+ )} + + {/* Chart builder */} + {result && result.columns.length >= 2 && ( + +
+ Chart: + {(['none', 'bar', 'line', 'scatter'] as ChartType[]).map((ct) => ( + + ))} + {chartType !== 'none' && ( + <> + + + + + )} +
+ {chartType !== 'none' && chartData.length > 0 && ( + + {chartType === 'bar' ? ( + + + + + + + + ) : chartType === 'line' ? ( + + + + + + + + ) : ( + + + + + + + + )} + + )} +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/Trading.tsx b/frontend/src/pages/Trading.tsx new file mode 100644 index 0000000..2ccc34c --- /dev/null +++ b/frontend/src/pages/Trading.tsx @@ -0,0 +1,160 @@ +import { useState } from 'react'; +import { + useTradingConfig, + useSetTradingMode, + usePendingApprovals, + useReviewApproval, + useActiveLockouts, +} from '../api/hooks'; +import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; + +export function TradingPage() { + const { data: config, isLoading: configLoading } = useTradingConfig(); + const { data: approvals } = usePendingApprovals(); + const { data: lockouts } = useActiveLockouts(); + const setMode = useSetTradingMode(); + const reviewApproval = useReviewApproval(); + const [confirmMode, setConfirmMode] = useState(null); + + if (configLoading) return ; + + const currentMode = config?.trading_mode ?? 'paper'; + + return ( +
+

Trading Controls

+ + {/* Trading Mode */} + +

Trading Mode

+
+ {['paper', 'live', 'disabled'].map((mode) => ( + + ))} +
+ + {/* Confirmation dialog for live mode */} + {confirmMode && ( +
+

+ Are you sure you want to switch to {confirmMode} mode? + This enables real order execution. +

+
+ + +
+
+ )} +
+ + {/* Pending Approvals */} + +

+ Pending Approvals ({approvals?.length ?? 0}) +

+ {!approvals?.length ? ( +

No pending approvals

+ ) : ( +
+ {approvals.map((a) => ( + reviewApproval.mutate({ id: a.id, approved, review_note: note })} /> + ))} +
+ )} +
+ + {/* Active Lockouts */} + +

+ Active Lockouts ({lockouts?.length ?? 0}) +

+ {!lockouts?.length ? ( +

No active lockouts

+ ) : ( +
+ {lockouts.map((l) => { + const expiresIn = l.expires_at ? Math.max(0, Math.round((new Date(l.expires_at).getTime() - Date.now()) / 60000)) : 0; + return ( +
+
+ {l.ticker} + + {l.reason} +
+ + {expiresIn > 0 ? `${expiresIn}m remaining` : 'expired'} + +
+ ); + })} +
+ )} +
+
+ ); +} + +function ApprovalRow({ approval, onReview }: { + approval: { id: string; ticker: string; side: string; quantity: number; estimated_value: number | null; requested_at: string }; + onReview: (approved: boolean, note: string) => void; +}) { + const [note, setNote] = useState(''); + + return ( +
+
+
+ {approval.ticker} + + qty: {approval.quantity} + {approval.estimated_value != null && ( + ${approval.estimated_value.toFixed(2)} + )} +
+ {new Date(approval.requested_at).toLocaleString()} +
+
+ setNote(e.target.value)} + className="flex-1 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-xs text-gray-200 placeholder-gray-500" + aria-label="Review note" + /> + + +
+
+ ); +} diff --git a/frontend/src/pages/TrendDetail.tsx b/frontend/src/pages/TrendDetail.tsx new file mode 100644 index 0000000..71fff76 --- /dev/null +++ b/frontend/src/pages/TrendDetail.tsx @@ -0,0 +1,106 @@ +import { useParams } from '@tanstack/react-router'; +import { useTrend, useTrendEvidence } from '../api/hooks'; +import { TrendArrow, ConfidenceBar, StatusBadge, LoadingSpinner, Card } from '../components/ui'; + +export function TrendDetailPage() { + const { id } = useParams({ from: '/trends/$id' }); + const { data: trend, isLoading } = useTrend(id); + const { data: evidenceData } = useTrendEvidence(id); + + if (isLoading || !trend) return ; + + const evidence = (evidenceData?.evidence ?? []) as Array>; + + return ( +
+
+

{trend.entity_id}

+ + {trend.window} +
+ + +
+
+
Direction
+
+ {trend.trend_direction} +
+
+
+
Strength
+
+
+
+
Confidence
+
+
+
+
Contradiction
+
0.5 ? 'text-yellow-400' : 'text-gray-300'}`}> + {(trend.contradiction_score * 100).toFixed(0)}% +
+
+
+
Generated
+
{new Date(trend.generated_at).toLocaleString()}
+
+
+
+ + {trend.dominant_catalysts && trend.dominant_catalysts.length > 0 && ( + +

Dominant Catalysts

+
+ {trend.dominant_catalysts.map((c, i) => ( + {c} + ))} +
+
+ )} + + {trend.material_risks && trend.material_risks.length > 0 && ( + +

Material Risks

+
    + {trend.material_risks.map((r, i) =>
  • {r}
  • )} +
+
+ )} + + {/* Evidence drill-down */} + +

Contributing Evidence ({evidence.length})

+ {evidence.length === 0 ? ( +

No evidence records

+ ) : ( +
+ {evidence.map((ev, i) => ( +
+
+
+ + {(ev.title as string) ?? 'Untitled'} +
+ rank: {((ev.rank_score as number) ?? 0).toFixed(3)} +
+
+ {ev.document_type as string} + {ev.source_type as string} + {ev.publisher && {ev.publisher as string}} + {ev.published_at && {new Date(ev.published_at as string).toLocaleDateString()}} +
+ {ev.intelligence && ( +
+ Summary: + {((ev.intelligence as Record).summary as string) ?? '—'} +
+ )} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/Trends.tsx b/frontend/src/pages/Trends.tsx new file mode 100644 index 0000000..178ae76 --- /dev/null +++ b/frontend/src/pages/Trends.tsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useTrends } from '../api/hooks'; +import { TrendArrow, ConfidenceBar, LoadingSpinner, TickerFilter, Card } from '../components/ui'; +import type { TrendSummary } from '../api/hooks'; + +const WINDOWS = ['intraday', '1d', '7d', '30d', '90d']; + +export function TrendsPage() { + const navigate = useNavigate(); + const [ticker, setTicker] = useState(''); + const [window, setWindow] = useState(undefined); + const { data, isLoading } = useTrends({ ticker: ticker || undefined, window, limit: 100 }); + + if (isLoading) return ; + + return ( +
+
+

Trends

+
+ +
+ + {WINDOWS.map((w) => ( + + ))} +
+
+
+ +
+ {(data ?? []).map((trend) => ( + navigate({ to: '/trends/$id', params: { id: trend.id } })} /> + ))} + {data?.length === 0 &&

No trends found

} +
+
+ ); +} + +function TrendCard({ trend, onClick }: { trend: TrendSummary; onClick: () => void }) { + const [expanded, setExpanded] = useState(false); + + return ( + +
+
+
+ {trend.entity_id} + +
+ {trend.window} +
+ +
+
+ Strength +
+
+
+
+
+ Confidence + +
+
+ Contradiction + 0.5 ? 'text-yellow-400' : 'text-gray-400'}`}> + {(trend.contradiction_score * 100).toFixed(0)}% + +
+
+ + {trend.dominant_catalysts && trend.dominant_catalysts.length > 0 && ( +
+ {trend.dominant_catalysts.map((c, i) => ( + {c} + ))} +
+ )} +
+ + {/* Expandable evidence preview */} + {(trend.top_supporting_evidence?.length || trend.top_opposing_evidence?.length) && ( + + )} + {expanded && ( +
+ {trend.top_supporting_evidence?.map((e, i) => ( +
+ {e}
+ ))} + {trend.top_opposing_evidence?.map((e, i) => ( +
− {e}
+ ))} +
+ )} + + ); +} diff --git a/frontend/src/pages/Watchlists.tsx b/frontend/src/pages/Watchlists.tsx new file mode 100644 index 0000000..9f01e91 --- /dev/null +++ b/frontend/src/pages/Watchlists.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useWatchlists, useWatchlistMembers, useCreateWatchlist } from '../api/hooks'; +import { LoadingSpinner, Card } from '../components/ui'; + +export function WatchlistsPage() { + const { data: watchlists, isLoading } = useWatchlists(); + const [selected, setSelected] = useState(null); + const [showCreate, setShowCreate] = useState(false); + + if (isLoading) return ; + + return ( +
+
+

Watchlists

+ +
+ + {showCreate && setShowCreate(false)} />} + +
+ {(watchlists ?? []).map((wl) => ( + + + + ))} +
+ + {selected && } +
+ ); +} + +function WatchlistMembers({ watchlistId }: { watchlistId: string }) { + const { data: members, isLoading } = useWatchlistMembers(watchlistId); + if (isLoading) return ; + if (!members?.length) return

No members in this watchlist

; + + return ( + +

Members

+
+ {members.map((m) => ( + + {m.ticker} — {m.legal_name} + + ))} +
+
+ ); +} + +function CreateWatchlistForm({ onClose }: { onClose: () => void }) { + const [name, setName] = useState(''); + const [desc, setDesc] = useState(''); + const mutation = useCreateWatchlist(); + + return ( + +
{ + e.preventDefault(); + if (name.trim()) mutation.mutate({ name: name.trim(), description: desc || undefined }, { onSuccess: onClose }); + }} + > + setName(e.target.value)} required className="rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="Watchlist name" /> + setDesc(e.target.value)} className="flex-1 rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="Watchlist description" /> + + +
+
+ ); +} diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx new file mode 100644 index 0000000..0909e3b --- /dev/null +++ b/frontend/src/routes.tsx @@ -0,0 +1,170 @@ +import { + createRouter, + createRootRoute, + createRoute, + Outlet, +} from '@tanstack/react-router'; +import { AppLayout } from './components/AppLayout'; + +import { CompaniesPage } from './pages/Companies'; +import { CompanyDetailPage } from './pages/CompanyDetail'; +import { WatchlistsPage } from './pages/Watchlists'; +import { DocumentsPage } from './pages/Documents'; +import { DocumentDetailPage } from './pages/DocumentDetail'; +import { TrendsPage } from './pages/Trends'; +import { TrendDetailPage } from './pages/TrendDetail'; +import { RecommendationsPage } from './pages/Recommendations'; +import { RecommendationDetailPage } from './pages/RecommendationDetail'; +import { OrdersPage } from './pages/Orders'; +import { OrderDetailPage } from './pages/OrderDetail'; +import { PositionsPage } from './pages/Positions'; +import { TradingPage } from './pages/Trading'; +import { OpsPipelinePage } from './pages/OpsPipeline'; +import { OpsIngestionPage } from './pages/OpsIngestion'; +import { OpsModelPage } from './pages/OpsModel'; +import { OpsCoveragePage } from './pages/OpsCoverage'; +import { SqlExplorerPage } from './pages/SqlExplorer'; +import { DashboardsPage } from './pages/Dashboards'; +import { HomePage } from './pages/Home'; + +// Root route wraps everything in the app shell layout +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: HomePage, +}); + +const companiesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/companies', + component: CompaniesPage, +}); +const companyDetailRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/companies/$id', + component: CompanyDetailPage, +}); +const watchlistsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/watchlists', + component: WatchlistsPage, +}); +const documentsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/documents', + component: DocumentsPage, +}); +const documentDetailRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/documents/$id', + component: DocumentDetailPage, +}); +const trendsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/trends', + component: TrendsPage, +}); +const trendDetailRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/trends/$id', + component: TrendDetailPage, +}); +const recommendationsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/recommendations', + component: RecommendationsPage, +}); +const recommendationDetailRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/recommendations/$id', + component: RecommendationDetailPage, +}); +const ordersRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/orders', + component: OrdersPage, +}); +const orderDetailRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/orders/$id', + component: OrderDetailPage, +}); +const positionsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/positions', + component: PositionsPage, +}); +const tradingRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/trading', + component: TradingPage, +}); +const opsPipelineRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/ops/pipeline', + component: OpsPipelinePage, +}); +const opsIngestionRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/ops/ingestion', + component: OpsIngestionPage, +}); +const opsModelRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/ops/model', + component: OpsModelPage, +}); +const opsCoverageRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/ops/coverage', + component: OpsCoveragePage, +}); +const analyticsQueryRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/analytics/query', + component: SqlExplorerPage, +}); +const analyticsDashboardsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/analytics/dashboards', + component: DashboardsPage, +}); + +const routeTree = rootRoute.addChildren([ + indexRoute, + companiesRoute, + companyDetailRoute, + watchlistsRoute, + documentsRoute, + documentDetailRoute, + trendsRoute, + trendDetailRoute, + recommendationsRoute, + recommendationDetailRoute, + ordersRoute, + orderDetailRoute, + positionsRoute, + tradingRoute, + opsPipelineRoute, + opsIngestionRoute, + opsModelRoute, + opsCoverageRoute, + analyticsQueryRoute, + analyticsDashboardsRoute, +]); + +export const router = createRouter({ routeTree }); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..50edfa9 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..3e19c55 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/infra/helm/stonks-oracle/templates/ingress.yaml b/infra/helm/stonks-oracle/templates/ingress.yaml index 2d5cfb7..bfdd503 100644 --- a/infra/helm/stonks-oracle/templates/ingress.yaml +++ b/infra/helm/stonks-oracle/templates/ingress.yaml @@ -99,4 +99,29 @@ spec: name: trino port: number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: stonks-dashboard-https + namespace: {{ .Release.Namespace }} + annotations: + cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer }} +spec: + ingressClassName: {{ .Values.ingress.className }} + tls: + - hosts: + - {{ .Values.ingress.hosts.dashboard }} + secretName: stonks-dashboard-tls + rules: + - host: {{ .Values.ingress.hosts.dashboard }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dashboard + port: + number: 80 {{- end }} diff --git a/infra/helm/stonks-oracle/values.yaml b/infra/helm/stonks-oracle/values.yaml index 95e481e..0b75990 100644 --- a/infra/helm/stonks-oracle/values.yaml +++ b/infra/helm/stonks-oracle/values.yaml @@ -135,6 +135,18 @@ services: probes: readiness: { path: /docs, port: 8000, initialDelay: 5, period: 10 } + dashboard: + replicas: 1 + image: dashboard + tier: frontend + port: 80 + resources: + requests: { cpu: 50m, memory: 64Mi } + limits: { cpu: 200m, memory: 128Mi } + probes: + readiness: { path: /, port: 80, initialDelay: 3, period: 10 } + liveness: { path: /, port: 80, initialDelay: 5, period: 30 } + ## ConfigMap data config: POSTGRES_HOST: "postgresql-rw.postgresql-service.svc.cluster.local" @@ -208,6 +220,7 @@ ingress: hosts: queryApi: stonks-api.celestium.life symbolRegistry: stonks-registry.celestium.life + dashboard: stonks.celestium.life superset: stonks-dash.celestium.life trino: stonks-trino.celestium.life diff --git a/infra/migrations/015_saved_queries.sql b/infra/migrations/015_saved_queries.sql new file mode 100644 index 0000000..d978bb6 --- /dev/null +++ b/infra/migrations/015_saved_queries.sql @@ -0,0 +1,12 @@ +-- Saved SQL queries for the analytics explorer +CREATE TABLE IF NOT EXISTS saved_queries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + description TEXT, + sql_text TEXT NOT NULL, + created_by TEXT NOT NULL DEFAULT 'operator', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_saved_queries_name ON saved_queries (name); diff --git a/services/api/app.py b/services/api/app.py index b3f066b..5677503 100644 --- a/services/api/app.py +++ b/services/api/app.py @@ -20,6 +20,7 @@ from typing import Any, Optional import asyncpg from fastapi import FastAPI, HTTPException, Query, Request +from pydantic import BaseModel from prometheus_client import CONTENT_TYPE_LATEST, generate_latest from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response @@ -1505,3 +1506,185 @@ async def get_source_coverage_gaps(): "missing_source_types": [_row_to_dict(r) for r in missing_types], "stale_sources": [_row_to_dict(r) for r in stale_sources], } + + +# --------------------------------------------------------------------------- +# Analytics: Trino SQL Proxy (Requirement 10.1, 10.3, 13.7) +# --------------------------------------------------------------------------- + +import time as _time +import httpx + + +@app.post("/api/analytics/query") +async def analytics_query(body: dict[str, Any]): + """Proxy SQL to Trino, enforce row limits, return structured results. + + Design: Section 9.3 (API proxy for Trino) + Requirements: 10.1, 10.3, 13.7 + """ + sql = body.get("sql", "").strip() + if not sql: + raise HTTPException(400, "sql is required") + + limit = min(int(body.get("limit", 1000)), 10000) + + trino_host = config.trino.host + trino_port = config.trino.port + trino_catalog = config.trino.catalog + trino_schema = config.trino.schema + + trino_url = f"http://{trino_host}:{trino_port}/v1/statement" + headers = { + "X-Trino-User": "stonks-dashboard", + "X-Trino-Catalog": trino_catalog, + "X-Trino-Schema": trino_schema, + } + + start = _time.monotonic() + try: + async with httpx.AsyncClient(timeout=60.0) as client: + # Submit query + resp = await client.post(trino_url, content=sql, headers=headers) + if resp.status_code != 200: + raise HTTPException(502, f"Trino error: {resp.text[:500]}") + + result = resp.json() + columns: list[dict[str, str]] = [] + all_rows: list[list[Any]] = [] + + # Extract columns from first response + if "columns" in result: + columns = [{"name": c["name"], "type": c.get("type", "unknown")} for c in result["columns"]] + + if "data" in result: + all_rows.extend(result["data"]) + + # Follow nextUri to get all results + while "nextUri" in result and len(all_rows) < limit: + next_url = result["nextUri"] + resp = await client.get(next_url, headers=headers) + if resp.status_code != 200: + break + result = resp.json() + if "columns" in result and not columns: + columns = [{"name": c["name"], "type": c.get("type", "unknown")} for c in result["columns"]] + if "data" in result: + all_rows.extend(result["data"]) + + elapsed_ms = round((_time.monotonic() - start) * 1000) + all_rows = all_rows[:limit] + + return { + "columns": columns, + "rows": all_rows, + "row_count": len(all_rows), + "elapsed_ms": elapsed_ms, + } + + except httpx.ConnectError: + raise HTTPException(502, "Cannot connect to Trino") + except httpx.TimeoutException: + raise HTTPException(504, "Trino query timed out") + + +@app.get("/api/analytics/schema") +async def analytics_schema(): + """Return Trino catalog/schema/table/column metadata for the schema browser. + + Requirements: 13.7 + """ + trino_host = config.trino.host + trino_port = config.trino.port + trino_catalog = config.trino.catalog + trino_schema = config.trino.schema + + trino_url = f"http://{trino_host}:{trino_port}/v1/statement" + headers = { + "X-Trino-User": "stonks-dashboard", + "X-Trino-Catalog": trino_catalog, + "X-Trino-Schema": trino_schema, + } + + async def _run_trino_query(sql: str) -> list[list[Any]]: + rows: list[list[Any]] = [] + async with httpx.AsyncClient(timeout=30.0) as client: + resp = await client.post(trino_url, content=sql, headers=headers) + if resp.status_code != 200: + return rows + result = resp.json() + if "data" in result: + rows.extend(result["data"]) + while "nextUri" in result: + resp = await client.get(result["nextUri"], headers=headers) + if resp.status_code != 200: + break + result = resp.json() + if "data" in result: + rows.extend(result["data"]) + return rows + + try: + # Get tables + table_rows = await _run_trino_query( + f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{trino_schema}' ORDER BY table_name" + ) + tables = [] + for tr in table_rows: + table_name = tr[0] if tr else None + if not table_name: + continue + # Get columns for each table + col_rows = await _run_trino_query( + f"SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = '{trino_schema}' AND table_name = '{table_name}' ORDER BY ordinal_position" + ) + columns = [{"name": cr[0], "type": cr[1]} for cr in col_rows if cr] + tables.append({"name": table_name, "columns": columns}) + + return { + "catalog": trino_catalog, + "schema": trino_schema, + "tables": tables, + } + except Exception: + return {"catalog": trino_catalog, "schema": trino_schema, "tables": []} + + +# --------------------------------------------------------------------------- +# Analytics: Saved Queries (Requirement 13.7) +# --------------------------------------------------------------------------- + +class SavedQueryBody(BaseModel): + name: str + description: str = "" + sql_text: str + + +@app.get("/api/analytics/saved-queries") +async def list_saved_queries(): + """List all saved queries.""" + rows = await pool.fetch( + "SELECT id, name, description, sql_text, created_by, created_at, updated_at FROM saved_queries ORDER BY updated_at DESC" + ) + return [_row_to_dict(r) for r in rows] + + +@app.post("/api/analytics/saved-queries", status_code=201) +async def create_saved_query(body: SavedQueryBody): + """Save a new query.""" + row = await pool.fetchrow( + """INSERT INTO saved_queries (name, description, sql_text) + VALUES ($1, $2, $3) + RETURNING id, name, description, sql_text, created_by, created_at""", + body.name, body.description, body.sql_text, + ) + return _row_to_dict(row) + + +@app.delete("/api/analytics/saved-queries/{query_id}") +async def delete_saved_query(query_id: str): + """Delete a saved query.""" + result = await pool.execute("DELETE FROM saved_queries WHERE id = $1::uuid", query_id) + if result == "DELETE 0": + raise HTTPException(404, "Query not found") + return {"status": "deleted"}