diff --git a/.gitignore b/.gitignore index a252467..1d6818c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ target/ Cargo.lock perf.* specs/* copy.excalidraw +ui/rpki-explorer/node_modules/ +ui/rpki-explorer/dist/ +ui/rpki-explorer/playwright-report/ +ui/rpki-explorer/test-results/ +ui/rpki-explorer/.vite/ diff --git a/ui/rpki-explorer/README.md b/ui/rpki-explorer/README.md new file mode 100644 index 0000000..cd90552 --- /dev/null +++ b/ui/rpki-explorer/README.md @@ -0,0 +1,81 @@ +# RPKI Explorer + +RPKI Explorer is the SPA frontend for browsing ours RP `rpki_query_service` outputs. + +## Scope + +Current MVP covers: + +- Overview dashboard from latest run, stats, validation reasons, and top repositories. +- Repository -> publication point -> object lazy browser. +- Object detail with live object record, validation summary, lazy parsed/chain/list tabs, and validation explain. +- Exact URI lookup, raw download action, and object / publication point export actions. + +Known backend-dependent limits: + +- Parsed projection, raw download, and export success require `rpki_query_service --repo-bytes-db `. +- Without repo bytes, the UI shows explicit unavailable/error states instead of fabricating data. +- VRP IP/prefix lookup is not part of this MVP and is tracked separately. +- Query service does not provide CORS headers; use a same-origin proxy. + +## Development + +Start query service first: + +```bash +rpki_2/rpki/target/llvm-cov-target/debug/rpki_query_service \ + --query-db .scratch/rpki_explorer_m3_api/query-db \ + --listen 127.0.0.1:19571 +``` + +Start the Vite dev server with a proxy: + +```bash +cd rpki_2/rpki/ui/rpki-explorer +npm install +RPKI_EXPLORER_API_TARGET=http://127.0.0.1:19571 npm run dev -- --port 5173 +``` + +The Vite dev server proxies `/api/v1/*` to `RPKI_EXPLORER_API_TARGET`; default target is `http://127.0.0.1:9557`. + +## Production Build Preview + +```bash +cd rpki_2/rpki/ui/rpki-explorer +npm run build +RPKI_EXPLORER_API_TARGET=http://127.0.0.1:19571 npm run preview -- --port 4173 +``` + +For real deployment, serve `dist/` through a static server or reverse proxy and route `/api/v1/*` to `rpki_query_service` on the same origin. + +## Validation + +```bash +npm run typecheck +npm run lint +npm run build +npm audit --audit-level=high +npm run test:e2e +``` + +Playwright is intentionally configured with `workers: 1`. The current query service object endpoints use lazy `report.json` scans, so parallel browser tests can create artificial backend contention and false failures. + +## Screenshot Evidence + +Milestone screenshots are stored under: + +```text +specs/develop/20260617/m*_playwright/ +``` + +## Directory Layout + +```text +src/api/ query-service client and TypeScript records +src/app/ app shell and top-level navigation state +src/components/layout/ sidebar/topbar shell +src/features/overview/ latest run overview dashboard +src/features/repositories repo -> PP -> object browser +src/features/object-detail live object detail and workflows +src/styles/globals.css MVP visual system +``` diff --git a/ui/rpki-explorer/eslint.config.js b/ui/rpki-explorer/eslint.config.js new file mode 100644 index 0000000..fd5b3c0 --- /dev/null +++ b/ui/rpki-explorer/eslint.config.js @@ -0,0 +1,26 @@ +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"; + +export default tseslint.config( + { ignores: ["dist", "node_modules", "playwright-report", "test-results"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }] + } + } +); diff --git a/ui/rpki-explorer/index.html b/ui/rpki-explorer/index.html new file mode 100644 index 0000000..3178b2e --- /dev/null +++ b/ui/rpki-explorer/index.html @@ -0,0 +1,12 @@ + + + + + + RPKI Explorer + + +
+ + + diff --git a/ui/rpki-explorer/package-lock.json b/ui/rpki-explorer/package-lock.json new file mode 100644 index 0000000..16ee8f4 --- /dev/null +++ b/ui/rpki-explorer/package-lock.json @@ -0,0 +1,3945 @@ +{ + "name": "rpki-explorer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rpki-explorer", + "version": "0.1.0", + "dependencies": { + "@tanstack/react-query": "^5.81.5", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", + "@xyflow/react": "^12.8.2", + "lucide-react": "^0.468.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", + "recharts": "^3.8.1", + "zod": "^3.25.67" + }, + "devDependencies": { + "@eslint/js": "^9.29.0", + "@playwright/test": "^1.53.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/node": "^24.0.3", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^9.29.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^8.0.16", + "vitest": "^4.1.9" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "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==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "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.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "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.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "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/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.3.tgz", + "integrity": "sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.1" + }, + "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/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.1.tgz", + "integrity": "sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/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-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "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-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "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/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "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==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.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.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.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.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "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.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.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.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.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.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "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/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz", + "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.77", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.77", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz", + "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/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/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "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": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-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-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-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/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "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": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.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": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/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", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "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/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/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/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dev": true, + "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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.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.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/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.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "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.18", + "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/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/ui/rpki-explorer/package.json b/ui/rpki-explorer/package.json new file mode 100644 index 0000000..2513349 --- /dev/null +++ b/ui/rpki-explorer/package.json @@ -0,0 +1,44 @@ +{ + "name": "rpki-explorer", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc -b && vite build", + "typecheck": "tsc -b --pretty false", + "lint": "eslint .", + "preview": "vite preview --host 127.0.0.1", + "test:e2e": "playwright test" + }, + "dependencies": { + "@tanstack/react-query": "^5.81.5", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", + "@xyflow/react": "^12.8.2", + "lucide-react": "^0.468.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", + "recharts": "^3.8.1", + "zod": "^3.25.67" + }, + "devDependencies": { + "@eslint/js": "^9.29.0", + "@playwright/test": "^1.53.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/node": "^24.0.3", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^9.29.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^8.0.16", + "vitest": "^4.1.9" + } +} diff --git a/ui/rpki-explorer/playwright.config.ts b/ui/rpki-explorer/playwright.config.ts new file mode 100644 index 0000000..dcf2fab --- /dev/null +++ b/ui/rpki-explorer/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + outputDir: "./test-results", + fullyParallel: true, + workers: 1, + reporter: [["list"], ["html", { open: "never" }]], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:5173", + trace: "retain-on-failure", + screenshot: "only-on-failure" + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] } + } + ] +}); diff --git a/ui/rpki-explorer/src/api/client.ts b/ui/rpki-explorer/src/api/client.ts new file mode 100644 index 0000000..4946bea --- /dev/null +++ b/ui/rpki-explorer/src/api/client.ts @@ -0,0 +1,101 @@ +export interface ApiEnvelope { + data: T; + page: { nextCursor: string | null; limit: number } | null; + meta: { runId: string | null; schemaVersion: number }; +} + +export class ApiError extends Error { + constructor( + message: string, + readonly status: number + ) { + super(message); + this.name = "ApiError"; + } +} + +export type JsonQuery = Record; + +export function withQuery(path: string, query?: JsonQuery): string { + if (!query) { + return path; + } + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null) { + continue; + } + search.set(key, String(value)); + } + const queryString = search.toString(); + return queryString ? `${path}?${queryString}` : path; +} + +export async function getJson(path: string, query?: JsonQuery): Promise> { + const response = await fetch(withQuery(path, query)); + const body = (await response.json().catch(() => ({}))) as { error?: string }; + if (!response.ok) { + throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status); + } + return normalizeEnvelope(body); +} + +export async function postJson(path: string, body?: unknown): Promise> { + const response = await fetch(path, { + body: JSON.stringify(body ?? {}), + headers: { "content-type": "application/json" }, + method: "POST" + }); + const responseBody = (await response.json().catch(() => ({}))) as { error?: string }; + if (!response.ok) { + throw new ApiError(responseBody.error ?? `Request failed: ${response.status}`, response.status); + } + return normalizeEnvelope(responseBody); +} + +export async function getBinary(path: string): Promise { + const response = await fetch(path); + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { error?: string }; + throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status); + } + return response.blob(); +} + +export function normalizeEnvelope(value: unknown): ApiEnvelope { + if (typeof value !== "object" || value === null) { + return { + data: value as T, + page: null, + meta: { runId: null, schemaVersion: 1 } + }; + } + const record = value as { + data?: T; + page?: { nextCursor?: string | null; limit?: number } | null; + meta?: { runId?: string | null; schemaVersion?: number } | null; + }; + return { + data: record.data as T, + page: record.page + ? { + nextCursor: record.page.nextCursor ?? null, + limit: record.page.limit ?? 0 + } + : null, + meta: { + runId: record.meta?.runId ?? null, + schemaVersion: record.meta?.schemaVersion ?? 1 + } + }; +} + +export function normalizeApiError(error: unknown): Error { + if (error instanceof ApiError) { + return error; + } + if (error instanceof Error) { + return error; + } + return new Error("Unknown API error"); +} diff --git a/ui/rpki-explorer/src/api/queryService.ts b/ui/rpki-explorer/src/api/queryService.ts new file mode 100644 index 0000000..6df22c7 --- /dev/null +++ b/ui/rpki-explorer/src/api/queryService.ts @@ -0,0 +1,301 @@ +import { getBinary, getJson, postJson } from "./client"; + +export interface ServiceInfo { + service: string; + version: number; +} + +export interface RunRecord { + schemaVersion: number; + runId: string; + runSeq: number | null; + runDir: string; + validationTime: string | null; + syncMode: string | null; + startedAt: string | null; + finishedAt: string | null; + wallMs: number | null; + artifactPaths: Record; + counts: { + publicationPoints: number; + objects: number; + freshObjects: number; + cachedObjects: number; + rejectedObjects: number; + freshRejectedObjects: number; + cachedRejectedObjects: number; + trustAnchors: number; + vrps: number; + aspas: number; + warnings: number; + }; + indexStatus: string; + indexError: string | null; +} + +export interface RepositoryRecord { + schemaVersion: number; + runId: string; + repoId: string; + uri: string; + host: string; + transport: string; + publicationPoints: number; + objects: number; + rejectedObjects: number; + downloadBytes: number | null; + syncDurationMsTotal: number; + phases: Record; + terminalStates: Record; +} + +export interface PublicationPointRecord { + schemaVersion: number; + runId: string; + ppId: string; + repoId: string; + nodeId: number | null; + parentNodeId: number | null; + rsyncBaseUri: string | null; + manifestRsyncUri: string | null; + publicationPointRsyncUri: string | null; + rrdpNotificationUri: string | null; + source: string | null; + repoSyncSource: string | null; + repoSyncPhase: string | null; + repoSyncDurationMs: number | null; + repoSyncError: string | null; + repoTerminalState: string | null; + thisUpdate: string | null; + nextUpdate: string | null; + verifiedAt: string | null; + objects: number; + rejectedObjects: number; + warnings: number; +} + +export interface ObjectInstanceRecord { + schemaVersion: number; + runId: string; + objectInstanceId: string; + uri: string; + uriHash: string; + sha256: string; + objectType: string; + result: string; + detailSummary: string | null; + repoId: string; + ppId: string; + sourceSection: string; + rejected: boolean; + rejectReason: string | null; +} + +export interface ObjectProjectionRecord { + schemaVersion?: number; + objectType?: string; + sha256?: string; + uri?: string; + parseStatus?: string; + errorSummary?: string | null; + projection?: unknown; +} + +export interface ValidationIssueRecord { + stage?: string; + severity?: string; + reasonCode?: string; + summary?: string; +} + +export interface ObjectValidationRecord { + objectInstanceId: string; + uri: string; + sha256: string; + objectType: string; + finalStatus: string; + auditResult: string; + detailSummary: string | null; + rejected: boolean; + rejectReason: string | null; + sourceSection: string; + explainAvailable: boolean; + authoritative: boolean; + fileValidation: { + status: string; + stage: string; + issues: ValidationIssueRecord[]; + detailSummary: string | null; + }; + chainValidation: { + status: string; + stage: string; + issues: ValidationIssueRecord[]; + note?: string; + edgesPage?: { nextCursor: string | null; limit: number }; + }; +} + +export interface ChainEdgeRecord { + relation: string; + fromUri: string; + toUri: string; + toObjectInstanceId: string | null; + toSha256: string | null; + status: string; + evidence: unknown; +} + +export interface ValidationExplainRecord { + schemaVersion: number; + explainVersion: number; + runId: string; + objectInstanceId: string; + uri: string; + sha256: string; + objectType: string; + finalStatus: string; + auditResult: string; + detailSummary: string | null; + authoritative: boolean; + explainMode: string; + generatedAt: string; + parsevalidate: unknown; + chainvalidate: unknown; + chainEdges: ChainEdgeRecord[]; +} + +export interface ObjectUriIndexRecord { + runId: string; + uri: string; + sha256: string; + objectInstanceId: string; + repoId: string; + ppId: string; +} + +export interface ExportJobRecord { + schemaVersion: number; + jobId: string; + runId: string; + scope: string; + repoId: string | null; + ppId: string | null; + status: string; + createdAt: string; + finishedAt: string | null; + outputPath: string | null; + objectCount: number; + bytesWritten: number; + error: string | null; +} + +export type CountsByKey = Record; + +export async function getServiceInfo() { + return getJson("/api/v1"); +} + +export async function getLatestRun() { + return getJson("/api/v1/latest_run"); +} + +export async function getRunSummary(runId: string) { + return getJson>(`/api/v1/runs/${runId}/summary`); +} + +export async function getStatsObjectTypes(runId: string) { + return getJson(`/api/v1/runs/${runId}/stats/object-types`); +} + +export async function getStatsValidation(runId: string) { + return getJson(`/api/v1/runs/${runId}/stats/validation`); +} + +export async function getStatsReasons(runId: string) { + return getJson(`/api/v1/runs/${runId}/stats/reasons`); +} + +export async function getStatsDownloads(runId: string) { + return getJson>(`/api/v1/runs/${runId}/stats/downloads`); +} + +export async function listRepos(runId: string, limit = 5, cursor?: string | null) { + return getJson(`/api/v1/runs/${runId}/repos`, { limit, cursor: cursor ?? undefined }); +} + +export async function listPublicationPointsForRepo(runId: string, repoId: string, limit = 50, cursor?: string | null) { + return getJson(`/api/v1/runs/${runId}/repos/${repoId}/publication-points`, { + limit, + cursor: cursor ?? undefined + }); +} + +export async function listObjectsForPublicationPoint(runId: string, ppId: string, limit = 50, cursor?: string | null) { + return getJson(`/api/v1/runs/${runId}/publication-points/${ppId}/objects`, { + limit, + cursor: cursor ?? undefined + }); +} + +export async function getObject(runId: string, objectInstanceId: string) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}`); +} + +export async function getObjectByUri(runId: string, uri: string) { + return getJson(`/api/v1/runs/${runId}/objects/by-uri`, { uri }); +} + +export async function getObjectParsed(runId: string, objectInstanceId: string) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed`); +} + +export async function getObjectValidation(runId: string, objectInstanceId: string) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation`); +} + +export async function getObjectChain(runId: string, objectInstanceId: string) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/chain`); +} + +export async function listManifestFiles(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/manifest-files`, { + limit, + cursor: cursor ?? undefined + }); +} + +export async function listRevokedCertificates(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) { + return getJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/revoked-certs`, { + limit, + cursor: cursor ?? undefined + }); +} + +export async function explainObjectValidation(runId: string, objectInstanceId: string, forceRefresh = false) { + return postJson(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation/explain`, { + forceRefresh + }); +} + +export async function downloadRawObject(runId: string, objectInstanceId: string) { + return getBinary(`/api/v1/runs/${runId}/objects/${objectInstanceId}/raw`); +} + +export async function createObjectSetExport(runId: string, objectInstanceIds: string[]) { + return postJson(`/api/v1/runs/${runId}/exports`, { + objectInstanceIds, + scope: "object_set" + }); +} + +export async function createPublicationPointExport(runId: string, ppId: string) { + return postJson(`/api/v1/runs/${runId}/exports`, { + ppId, + scope: "publication_point" + }); +} + +export async function getExportJob(runId: string, jobId: string) { + return getJson(`/api/v1/runs/${runId}/exports/${jobId}`); +} diff --git a/ui/rpki-explorer/src/app/App.tsx b/ui/rpki-explorer/src/app/App.tsx new file mode 100644 index 0000000..69ceb9b --- /dev/null +++ b/ui/rpki-explorer/src/app/App.tsx @@ -0,0 +1,35 @@ +import { Activity, Braces, Database, FileSearch, GitBranch, Home, PackageOpen, ShieldCheck } from "lucide-react"; +import { useState } from "react"; +import { Shell } from "../components/layout/Shell"; +import { ObjectDetailPage } from "../features/object-detail/ObjectDetailPage"; +import { OverviewPage } from "../features/overview/OverviewPage"; +import { RepositoriesPage } from "../features/repositories/RepositoriesPage"; + +const navigationItems = [ + { id: "overview", label: "Overview", icon: Home }, + { id: "repositories", label: "Repositories", icon: Database }, + { id: "publication-points", label: "Publication Points", icon: GitBranch }, + { id: "objects", label: "Objects", icon: Braces }, + { id: "validation", label: "Validation", icon: ShieldCheck }, + { id: "exports", label: "Exports", icon: PackageOpen }, + { id: "runs", label: "Runs", icon: Activity }, + { id: "api", label: "API", icon: FileSearch } +]; + +export function App() { + const [activeView, setActiveView] = useState("overview"); + const [selectedObjectInstanceId, setSelectedObjectInstanceId] = useState(null); + + const openObject = (objectInstanceId: string) => { + setSelectedObjectInstanceId(objectInstanceId); + setActiveView("objects"); + }; + + return ( + + {activeView === "objects" ? : null} + {activeView === "repositories" ? : null} + {activeView !== "objects" && activeView !== "repositories" ? : null} + + ); +} diff --git a/ui/rpki-explorer/src/app/providers.tsx b/ui/rpki-explorer/src/app/providers.tsx new file mode 100644 index 0000000..2370cec --- /dev/null +++ b/ui/rpki-explorer/src/app/providers.tsx @@ -0,0 +1,7 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import type { PropsWithChildren } from "react"; +import { queryClient } from "./queryClient"; + +export function AppProviders({ children }: PropsWithChildren) { + return {children}; +} diff --git a/ui/rpki-explorer/src/app/queryClient.ts b/ui/rpki-explorer/src/app/queryClient.ts new file mode 100644 index 0000000..f29ac8e --- /dev/null +++ b/ui/rpki-explorer/src/app/queryClient.ts @@ -0,0 +1,15 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + gcTime: 10 * 60_000, + retry: 1, + refetchOnWindowFocus: false + }, + mutations: { + retry: false + } + } +}); diff --git a/ui/rpki-explorer/src/components/layout/Shell.tsx b/ui/rpki-explorer/src/components/layout/Shell.tsx new file mode 100644 index 0000000..4cac951 --- /dev/null +++ b/ui/rpki-explorer/src/components/layout/Shell.tsx @@ -0,0 +1,66 @@ +import type { LucideIcon } from "lucide-react"; +import { BarChart3, ChevronDown, CircleHelp, Search, Sun } from "lucide-react"; +import type { PropsWithChildren } from "react"; + +export interface NavigationItem { + id: string; + label: string; + icon: LucideIcon; +} + +interface ShellProps extends PropsWithChildren { + activeView: string; + navigationItems: NavigationItem[]; + onNavigate: (id: string) => void; +} + +export function Shell({ activeView, navigationItems, onNavigate, children }: ShellProps) { + return ( +
+ +
+
+
+ Run + latest run +
+
+ +
Ready
+ + + +
+
+ {children} +
+
+ ); +} diff --git a/ui/rpki-explorer/src/features/object-detail/ObjectDetailPage.tsx b/ui/rpki-explorer/src/features/object-detail/ObjectDetailPage.tsx new file mode 100644 index 0000000..6be8291 --- /dev/null +++ b/ui/rpki-explorer/src/features/object-detail/ObjectDetailPage.tsx @@ -0,0 +1,530 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Check, Copy, Download, FileText, GitBranch, Loader2, Search, ShieldCheck } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { normalizeApiError } from "../../api/client"; +import { + createObjectSetExport, + createPublicationPointExport, + downloadRawObject, + explainObjectValidation, + getLatestRun, + getObject, + getObjectByUri, + getObjectChain, + getObjectParsed, + getObjectValidation, + listManifestFiles, + listObjectsForPublicationPoint, + listRevokedCertificates, + type ChainEdgeRecord, + type ObjectInstanceRecord, + type ObjectValidationRecord, + type ValidationExplainRecord +} from "../../api/queryService"; + +const SAMPLE_PP_ID = "node_10342"; +const tabs = ["Parsed", "Validation", "Chain", "Manifest files", "Revoked certs"] as const; +type ObjectTab = (typeof tabs)[number]; + +interface ObjectDetailPageProps { + initialObjectInstanceId?: string | null; +} + +export function ObjectDetailPage({ initialObjectInstanceId }: ObjectDetailPageProps) { + const [selectedObjectInstanceId, setSelectedObjectInstanceId] = useState(initialObjectInstanceId ?? null); + const [activeTab, setActiveTab] = useState("Validation"); + const [searchUri, setSearchUri] = useState(""); + const latestRunQuery = useQuery({ queryKey: ["object-detail-latest-run"], queryFn: getLatestRun }); + const runId = latestRunQuery.data?.data.runId ?? "latest"; + const objectsQuery = useQuery({ + enabled: Boolean(latestRunQuery.data?.data.runId), + queryKey: ["object-detail-sample-objects", runId, SAMPLE_PP_ID], + queryFn: () => listObjectsForPublicationPoint(runId, SAMPLE_PP_ID, 50), + staleTime: 5 * 60 * 1000 + }); + + const objectRows = useMemo(() => objectsQuery.data?.data ?? [], [objectsQuery.data?.data]); + const objectOptions = useMemo( + () => objectRows.map((object) => ({ + object, + label: `${labelObjectType(object.objectType)} ${shortName(object.uri)}` + })), + [objectRows] + ); + useEffect(() => { + if (!selectedObjectInstanceId && objectRows.length > 0) { + const preferred = objectRows.find((item) => item.objectType === "manifest") ?? objectRows[0]; + setSelectedObjectInstanceId(preferred.objectInstanceId); + } + }, [objectRows, selectedObjectInstanceId]); + + const objectQuery = useQuery({ + enabled: Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + queryKey: ["object-detail-object", runId, selectedObjectInstanceId], + queryFn: () => getObject(runId, selectedObjectInstanceId ?? ""), + staleTime: 5 * 60 * 1000 + }); + const validationQuery = useQuery({ + enabled: Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + queryKey: ["object-detail-validation", runId, selectedObjectInstanceId], + queryFn: () => getObjectValidation(runId, selectedObjectInstanceId ?? ""), + staleTime: 5 * 60 * 1000 + }); + const parsedQuery = useQuery({ + enabled: activeTab === "Parsed" && Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + queryKey: ["object-detail-parsed", runId, selectedObjectInstanceId], + queryFn: () => getObjectParsed(runId, selectedObjectInstanceId ?? ""), + staleTime: 5 * 60 * 1000 + }); + const chainQuery = useQuery({ + enabled: activeTab === "Chain" && Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + queryKey: ["object-detail-chain", runId, selectedObjectInstanceId], + queryFn: () => getObjectChain(runId, selectedObjectInstanceId ?? ""), + staleTime: 5 * 60 * 1000 + }); + const manifestFilesQuery = useQuery({ + enabled: activeTab === "Manifest files" && Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + retry: false, + queryKey: ["object-detail-manifest-files", runId, selectedObjectInstanceId], + queryFn: () => listManifestFiles(runId, selectedObjectInstanceId ?? "", 50), + staleTime: 5 * 60 * 1000 + }); + const revokedCertsQuery = useQuery({ + enabled: activeTab === "Revoked certs" && Boolean(selectedObjectInstanceId && latestRunQuery.data?.data.runId), + retry: false, + queryKey: ["object-detail-revoked-certs", runId, selectedObjectInstanceId], + queryFn: () => listRevokedCertificates(runId, selectedObjectInstanceId ?? "", 50), + staleTime: 5 * 60 * 1000 + }); + const explainMutation = useMutation({ + mutationFn: () => explainObjectValidation(runId, selectedObjectInstanceId ?? "") + }); + const uriSearchMutation = useMutation({ + mutationFn: (uri: string) => getObjectByUri(runId, uri), + onSuccess: (result) => { + setSelectedObjectInstanceId(result.data.objectInstanceId); + setActiveTab("Validation"); + } + }); + const rawDownloadMutation = useMutation({ + mutationFn: () => downloadRawObject(runId, selectedObjectInstanceId ?? "") + }); + const objectExportMutation = useMutation({ + mutationFn: () => createObjectSetExport(runId, selectedObjectInstanceId ? [selectedObjectInstanceId] : []) + }); + const ppExportMutation = useMutation({ + mutationFn: () => createPublicationPointExport(runId, selectedObject?.ppId ?? SAMPLE_PP_ID) + }); + + const selectedObject = objectQuery.data?.data ?? objectRows.find((item) => item.objectInstanceId === selectedObjectInstanceId) ?? null; + const validation = validationQuery.data?.data ?? null; + const apiError = latestRunQuery.error ?? objectsQuery.error ?? objectQuery.error ?? validationQuery.error; + + return ( +
+ {apiError ? : null} +
+ + +
+
+
+
{ + event.preventDefault(); + if (searchUri.trim()) { + uriSearchMutation.mutate(searchUri.trim()); + } + }} + > +
+ + + + + + + + + + + + {objectRows.map((object) => ( + setSelectedObjectInstanceId(object.objectInstanceId)} + > + + + + + + + ))} + +
TypeURIHash (SHA-256)SourceStatus
{labelObjectType(object.objectType)}{object.uri}{object.sha256.slice(0, 16)}...{object.sourceSection}
+
+ + +
+
+
+ ); +} + +function ObjectMeta({ object, validation }: { object: ObjectInstanceRecord; validation: ObjectValidationRecord | null }) { + return ( +
+ + + + + + +
+ ); +} + +function WorkflowStatus({ + objectExport, + objectExportError, + ppExport, + ppExportError, + rawError, + rawPending, + searchError, + searchPending +}: { + objectExport: { jobId?: string; status?: string } | null; + objectExportError: unknown; + ppExport: { jobId?: string; status?: string } | null; + ppExportError: unknown; + rawError: unknown; + rawPending: boolean; + searchError: unknown; + searchPending: boolean; +}) { + const lines = [ + searchPending ? "Searching object URI..." : null, + searchError ? `URI search failed: ${normalizeApiError(searchError).message}` : null, + rawPending ? "Downloading raw object..." : null, + rawError ? `Raw download failed: ${normalizeApiError(rawError).message}` : null, + objectExport ? `Object export job ${objectExport.jobId ?? ""} ${objectExport.status ?? ""}`.trim() : null, + objectExportError ? `Object export failed: ${normalizeApiError(objectExportError).message}` : null, + ppExport ? `PP export job ${ppExport.jobId ?? ""} ${ppExport.status ?? ""}`.trim() : null, + ppExportError ? `PP export failed: ${normalizeApiError(ppExportError).message}` : null + ].filter((line): line is string => Boolean(line)); + if (lines.length === 0) { + return null; + } + return ( +
+ +
    + {lines.map((line) => ( +
  • {line}
  • + ))} +
+
+ ); +} + +function ParsedPanel({ parsed, isFetching, error }: { parsed: unknown; isFetching: boolean; error: unknown }) { + if (isFetching) { + return ; + } + if (error) { + return ; + } + if (!parsed) { + return ; + } + return ( +
+ } /> + +
+ ); +} + +function ValidationPanel({ validation, isFetching, explain, explainPending, onExplain, explainError }: { + validation: ObjectValidationRecord | null; + isFetching: boolean; + explain: ValidationExplainRecord | null; + explainPending: boolean; + onExplain: () => void; + explainError: unknown; +}) { + if (isFetching) { + return ; + } + return ( +
+ } /> +
+ + + +
+
+ + Explain is an audit projection, not full authoritative revalidation. +
+ {explainError ?
Explain failed: {normalizeApiError(explainError).message}
: null} + {explain ? ( +
+ + + + +
+ ) : null} +
+ ); +} + +function ChainPanel({ edges, isFetching, error }: { edges: ChainEdgeRecord[]; isFetching: boolean; error: unknown }) { + if (isFetching) { + return ; + } + if (error) { + return ; + } + return ( +
+ } /> + {edges.length === 0 ?
No chain edges recorded for this object.
: null} +
+ {edges.map((edge) => ( +
+ {edge.relation} + {edge.status} + {edge.toUri} +
+ ))} +
+
+ ); +} + +function ArrayPanel({ title, rows, isFetching, error }: { title: string; rows: unknown[]; isFetching: boolean; error: unknown }) { + if (isFetching) { + return ; + } + if (error) { + return ; + } + return ( +
+ + {rows.length === 0 ?
No rows returned for this object.
: null} + {rows.length > 0 ? : null} +
+ ); +} + +function LoadingPanel({ title, label }: { title: string; label: string }) { + return ( +
+ + +
+ ); +} + +function NoticePanel({ title, message }: { title: string; message: string }) { + return ( +
+ +
{message}
+
+ ); +} + +function PanelHeading({ eyebrow, title, icon }: { eyebrow: string; title: string; icon?: React.ReactNode }) { + return ( +
+
+

{eyebrow}

+

{title}

+
+ {icon} +
+ ); +} + +function ValidationCheck({ label, status, note }: { label: string; status: string; note: string }) { + const ok = status === "valid" || status === "ok"; + return ( +
+ +
+ {label}: {status} +

{note}

+
+
+ ); +} + +function JsonPreview({ value }: { value: unknown }) { + const text = useMemo(() => JSON.stringify(value, null, 2), [value]); + return
{text}
; +} + +function ErrorBanner({ error }: { error: unknown }) { + return ( +
+ Object API unavailable + {normalizeApiError(error).message} +
+ ); +} + +function StatusPill({ state }: { state: string }) { + const normalized = state.toLowerCase(); + const className = normalized.includes("fail") || normalized.includes("reject") || normalized.includes("error") || normalized.includes("invalid") + ? "warning" + : normalized.includes("cache") || normalized.includes("fallback") + ? "fallback" + : "healthy"; + return {state}; +} + +function LoadingLine({ label }: { label: string }) { + return ( +
+ + {label} +
+ ); +} + +function Meta({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} + +function labelObjectType(type: string) { + const labels: Record = { + certificate: "CER", + crl: "CRL", + manifest: "MFT", + roa: "ROA", + aspa: "ASPA" + }; + return labels[type] ?? type.toUpperCase(); +} + +function shortName(uri: string) { + return uri.split("/").filter(Boolean).at(-1) ?? uri; +} + +function issuesText(issues: { summary?: string }[] | undefined) { + if (!issues || issues.length === 0) { + return null; + } + return issues.map((issue) => issue.summary).filter(Boolean).join("; "); +} diff --git a/ui/rpki-explorer/src/features/overview/OverviewPage.tsx b/ui/rpki-explorer/src/features/overview/OverviewPage.tsx new file mode 100644 index 0000000..d4b585a --- /dev/null +++ b/ui/rpki-explorer/src/features/overview/OverviewPage.tsx @@ -0,0 +1,347 @@ +import { useQuery } from "@tanstack/react-query"; +import { AlertTriangle, ArrowUpRight, CheckCircle2, Clock3, ShieldCheck } from "lucide-react"; +import { objectTypeRows, overviewKpis, topRepositories, validationIssues, validationSlices } from "../../test/fixtures/dashboard"; +import { + getLatestRun, + getStatsObjectTypes, + getStatsReasons, + getStatsValidation, + listRepos, + type CountsByKey, + type RepositoryRecord, + type RunRecord +} from "../../api/queryService"; +import { normalizeApiError } from "../../api/client"; + +const validationColors = ["#0ca678", "#facc15", "#e11d48", "#94a3b8"]; +const objectTypeLabels: Record = { + aspa: "ASPA", + certificate: "CER / RC", + crl: "CRL", + gbr: "GBR", + manifest: "MFT", + roa: "ROA", + router_certificate: "Router Cert" +}; + +export function OverviewPage() { + const latestRunQuery = useQuery({ + queryKey: ["latest-run"], + queryFn: getLatestRun + }); + const liveRun = latestRunQuery.data?.data; + const runId = liveRun?.runId ?? "latest"; + const enabled = Boolean(liveRun); + const objectTypesQuery = useQuery({ + queryKey: ["stats-object-types", runId], + queryFn: () => getStatsObjectTypes(runId), + enabled + }); + const validationQuery = useQuery({ + queryKey: ["stats-validation", runId], + queryFn: () => getStatsValidation(runId), + enabled + }); + const reasonsQuery = useQuery({ + queryKey: ["stats-reasons", runId], + queryFn: () => getStatsReasons(runId), + enabled + }); + const reposQuery = useQuery({ + queryKey: ["top-repos", runId], + queryFn: () => listRepos(runId, 8), + enabled + }); + + const error = latestRunQuery.error ? normalizeApiError(latestRunQuery.error) : null; + const kpis = liveRun ? kpisFromRun(liveRun) : overviewKpis; + const objectRows = objectTypesQuery.data?.data ? objectRowsFromStats(objectTypesQuery.data.data) : objectTypeRows; + const validationRows = validationQuery.data?.data ? validationRowsFromStats(validationQuery.data.data) : validationSlices; + const validPercent = percent(validationQuery.data?.data?.ok ?? validationRows[0]?.value ?? 0, sumValues(validationQuery.data?.data) || 100); + const repoRows = reposQuery.data?.data ? reposFromApi(reposQuery.data.data) : topRepositories; + const issueRows = reasonsQuery.data?.data ? issuesFromReasons(reasonsQuery.data.data) : validationIssues; + const statusText = liveRun ? statusForRun(liveRun) : "Static fixture"; + const statusSubtext = liveRun ? `run ${liveRun.runSeq ?? liveRun.runId} · ${formatDuration(liveRun.wallMs)}` : "query service not connected"; + + return ( +
+ {error ? ( +
+ Query service unavailable + {error.message}. Showing static fallback data for layout inspection. +
+ ) : null} +
+
+

{liveRun ? `Latest ready run · ${liveRun.syncMode ?? "unknown"} mode` : "Latest ready run · fallback"}

+

Global RPKI validation health

+

+ {liveRun + ? `Live query-service view for ${liveRun.runId}, validated at ${formatTimestamp(liveRun.validationTime)}.` + : "Static fallback showing how operators inspect validation output, repository health, and input quality."} +

+
+
+ +
+ {statusText} + {statusSubtext} +
+
+
+ +
+ {kpis.map((metric) => ( +
+ {metric.label} + {metric.value} + {metric.delta} +
+ ))} +
+ +
+
+
+
+

Validation

+

Result distribution

+
+ +
+
+
+
+ {validPercent}% + usable +
+
+
+ {validationRows.map((slice) => ( +
+ + + {slice.value}% +
+ ))} +
+
+ +
+
+
+

Objects

+

Object type mix

+
+ +
+
+ {objectRows.map((row) => ( +
+
+ {row.type} + {row.count} +
+
+ +
+
+ ))} +
+
+ +
+
+
+

Repositories

+

Top repositories by workload

+
+ +
+ + + + + + + + + + + + {repoRows.map((repo) => ( + + + + + + + + ))} + +
HostTransportDurationObjectsStatus
{repo.host}{repo.transport}{repo.duration}{repo.objects} + {repo.status} +
+
+ +
+
+
+

Triage

+

Recent validation issues

+
+ +
+
+ {issueRows.map((issue) => ( +
+
+ {issue.severity} + {issue.type} +
+

{issue.reason}

+ {issue.uri} + {issue.repo} +
+ ))} +
+
+
+
+ ); +} + +function kpisFromRun(run: RunRecord) { + return [ + { label: "VRPs", value: formatNumber(run.counts.vrps), delta: "latest run", tone: "blue" }, + { label: "ASPAs", value: formatNumber(run.counts.aspas), delta: "latest run", tone: "cyan" }, + { label: "Objects", value: formatNumber(run.counts.objects), delta: "indexed inputs", tone: "blue" }, + { label: "Publication Points", value: formatNumber(run.counts.publicationPoints), delta: "indexed", tone: "purple" }, + { label: "Rejected Objects", value: formatNumber(run.counts.rejectedObjects), delta: "validation rejects", tone: "red" }, + { label: "Warnings", value: formatNumber(run.counts.warnings), delta: `${formatDuration(run.wallMs)} wall`, tone: "amber" } + ]; +} + +function objectRowsFromStats(stats: CountsByKey) { + const total = sumValues(stats) || 1; + return Object.entries(stats) + .sort(([, left], [, right]) => right - left) + .slice(0, 7) + .map(([type, count]) => ({ + type: objectTypeLabels[type] ?? type, + count: formatNumber(count), + percent: Math.max(1, Math.round((count / total) * 100)) + })); +} + +function validationRowsFromStats(stats: CountsByKey) { + const total = sumValues(stats) || 1; + return Object.entries(stats) + .sort(([, left], [, right]) => right - left) + .map(([label, count], index) => ({ + label: titleCase(label), + value: Math.round((count / total) * 100), + color: validationColors[index] ?? "#94a3b8" + })); +} + +function reposFromApi(repos: RepositoryRecord[]) { + return [...repos] + .sort((left, right) => right.objects - left.objects) + .slice(0, 6) + .map((repo) => ({ + host: repo.host, + transport: repo.transport.toUpperCase(), + duration: formatDuration(repo.syncDurationMsTotal), + objects: formatNumber(repo.objects), + status: repoStatus(repo) + })); +} + +function issuesFromReasons(reasons: CountsByKey) { + const entries = Object.entries(reasons).sort(([, left], [, right]) => right - left); + if (entries.length === 0) { + return [ + { + severity: "low", + type: "OK", + reason: "No validation reject reasons reported in the latest run.", + uri: "query-service://validation/reasons", + repo: "latest run" + } + ]; + } + return entries.slice(0, 4).map(([reason, count], index) => ({ + severity: index === 0 ? "high" : index === 1 ? "medium" : "low", + type: "Reject", + reason: `${formatNumber(count)} object(s): ${reason}`, + uri: `query-service://validation/reasons/${index + 1}`, + repo: "latest run" + })); +} + +function repoStatus(repo: RepositoryRecord) { + if (repo.terminalStates.failed_no_cache || repo.phases.rrdp_failed_rsync_failed) { + return "warning"; + } + if (repo.terminalStates.fallback_current_instance || repo.phases.rsync_ok) { + return "fallback"; + } + return "healthy"; +} + +function statusForRun(run: RunRecord) { + if (run.counts.rejectedObjects > 0 || run.counts.warnings > 0) { + return "Healthy with warnings"; + } + return "Healthy"; +} + +function donutGradient(rows: Array<{ value: number; color: string }>) { + let start = 0; + const parts = rows.map((row) => { + const end = Math.min(100, start + row.value); + const segment = `${row.color} ${start}% ${end}%`; + start = end; + return segment; + }); + return `conic-gradient(${parts.join(", ")})`; +} + +function sumValues(values?: CountsByKey) { + return Object.values(values ?? {}).reduce((sum, value) => sum + value, 0); +} + +function percent(value: number, total: number) { + if (total <= 0) { + return 0; + } + return Math.round((value / total) * 100); +} + +function formatNumber(value: number) { + return new Intl.NumberFormat("en-US").format(value); +} + +function formatDuration(ms: number | null) { + if (ms === null || ms === undefined) { + return "—"; + } + if (ms < 1000) { + return `${ms}ms`; + } + return `${(ms / 1000).toFixed(1)}s`; +} + +function formatTimestamp(value: string | null) { + if (!value) { + return "unknown time"; + } + return value.replace("T", " ").replace("Z", " UTC"); +} + +function titleCase(value: string) { + return value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); +} diff --git a/ui/rpki-explorer/src/features/repositories/RepositoriesPage.tsx b/ui/rpki-explorer/src/features/repositories/RepositoriesPage.tsx new file mode 100644 index 0000000..2e4676f --- /dev/null +++ b/ui/rpki-explorer/src/features/repositories/RepositoriesPage.tsx @@ -0,0 +1,224 @@ +import { useQuery } from "@tanstack/react-query"; +import { ChevronRight, Database, FileCode2, GitBranch, Loader2 } from "lucide-react"; +import type { ReactNode } from "react"; +import { useState } from "react"; +import { + getLatestRun, + listObjectsForPublicationPoint, + listPublicationPointsForRepo, + listRepos, + type ObjectInstanceRecord +} from "../../api/queryService"; +import { normalizeApiError } from "../../api/client"; + +interface RepositoriesPageProps { + onOpenObject?: (objectInstanceId: string) => void; +} + +export function RepositoriesPage({ onOpenObject }: RepositoriesPageProps) { + const [selectedRepoId, setSelectedRepoId] = useState(null); + const [selectedPpId, setSelectedPpId] = useState(null); + const latestRunQuery = useQuery({ + queryKey: ["repositories-latest-run"], + queryFn: getLatestRun + }); + const runId = latestRunQuery.data?.data.runId ?? "latest"; + const reposQuery = useQuery({ + queryKey: ["repositories", runId], + queryFn: () => listRepos(runId, 50), + enabled: Boolean(latestRunQuery.data?.data.runId) + }); + + const repos = reposQuery.data?.data ?? []; + const selectedRepo = selectedRepoId ? repos.find((repo) => repo.repoId === selectedRepoId) ?? null : null; + const ppsQuery = useQuery({ + queryKey: ["repository-publication-points", runId, selectedRepo?.repoId ?? ""], + queryFn: () => listPublicationPointsForRepo(runId, selectedRepo?.repoId ?? "", 50), + enabled: Boolean(selectedRepo?.repoId) + }); + + const publicationPoints = ppsQuery.data?.data ?? []; + const selectedPp = selectedPpId + ? publicationPoints.find((pp) => pp.ppId === selectedPpId) ?? null + : null; + const objectsQuery = useQuery({ + queryKey: ["publication-point-objects", runId, selectedPp?.ppId ?? ""], + queryFn: () => listObjectsForPublicationPoint(runId, selectedPp?.ppId ?? "", 50), + enabled: Boolean(selectedPp?.ppId) + }); + + const error = latestRunQuery.error ?? reposQuery.error ?? ppsQuery.error ?? objectsQuery.error; + + return ( +
+
+
+

Repository browser · live query service

+

Repository / publication point / object browser

+

Browse repo trees first, then expand publication points and object rows only on demand.

+
+
+ +
+ {latestRunQuery.data?.data.runId ?? "Loading run"} + {repos.length ? `${repos.length} repositories loaded` : "waiting for query service"} +
+
+
+ + {error ? ( +
+ Repository API unavailable + {normalizeApiError(error).message} +
+ ) : null} + +
+
+ } label="Repositories" meta={reposQuery.isFetching ? "loading" : `${repos.length} shown`} /> +
+ {repos.map((repo) => ( + + ))} +
+
+ +
+ } + label="Publication Points" + meta={ppsQuery.isFetching ? "loading" : `${publicationPoints.length} shown`} + /> + {!selectedRepo ? ( +
Select a repository to load its publication points.
+ ) : null} + {ppsQuery.isFetching ? : null} +
+ {publicationPoints.map((pp) => ( + + ))} +
+
+ +
+ } + label="Objects" + meta={objectsQuery.isFetching ? "loading" : `${objectsQuery.data?.data.length ?? 0} of selected PP`} + /> + {!selectedPp ? ( +
Select a publication point to load its objects.
+ ) : null} + {objectsQuery.isFetching ? : null} + {selectedPp && !objectsQuery.isFetching ? ( + + ) : null} +
+ Default query uses limit=50. Next cursor: {objectsQuery.data?.page?.nextCursor ?? "none"} +
+
+
+
+ ); +} + +function PanelTitle({ icon, label, meta }: { icon: ReactNode; label: string; meta: string }) { + return ( +
+
+

{label}

+

{label}

+
+ + {icon} + {meta} + +
+ ); +} + +function ObjectTable({ objects, onOpenObject }: { objects: ObjectInstanceRecord[]; onOpenObject?: (objectInstanceId: string) => void }) { + if (objects.length === 0) { + return
No objects returned for this publication point.
; + } + return ( + + + + + + + + + + + + + {objects.map((object) => ( + + + + + + + + + ))} + +
TypeURIHashSourceResultAction
{object.objectType}{object.uri}{object.sha256.slice(0, 12)}...{object.sourceSection} + +
+ ); +} + +function StatusPill({ state }: { state: string }) { + const normalized = state.toLowerCase(); + const className = normalized.includes("fail") || normalized.includes("reject") || normalized.includes("error") + ? "warning" + : normalized.includes("cache") || normalized.includes("fallback") + ? "fallback" + : "healthy"; + return {state}; +} + +function LoadingLine() { + return ( +
+ + Loading selected branch... +
+ ); +} + +function shortName(uri: string) { + return uri.split("/").filter(Boolean).at(-1) ?? uri; +} diff --git a/ui/rpki-explorer/src/main.tsx b/ui/rpki-explorer/src/main.tsx new file mode 100644 index 0000000..4b1ff6f --- /dev/null +++ b/ui/rpki-explorer/src/main.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { AppProviders } from "./app/providers"; +import { App } from "./app/App"; +import "./styles/globals.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + +); diff --git a/ui/rpki-explorer/src/styles/globals.css b/ui/rpki-explorer/src/styles/globals.css new file mode 100644 index 0000000..f8c75f9 --- /dev/null +++ b/ui/rpki-explorer/src/styles/globals.css @@ -0,0 +1,1171 @@ +:root { + color: #0b1733; + background: #f7faff; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: #f7faff; +} + +button, +input { + font: inherit; +} + +button { + cursor: pointer; +} + +button:focus-visible, +input:focus-visible { + outline: 3px solid rgba(11, 91, 211, 0.32); + outline-offset: 2px; +} + +.app-shell { + display: grid; + grid-template-columns: 230px minmax(0, 1fr); + min-height: 100vh; + background: linear-gradient(180deg, #ffffff 0%, #f4f8fe 100%); +} + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + border-right: 1px solid #dbe4f0; + background: rgba(255, 255, 255, 0.88); + color: #10213f; + padding: 22px 14px; +} + +.brand { + display: flex; + align-items: center; + gap: 11px; + margin-bottom: 30px; + padding: 0 10px; +} + +.brand-mark { + display: grid; + width: 36px; + height: 36px; + place-items: center; + border-radius: 12px; + border: 2px solid #1459d9; + color: #1459d9; + font-weight: 850; +} + +.brand-title { + margin: 0; + color: #07142d; + font-size: 21px; + font-weight: 850; + letter-spacing: -0.04em; +} + +.brand-subtitle { + color: #0b5bd3; + font-size: 9px; + font-weight: 850; + letter-spacing: 0.15em; + text-transform: uppercase; +} + +.eyebrow { + margin-bottom: 8px; + color: #60718e; + font-size: 12px; + font-weight: 850; + letter-spacing: 0.15em; + text-transform: uppercase; +} + +.nav-list { + display: grid; + gap: 8px; +} + +.nav-item { + display: flex; + width: 100%; + align-items: center; + gap: 12px; + border: 0; + border-radius: 8px; + padding: 12px 13px; + background: transparent; + color: #122544; + text-align: left; +} + +.nav-item.active, +.nav-item:hover { + background: #eaf3ff; + color: #0759d7; +} + +.nav-item svg { + color: #24466f; +} + +.nav-item.active svg, +.nav-item:hover svg { + color: #0759d7; +} + +.main-panel { + min-width: 0; +} + +.topbar { + display: flex; + min-height: 76px; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #dbe4f0; + background: rgba(255, 255, 255, 0.88); + padding: 14px 28px; +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h2 { + margin-bottom: 8px; + font-size: 24px; + letter-spacing: -0.04em; +} + +h3 { + margin-bottom: 0; + font-size: 18px; + letter-spacing: -0.02em; +} + +.run-selector { + display: flex; + align-items: center; + gap: 18px; + min-width: 206px; + border: 1px solid #d2deee; + border-radius: 9px; + background: white; + color: #07142d; + padding: 11px 14px; +} + +.run-selector span { + font-weight: 750; +} + +.run-selector strong { + flex: 1; + font-size: 14px; +} + +.topbar-actions { + display: flex; + flex: 1; + align-items: center; + justify-content: flex-end; + gap: 14px; +} + +.search-box { + display: flex; + align-items: center; + gap: 10px; + width: min(740px, 100%); + border: 1px solid #d2deee; + border-radius: 9px; + background: white; + padding: 11px 14px; + color: #64748b; +} + +.search-box input { + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: #0f172a; +} + +.ready-pill { + display: inline-flex; + align-items: center; + gap: 9px; + border: 1px solid #b7efd4; + border-radius: 9px; + background: #eefdf5; + color: #047857; + font-size: 14px; + font-weight: 800; + padding: 11px 18px; +} + +.ready-pill span { + width: 8px; + height: 8px; + border-radius: 50%; + background: #10b981; +} + +.icon-button { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border: 0; + border-radius: 9px; + background: transparent; + color: #24466f; +} + +.page-stack { + display: grid; + gap: 18px; + padding: 18px; +} + +.alert-banner { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + border: 1px solid #fed7aa; + border-radius: 12px; + background: #fff7ed; + color: #9a3412; + padding: 12px 16px; +} + +.alert-banner strong { + color: #7c2d12; +} + +.hero-dashboard { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + border: 1px solid #dbe4f0; + border-radius: 14px; + background: white; + padding: 18px 20px; +} + +.compact-hero { + padding: 16px 20px; +} + +.hero-dashboard p { + max-width: 760px; + margin-bottom: 0; + color: #53657f; + line-height: 1.55; +} + +.hero-status { + display: flex; + min-width: 230px; + align-items: center; + gap: 12px; + border: 1px solid #dbe4f0; + border-radius: 12px; + background: #f8fbff; + color: #0b5bd3; + padding: 14px; +} + +.hero-status strong, +.hero-status span { + display: block; +} + +.hero-status span { + color: #60718e; + font-size: 13px; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 16px; +} + +.metric-card { + position: relative; + display: grid; + grid-template-columns: 52px 1fr; + gap: 13px; + min-height: 110px; + align-items: center; + border: 1px solid #dbe4f0; + border-radius: 14px; + background: white; + box-shadow: 0 14px 34px rgba(16, 33, 63, 0.05); + padding: 18px; +} + +.metric-card::before { + display: grid; + width: 52px; + height: 52px; + grid-row: span 3; + place-items: center; + border-radius: 50%; + background: var(--metric-bg, #edf4ff); + color: var(--metric-fg, #0b5bd3); + content: "◉"; + font-size: 22px; +} + +.metric-card span, +.metric-card small { + display: block; + color: #53657f; + font-size: 13px; +} + +.metric-card span { + font-weight: 750; +} + +.metric-card strong { + display: block; + margin: 2px 0; + color: #07142d; + font-size: 26px; + font-weight: 850; + letter-spacing: -0.045em; +} + +.metric-card small { + color: #05825f; +} + +.tone-blue { + --metric-bg: #eaf3ff; + --metric-fg: #0b5bd3; +} + +.tone-cyan { + --metric-bg: #e7f8fb; + --metric-fg: #0891b2; +} + +.tone-purple { + --metric-bg: #f1eafe; + --metric-fg: #7c3aed; +} + +.tone-red { + --metric-bg: #fff0f3; + --metric-fg: #e11d48; +} + +.tone-amber { + --metric-bg: #fff6e9; + --metric-fg: #ea580c; +} + +.dashboard-grid { + display: grid; + grid-template-columns: minmax(360px, 0.96fr) minmax(420px, 1.04fr) minmax(360px, 0.92fr); + gap: 16px; +} + +.panel, +.repository-tree, +.object-hero, +.object-meta-grid, +.tabs, +.object-browser, +.object-detail-card { + border: 1px solid #dbe4f0; + border-radius: 14px; + background: white; + box-shadow: 0 14px 34px rgba(16, 33, 63, 0.05); +} + +.panel { + min-width: 0; + padding: 18px; +} + +.wide-panel { + grid-column: span 2; +} + +.panel-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +.compact-heading { + align-items: center; +} + +.panel-meta { + display: inline-flex; + align-items: center; + gap: 7px; + color: #53657f; + font-size: 12px; + font-weight: 800; +} + +.repo-browser-grid { + display: grid; + grid-template-columns: minmax(300px, 0.85fr) minmax(340px, 1fr) minmax(520px, 1.35fr); + gap: 16px; +} + +.list-panel { + min-height: 640px; +} + +.stack-list { + display: grid; + gap: 8px; + max-height: 600px; + overflow: auto; + padding-right: 4px; +} + +.stack-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; + align-items: center; + width: 100%; + border: 1px solid #e3ebf5; + border-radius: 10px; + background: #ffffff; + color: #122544; + padding: 10px; + text-align: left; +} + +.stack-row.active, +.stack-row:hover { + border-color: #b7d2fb; + background: #eaf3ff; +} + +.stack-row strong, +.stack-row small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stack-row small { + margin-top: 4px; + color: #60718e; + font-size: 12px; +} + +.stack-row em { + color: #0759d7; + font-size: 12px; + font-style: normal; + font-weight: 850; +} + +.objects-live-panel { + min-width: 0; +} + +.uri-cell { + max-width: 330px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-link-button { + border: 1px solid #b7d2fb; + border-radius: 7px; + background: #eaf3ff; + color: #0759d7; + font-size: 12px; + font-weight: 850; + padding: 6px 9px; +} + +.pagination-note, +.empty-state, +.loading-line { + margin-top: 12px; + color: #60718e; + font-size: 13px; +} + +.loading-line { + display: inline-flex; + gap: 8px; + align-items: center; + margin: 0 0 12px; +} + +.loading-line svg { + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.panel-icon { + color: #0b5bd3; +} + +.panel-icon.success { + color: #059669; +} + +.panel-icon.warning { + color: #ea580c; +} + +.donut-wrap { + position: relative; + display: grid; + min-height: 178px; + place-items: center; +} + +.donut { + width: 166px; + height: 166px; + border-radius: 50%; + background: conic-gradient(#0ca678 0 87%, #facc15 87% 96%, #e11d48 96% 100%); + box-shadow: inset 0 0 0 24px white; +} + +.donut-center { + position: absolute; + display: grid; + place-items: center; +} + +.donut-center strong { + color: #07142d; + font-size: 30px; + letter-spacing: -0.05em; +} + +.donut-center span { + color: #53657f; + font-size: 13px; + font-weight: 750; +} + +.legend-list, +.bar-list, +.issue-list, +.check-list, +.detail-list, +.chain-flow { + display: grid; + gap: 12px; +} + +.legend-row, +.bar-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.legend-row span:first-child { + width: 10px; + height: 10px; + border-radius: 999px; +} + +.legend-row label { + flex: 1; + color: #122544; +} + +.bar-meta { + margin-bottom: 7px; + color: #53657f; + font-size: 13px; +} + +.bar-track { + overflow: hidden; + height: 10px; + border-radius: 999px; + background: #e6edf6; +} + +.bar-track span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #0b5bd3, #20b7c9); +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + color: #60718e; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.06em; + text-align: left; + text-transform: uppercase; +} + +td, +th { + border-bottom: 1px solid #e3ebf5; + padding: 13px 10px; +} + +td { + color: #263c59; + font-size: 14px; +} + +.status-badge, +.severity { + display: inline-flex; + align-items: center; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + padding: 5px 9px; + text-transform: capitalize; +} + +.status-badge.healthy, +.status-badge.matched, +.status-badge.current, +.status-badge.valid { + background: #dcfce7; + color: #047857; +} + +.status-badge.warning { + background: #fff7cc; + color: #b45309; +} + +.status-badge.fallback, +.status-badge.cached { + background: #eaf3ff; + color: #0759d7; +} + +.issue-card { + border-bottom: 1px solid #e3ebf5; + padding: 14px 0; +} + +.issue-card:last-child { + border-bottom: 0; +} + +.issue-card div { + display: flex; + align-items: center; + gap: 8px; +} + +.issue-card p { + margin: 9px 0; + color: #122544; +} + +.issue-card code { + display: block; + overflow: hidden; + color: #53657f; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.issue-card small { + display: block; + margin-top: 7px; + color: #60718e; +} + +.severity.high { + background: #ffe5eb; + color: #be123c; +} + +.severity.medium { + background: #fff3dc; + color: #c2410c; +} + +.severity.low { + background: #eaf3ff; + color: #0759d7; +} + +.ghost-button, +.primary-button { + display: inline-flex; + align-items: center; + gap: 8px; + border-radius: 8px; + font-size: 13px; + font-weight: 800; + padding: 9px 12px; +} + +.ghost-button { + border: 1px solid #d2deee; + background: white; + color: #0b5bd3; +} + +.primary-button { + border: 1px solid #0b5bd3; + background: #0b5bd3; + color: white; +} + +.object-page { + padding: 0; +} + +.object-layout { + display: grid; + grid-template-columns: 248px minmax(350px, 1fr) minmax(330px, 0.9fr); + min-height: calc(100vh - 76px); + gap: 0; +} + +.repository-tree { + border-top: 0; + border-bottom: 0; + border-left: 0; + border-radius: 0; + box-shadow: none; + padding: 20px 14px; +} + +.tree-node { + position: relative; + margin-top: 10px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: #263c59; + font-size: 13px; + font-weight: 750; + padding: 9px 10px; +} + +.tree-node.root { + background: #f7faff; +} + +.tree-node.nested { + margin-left: 18px; +} + +.tree-node.current { + border-color: #b7d2fb; + background: #eaf3ff; + color: #0759d7; +} + +.object-picker-list { + display: grid; + gap: 4px; + margin-top: 10px; +} + +.object-picker { + display: block; + width: calc(100% - 18px); + text-align: left; +} + +.object-picker strong, +.object-picker span { + display: block; +} + +.object-picker span { + overflow: hidden; + margin-top: 4px; + color: #60718e; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.object-main { + display: contents; +} + +.object-browser { + overflow: hidden; + border-top: 0; + border-bottom: 0; + border-radius: 0; + box-shadow: none; +} + +.browser-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid #dbe4f0; + padding: 18px; +} + +.browser-toolbar .search-box { + max-width: 240px; + padding: 9px 12px; +} + +.browser-toolbar .object-uri-search { + max-width: 420px; +} + +.filter-pill { + border: 1px solid #d2deee; + border-radius: 8px; + background: white; + color: #53657f; + padding: 9px 12px; +} + +.object-row-active { + background: #eef6ff; + box-shadow: inset 3px 0 0 #0b5bd3; +} + +.object-type { + color: #0b5bd3; + font-weight: 850; +} + +.object-detail-card { + align-self: start; + margin: 14px; + padding: 18px; +} + +.object-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + border: 0; + border-radius: 0; + box-shadow: none; + padding: 0 0 14px; +} + +.object-hero p { + overflow-wrap: anywhere; + color: #53657f; + line-height: 1.55; +} + +.object-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.object-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + border-radius: 8px; + box-shadow: none; + padding: 12px; +} + +.meta-item { + min-width: 0; +} + +.meta-item span { + display: block; + margin-bottom: 5px; + color: #60718e; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.meta-item strong { + display: block; + overflow-wrap: anywhere; + color: #07142d; + font-size: 13px; +} + +.tabs { + display: flex; + gap: 20px; + margin-top: 12px; + border: 0; + border-bottom: 1px solid #dbe4f0; + border-radius: 0; + box-shadow: none; + padding: 0; +} + +.tabs button { + border: 0; + border-bottom: 2px solid transparent; + background: transparent; + color: #53657f; + font-weight: 800; + padding: 12px 0; +} + +.tabs button.active { + border-color: #0b5bd3; + color: #0b5bd3; +} + +.object-content-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + margin-top: 12px; +} + +.prefix-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.prefix-list span { + border-radius: 8px; + background: #eaf3ff; + color: #0759d7; + font-size: 13px; + font-weight: 800; + padding: 8px 10px; +} + +.check-item { + display: flex; + gap: 10px; + border: 1px solid #c9f3dd; + border-radius: 10px; + background: #f0fdf4; + color: #047857; + padding: 10px; +} + +.check-item.warning-check { + border-color: #fed7aa; + background: #fff7ed; + color: #c2410c; +} + +.check-item p { + margin: 4px 0 0; + color: #53657f; + font-size: 13px; + line-height: 1.45; +} + +.chain-flow { + grid-template-columns: repeat(4, minmax(92px, 1fr)); +} + +.live-chain-flow { + grid-template-columns: 1fr; +} + +.chain-node { + position: relative; + border: 1px solid #c9f3dd; + border-radius: 10px; + background: #f0fdf4; + color: #047857; + padding: 10px; +} + +.chain-node strong, +.chain-node span { + display: block; +} + +.chain-node span { + margin-top: 5px; + color: #60718e; + font-size: 12px; + font-weight: 800; +} + +.chain-node code { + display: block; + overflow-wrap: anywhere; + margin-top: 8px; + color: #24466f; + font-size: 11px; +} + +.json-preview { + overflow: auto; + max-height: 360px; + border: 1px solid #dbe4f0; + border-radius: 10px; + background: #f8fbff; + color: #10213f; + font-size: 12px; + line-height: 1.55; + padding: 12px; + white-space: pre-wrap; +} + +.explain-box { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + margin-top: 14px; +} + +.explain-box span { + color: #60718e; + font-size: 13px; +} + +.explain-result { + margin-top: 14px; +} + +.inline-actions { + justify-content: flex-start; + margin-top: 10px; +} + +.workflow-status { + margin-top: 12px; + box-shadow: none; +} + +.workflow-status-list { + display: grid; + gap: 8px; + margin: 0; + padding-left: 18px; + color: #53657f; + font-size: 13px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} + +@media (max-width: 1380px) { + .metric-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .dashboard-grid { + grid-template-columns: 1fr 1fr; + } + + .repo-browser-grid { + grid-template-columns: 1fr; + } + + .issue-panel { + grid-column: span 2; + } +} + +@media (max-width: 1180px) { + .dashboard-grid, + .object-layout { + grid-template-columns: 1fr; + } + + .wide-panel, + .issue-panel { + grid-column: auto; + } + + .repository-tree { + border-right: 0; + border-bottom: 1px solid #dbe4f0; + } +} + +@media (max-width: 900px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + height: auto; + } + + .topbar, + .hero-dashboard, + .topbar-actions { + align-items: stretch; + flex-direction: column; + } + + .metric-grid, + .object-meta-grid, + .chain-flow { + grid-template-columns: 1fr; + } +} diff --git a/ui/rpki-explorer/src/test/fixtures/dashboard.ts b/ui/rpki-explorer/src/test/fixtures/dashboard.ts new file mode 100644 index 0000000..ae10e10 --- /dev/null +++ b/ui/rpki-explorer/src/test/fixtures/dashboard.ts @@ -0,0 +1,112 @@ +export const overviewKpis = [ + { label: "VRPs", value: "948,216", delta: "+12,834 (1.35%)", tone: "blue" }, + { label: "ASPAs", value: "4,812", delta: "+61 (2.95%)", tone: "cyan" }, + { label: "Repositories", value: "1,265", delta: "+8 (0.64%)", tone: "blue" }, + { label: "Publication Points", value: "92,438", delta: "+842 (1.63%)", tone: "purple" }, + { label: "Rejected Objects", value: "1,274", delta: "-31 (2.38%)", tone: "red" }, + { label: "Warnings", value: "3,219", delta: "+116 (3.75%)", tone: "amber" } +]; + +export const validationSlices = [ + { label: "Valid", value: 87, color: "#2563eb" }, + { label: "Warnings", value: 9, color: "#f59e0b" }, + { label: "Rejected", value: 4, color: "#ef4444" } +]; + +export const objectTypeRows = [ + { type: "ROA", count: "741,092", percent: 64 }, + { type: "CER / RC", count: "118,437", percent: 18 }, + { type: "MFT", count: "54,221", percent: 10 }, + { type: "CRL", count: "31,804", percent: 6 }, + { type: "ASPA", count: "4,812", percent: 2 } +]; + +export const topRepositories = [ + { + host: "rpki.apnic.net", + transport: "RRDP", + duration: "12.8s", + objects: "228,192", + status: "healthy" + }, + { + host: "rrdp.arin.net", + transport: "RRDP", + duration: "18.4s", + objects: "214,776", + status: "healthy" + }, + { + host: "rpki.ripe.net", + transport: "RRDP", + duration: "22.1s", + objects: "308,427", + status: "warning" + }, + { + host: "rpki-repo.registro.br", + transport: "RSYNC", + duration: "9.7s", + objects: "19,031", + status: "fallback" + } +]; + +export const validationIssues = [ + { + severity: "high", + type: "MFT", + reason: "Manifest rejected by CRL validation", + uri: "rsync://rpki-repo.registro.br/repo/9mbS.../3kM7.mft", + repo: "rpki-repo.registro.br" + }, + { + severity: "medium", + type: "ROA", + reason: "EE certificate expired", + uri: "rsync://rpki.example.net/repository/AS64496.roa", + repo: "rpki.example.net" + }, + { + severity: "low", + type: "CRL", + reason: "Next update is close", + uri: "rsync://rpki.apnic.net/member_repository/example.crl", + repo: "rpki.apnic.net" + } +]; + +export const objectDetail = { + id: "obj_7d4b_00042", + type: "ROA", + result: "valid", + uri: "rsync://rpki.apnic.net/member_repository/A91ED7F4/AS64500-203.0.113.0-24.roa", + sha256: "7d4b9a0c7f8832d5f6a9807f4371b61cf3131b6fe0fd1a89c9e4eeb7e33b8872", + source: "fresh validated", + repo: "rpki.apnic.net", + pp: "rsync://rpki.apnic.net/member_repository/A91ED7F4/", + parsed: { + asn: "AS64500", + prefixes: ["203.0.113.0/24 maxLength 24", "2001:db8:1200::/48 maxLength 48"], + eeSubject: "CN=AS64500 ROA EE", + validity: "2026-06-15T00:00:00Z → 2026-06-22T00:00:00Z" + }, + chain: [ + { label: "APNIC TAL", state: "trust anchor" }, + { label: "APNIC Root CA", state: "valid CA" }, + { label: "Member CA A91ED7F4", state: "valid CA" }, + { label: "Manifest", state: "current" }, + { label: "ROA object", state: "valid" } + ], + manifestFiles: [ + { name: "AS64500-203.0.113.0-24.roa", hash: "7d4b9a0c…8872", state: "matched" }, + { name: "A91ED7F4.crl", hash: "91ee1db2…af09", state: "current" }, + { name: "A91ED7F4.cer", hash: "3acaa8f0…21aa", state: "valid" } + ], + validation: [ + { label: "CMS signature", status: "passed", note: "Signed attributes and EE chain are valid." }, + { label: "Resource containment", status: "passed", note: "ROA prefixes are covered by parent resources." }, + { label: "Manifest membership", status: "passed", note: "Object hash matches the current manifest entry." }, + { label: "Revocation", status: "passed", note: "EE certificate is not listed in current CRL." } + ] +}; diff --git a/ui/rpki-explorer/tests/e2e/object-detail-api.spec.ts b/ui/rpki-explorer/tests/e2e/object-detail-api.spec.ts new file mode 100644 index 0000000..e0fc7a2 --- /dev/null +++ b/ui/rpki-explorer/tests/e2e/object-detail-api.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; + +test.setTimeout(120_000); + +test("renders live object detail and lazy tab requests", async ({ page }) => { + const apiRequests: string[] = []; + const consoleErrors: string[] = []; + + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.pathname.startsWith("/api/v1")) { + apiRequests.push(`${url.pathname}${url.search}`); + } + }); + page.on("console", (message) => { + if (message.type() === "error") { + consoleErrors.push(message.text()); + } + }); + + await page.goto("/"); + await page.getByRole("button", { name: /Objects/i }).click(); + + await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 }); + await expect(page.getByText("5A179648B3EF2369DCE7BDB58140FF7DC7060ABF.mft").first()).toBeVisible(); + await expect(page.getByText("Final status: valid")).toBeVisible(); + const objectDetail = page.getByRole("complementary", { name: "Live object detail" }); + await expect(objectDetail.getByText("Authoritative", { exact: true })).toBeVisible(); + expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/parsed"))).toBe(false); + + await page.getByRole("tab", { name: "Parsed" }).click(); + await expect(page.getByText("Projection unavailable")).toBeVisible({ timeout: 30_000 }); + expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/parsed"))).toBe(true); + + await page.getByRole("tab", { name: "Chain" }).click(); + await expect(page.getByText("No chain edges recorded for this object.")).toBeVisible({ timeout: 30_000 }); + expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/chain"))).toBe(true); + + await page.getByRole("tab", { name: "Validation" }).click(); + await page.getByRole("button", { name: "Explain validation" }).click(); + await expect(page.getByText("audit_projection")).toBeVisible({ timeout: 30_000 }); + await expect(objectDetail.getByText("false").last()).toBeVisible(); + expect(apiRequests.some((request) => request.includes("/validation/explain"))).toBe(true); + + await page.screenshot({ + path: "../../../../specs/develop/20260617/m5_playwright/rpki-explorer-object-detail-live.png", + fullPage: true + }); + expect(consoleErrors).toEqual([]); +}); diff --git a/ui/rpki-explorer/tests/e2e/overview-api.spec.ts b/ui/rpki-explorer/tests/e2e/overview-api.spec.ts new file mode 100644 index 0000000..a278621 --- /dev/null +++ b/ui/rpki-explorer/tests/e2e/overview-api.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from "@playwright/test"; + +test("renders live overview data from query service without global object list", async ({ page }) => { + const apiRequests: string[] = []; + const consoleErrors: string[] = []; + + page.on("request", (request) => { + const path = new URL(request.url()).pathname; + if (path.startsWith("/api/v1")) { + apiRequests.push(path); + } + }); + page.on("console", (message) => { + if (message.type() === "error") { + consoleErrors.push(message.text()); + } + }); + + await page.goto("/"); + + await expect(page.getByText("run 7144")).toBeVisible(); + await expect(page.getByText("963,779")).toBeVisible(); + await expect(page.getByText("534,760")).toBeVisible(); + await expect(page.getByText("Top repositories by workload")).toBeVisible(); + await expect(page.locator(".issue-card").first().getByText("Reject")).toBeVisible(); + + expect(apiRequests).toContain("/api/v1/latest_run"); + expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/object-types"); + expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/validation"); + expect(apiRequests).toContain("/api/v1/runs/run_7144/stats/reasons"); + expect(apiRequests).toContain("/api/v1/runs/run_7144/repos"); + expect(apiRequests).not.toContain("/api/v1/runs/run_7144/objects"); + expect(consoleErrors).toEqual([]); + + await page.screenshot({ + path: "../../../../specs/develop/20260617/m3_playwright/rpki-explorer-overview-live.png", + fullPage: true + }); +}); diff --git a/ui/rpki-explorer/tests/e2e/repositories-api.spec.ts b/ui/rpki-explorer/tests/e2e/repositories-api.spec.ts new file mode 100644 index 0000000..4fee45a --- /dev/null +++ b/ui/rpki-explorer/tests/e2e/repositories-api.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from "@playwright/test"; + +test.setTimeout(120_000); + +test("loads repository, publication point, and object data on demand", async ({ page }) => { + const apiRequests: string[] = []; + const consoleErrors: string[] = []; + + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.pathname.startsWith("/api/v1")) { + apiRequests.push(`${url.pathname}${url.search}`); + } + }); + page.on("console", (message) => { + if (message.type() === "error") { + consoleErrors.push(message.text()); + } + }); + + await page.goto("/"); + await page.getByRole("button", { name: /Repositories/i }).click(); + + await expect(page.getByRole("heading", { name: "Repository / publication point / object browser" })).toBeVisible(); + await expect(page.getByText("Select a repository to load its publication points.")).toBeVisible(); + expect(apiRequests.some((request) => request.includes("/publication-points"))).toBe(false); + expect(apiRequests.some((request) => request.includes("/objects"))).toBe(false); + + await page.getByRole("button", { name: /sakuya\.nat\.moe/i }).click(); + await expect(page.getByRole("button", { name: /5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/i })).toBeVisible({ timeout: 30_000 }); + expect(apiRequests.some((request) => request.includes("/repos/0490c1fe6e4d4ae5cc354948/publication-points"))).toBe(true); + expect(apiRequests.some((request) => request.includes("/objects"))).toBe(false); + + await page.getByRole("button", { name: /5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/i }).click(); + await expect.poll( + () => apiRequests.some((request) => request.includes("/publication-points/node_10342/objects")), + { timeout: 70_000 } + ).toBe(true); + const objectsPanel = page.getByRole("region", { name: "Objects for publication point" }); + await expect( + objectsPanel.getByText(/rsync:\/\/sakuya\.nat\.moe\/repo\/NATOCA\/1\/5A179648B3EF2369DCE7BDB58140FF7DC7060ABF\.mft/) + ).toBeVisible({ timeout: 70_000 }); + await expect(objectsPanel.getByText("manifest").first()).toBeVisible(); + await expect(page.getByText("limit=50")).toBeVisible(); + expect(consoleErrors).toEqual([]); + + await page.screenshot({ + path: "../../../../specs/develop/20260617/m4_playwright/rpki-explorer-repository-browser.png", + fullPage: true + }); +}); diff --git a/ui/rpki-explorer/tests/e2e/shell.spec.ts b/ui/rpki-explorer/tests/e2e/shell.spec.ts new file mode 100644 index 0000000..3434e27 --- /dev/null +++ b/ui/rpki-explorer/tests/e2e/shell.spec.ts @@ -0,0 +1,56 @@ +import { expect, test } from "@playwright/test"; + +test("opens the RPKI Explorer overview prototype", async ({ page }) => { + const consoleErrors: string[] = []; + page.on("console", (message) => { + if (message.type() === "error") { + const text = message.text(); + if (!text.includes("Failed to load resource")) { + consoleErrors.push(text); + } + } + }); + + await page.goto("/"); + + await expect(page.getByRole("heading", { name: "RPKI Explorer" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Global RPKI validation health" })).toBeVisible(); + await expect(page.getByRole("button", { name: /Overview/i })).toBeVisible(); + await expect(page.getByText("Top repositories by workload")).toBeVisible(); + await expect(page.getByText("Recent validation issues")).toBeVisible(); + await expect(page.getByRole("cell", { name: /RRDP|RSYNC/i }).first()).toBeVisible(); + await page.screenshot({ + path: "../../../../specs/develop/20260617/m2_playwright/rpki-explorer-overview.png", + fullPage: true + }); + expect(consoleErrors).toEqual([]); +}); + +test("opens the RPKI Explorer object detail prototype", async ({ page }) => { + test.setTimeout(90_000); + const consoleErrors: string[] = []; + page.on("console", (message) => { + if (message.type() === "error") { + const text = message.text(); + if (!text.includes("Failed to load resource")) { + consoleErrors.push(text); + } + } + }); + + await page.goto("/"); + await page.getByRole("button", { name: /Objects/i }).click(); + + await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 }); + await expect(page.getByText("Object detail · live query service")).toBeVisible(); + await expect(page.getByText("File and chain checks")).toBeVisible(); + await expect(page.getByRole("tab", { name: "Validation" })).toBeVisible(); + await page.getByRole("tab", { name: "Validation" }).click(); + await expect(page.getByText("Final status: valid")).toBeVisible(); + + await page.screenshot({ + path: "../../../../specs/develop/20260617/m2_playwright/rpki-explorer-object-detail.png", + fullPage: true + }); + expect(consoleErrors).toEqual([]); +}); diff --git a/ui/rpki-explorer/tests/e2e/workflows-api.spec.ts b/ui/rpki-explorer/tests/e2e/workflows-api.spec.ts new file mode 100644 index 0000000..65887f3 --- /dev/null +++ b/ui/rpki-explorer/tests/e2e/workflows-api.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from "@playwright/test"; + +test.setTimeout(120_000); + +test("supports URI search and reports raw/export workflow status", async ({ page }) => { + const apiRequests: string[] = []; + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.pathname.startsWith("/api/v1")) { + apiRequests.push(`${request.method()} ${url.pathname}${url.search}`); + } + }); + + await page.goto("/"); + await page.getByRole("button", { name: /Objects/i }).click(); + await expect(page.getByRole("heading", { name: "MFT object" })).toBeVisible({ timeout: 70_000 }); + + await page.getByRole("button", { name: "Use selected URI" }).click(); + await page.getByPlaceholder("Exact URI lookup...").press("Enter"); + await expect.poll( + () => apiRequests.some((request) => request.includes("/objects/by-uri")), + { timeout: 30_000 } + ).toBe(true); + + await page.getByRole("button", { name: "Download raw" }).click(); + await expect(page.getByText("Raw download failed: repo-bytes db is not configured for raw download")).toBeVisible({ timeout: 30_000 }); + expect(apiRequests.some((request) => request.includes("/objects/ff44545d40ad3732405b46ec/raw"))).toBe(true); + + await page.getByRole("button", { name: "Export object" }).click(); + await expect(page.getByText("Object export failed: repo-bytes db is required for export jobs")).toBeVisible({ timeout: 30_000 }); + expect(apiRequests.some((request) => request.includes("POST /api/v1/runs/run_7144/exports"))).toBe(true); + + await page.getByRole("button", { name: "Export selected PP" }).click(); + await expect(page.getByText("PP export failed: repo-bytes db is required for export jobs")).toBeVisible({ timeout: 30_000 }); + + await page.screenshot({ + path: "../../../../specs/develop/20260617/m6_playwright/rpki-explorer-workflows.png", + fullPage: true + }); +}); diff --git a/ui/rpki-explorer/tsconfig.app.json b/ui/rpki-explorer/tsconfig.app.json new file mode 100644 index 0000000..7a7a882 --- /dev/null +++ b/ui/rpki-explorer/tsconfig.app.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/ui/rpki-explorer/tsconfig.json b/ui/rpki-explorer/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/ui/rpki-explorer/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/ui/rpki-explorer/tsconfig.node.json b/ui/rpki-explorer/tsconfig.node.json new file mode 100644 index 0000000..d5a3fa8 --- /dev/null +++ b/ui/rpki-explorer/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts", "playwright.config.ts", "eslint.config.js"] +} diff --git a/ui/rpki-explorer/vite.config.ts b/ui/rpki-explorer/vite.config.ts new file mode 100644 index 0000000..e02aabd --- /dev/null +++ b/ui/rpki-explorer/vite.config.ts @@ -0,0 +1,29 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig, loadEnv } from "vite"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const queryServiceTarget = env.RPKI_EXPLORER_API_TARGET ?? "http://127.0.0.1:9557"; + + return { + plugins: [react()], + server: { + port: 5173, + strictPort: false, + proxy: { + "/api/v1": { + target: queryServiceTarget, + changeOrigin: true + } + } + }, + preview: { + proxy: { + "/api/v1": { + target: queryServiceTarget, + changeOrigin: true + } + } + } + }; +});