diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 7c605ab..91aa13a 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -42,3 +42,4 @@ jobs: platforms: linux/amd64 push: true tags: git.coopcloud.tech/toolshed/coop-cloud-webui:nightly-${{ env.COMMIT_SHORT_SHA }} + tags: git.coopcloud.tech/toolshed/coop-cloud-webui:nightly diff --git a/.gitea/workflows/tag-build.yaml b/.gitea/workflows/tag-build.yaml index 4192172..e287c94 100644 --- a/.gitea/workflows/tag-build.yaml +++ b/.gitea/workflows/tag-build.yaml @@ -50,4 +50,4 @@ jobs: file: ./Dockerfile platforms: linux/amd64 push: true - tags: git.coopcloud.tech/toolshed/coop-cloud-webui:${{ env.RELEASE_TAG }} + tags: git.coopcloud.tech/toolshed/coop-cloud-webui:${{ env.RELEASE_TAG }} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 818a087..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "web"] - path = web - url = https://git.coopcloud.tech/toolshed/coop-cloud-front.git diff --git a/Dockerfile b/Dockerfile index f1d2aba..a3975fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,17 +3,23 @@ RUN mkdir /backend COPY --parents ["cli", "internal", "go.mod", "go.sum", "app.go", "/backend/"] RUN go build -C /backend -o gobackend -FROM node:slim +FROM git.coopcloud.tech/toolshed/abra@sha256:38de102e0d4cf40558bd354edb46fa9ae2438221742e74366560c1cfa0b4ced7 AS abra + +FROM node:26-bookworm-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + openssh-client \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* RUN mkdir /home/node/wizard +COPY --parents web /home/node/wizard COPY --from=0 /backend/gobackend /home/node/wizard/gobackend USER node -COPY ./command.sh / -RUN curl https://install.abra.coopcloud.tech | bash -ENV ABRA_BIN=/home/node/.local/bin/abra -# RUN $ABRA_BIN recipe fetch -a +COPY --from=abra /abra /usr/local/bin/abra +COPY ./command.sh /command.sh -COPY --parents web /home/node/wizard WORKDIR /home/node/wizard/web USER root RUN npm install . diff --git a/README.md b/README.md index 2f70c32..d413b24 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,18 @@ A Go service that exposes RESTful API endpoints to manage Abra programmatically. Integrates with https://git.coopcloud.tech/toolshed/coop-cloud-front. ## Starting the service with Docker -Build the container: +Pull the container: ```bash -docker build -t coop-cloud-wizard:latest . +docker pull git.coopcloud.tech/toolshed/coop-cloud-webui:nightly ``` - Run the container: ```bash -docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.ssh:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-wizard +docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.ssh:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-webui +``` +Upstream container not working? You can build the container manually: +```bash +docker build -t coop-cloud-webui:latest . ``` - ## Getting started with development - Clone the front end application `git clone https://git.coopcloud.tech/toolshed/coop-cloud-front.git` - Checkout the `dev-nomock` branch `cd coop-cloud-front && git checkout dev-nomock` @@ -20,29 +22,19 @@ docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.ssh:/home/node/.ssh - Start this Go app `go run .` - Navigate to the React App (http://localhost:5173) -> [!WARNING] -> This is an extremely early prototype, only viewing/deploying apps is supported -> and may fail for your local machine - -## Notes: -`api/` is a deprecated path, currently has no function # Roadmap ## General Roadmap: -Currently refactored the backend to be much more Abra CLI reliant. This is ok but is not ideal from an engineering standpoint (probably). -First step is just to polish this backend & frontend, estimate 1 - 2 months to be in a fairly usable state. -Write test suite for the full stack combination. -Then we want to package this app into a dockerized form, easily to deploy and use on one's own server. -Slowly introduce full Abra CLI functionality into app. +TODO Repackaging ## Basic Abra Features | Feature | Status | Follow-up | | ----------- | ----------- | ----------- | | More options for app deploy (esp chaos) | TODO | | -| View and pull in recipes from catalogue | In Progress | | -| Deploy should show deployment status similar to Abra CLI | In Progress | | -| App screen should display services associated with app and their status | TODO | | -| Easily show service logs | In Progress | | -| Modify App config | TODO | | -| Manage updates and rollbacks | TODO | | +| View and pull in recipes from catalogue | Done | Refine Frontend | +| Deploy should show deployment status similar to Abra CLI | Done | Make more responsive and have page render more accurately | +| App screen should display services associated with app and their status | Done | | +| Easily show service logs | Done | More ways to process logs? | +| Modify App config | Done | | +| Manage updates and rollbacks | Done | | | Manage secrets and Abra commands | In Progress | | diff --git a/web b/web deleted file mode 160000 index e09aa9a..0000000 --- a/web +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e09aa9a1cade23adf20710625d7469aa5c4bbd87 diff --git a/web/.env b/web/.env new file mode 100644 index 0000000..a53bee2 --- /dev/null +++ b/web/.env @@ -0,0 +1,3 @@ +VITE_API_URL=http://localhost:3000/api +VITE_WS_URL=ws://localhost:3000 +VITE_MOCK_AUTH=true \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..6a1f366 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +pnpm-lock.yaml +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/.tool-versions b/web/.tool-versions new file mode 100644 index 0000000..7cada80 --- /dev/null +++ b/web/.tool-versions @@ -0,0 +1,2 @@ +pnpm 10.20.0 +nodejs 22.12.0 \ No newline at end of file diff --git a/web/.vite/deps/_metadata.json b/web/.vite/deps/_metadata.json new file mode 100644 index 0000000..988ccbc --- /dev/null +++ b/web/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "97e400e8", + "configHash": "539cfeba", + "lockfileHash": "e3b0c442", + "browserHash": "b743bb36", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/web/.vite/deps/package.json b/web/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/web/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..de431f2 --- /dev/null +++ b/web/README.md @@ -0,0 +1,52 @@ +# Coop Cloud Front + +Frontend for Coop Cloud — a web UI wrapper around the `abra` CLI for +managing VPSs, containers and application deployments. + +This repository contains a Vite + React + TypeScript app styled +with SCSS. + +## Quick start + +- Install dependencies (pnpm is recommended): + +pnpm install +pnpm dev + +``` + +The app uses Vite; start the dev server with `pnpm dev`. + +## Environment + +- The app reads runtime flags from Vite env variables. The default build uses mocked api data. +-To deploy on a real API without mocking, run: +pnpm start:prod + +Modify `.env` file at the project root if you need to override values for +development (see Vite docs for env var conventions). + +## Scripts + +- `pnpm dev` — start dev server +- `pnpm build` — run TypeScript and build production assets +- `pnpm preview` — preview production build locally +- `pnpm lint` — run ESLint +- `pnpm lint:scss` — run Stylelint +- `pnpm format` — format with Prettier + +## Notes about local development + +- The repo contains mock JSON and a `mockApi` service to develop UI without + a backend. Toggle mock mode with `VITE_MOCK_AUTH=true`. +- Routes are client-side using `react-router-dom`. + +## Next steps / suggested work items + +- Add form validation and better UX for `RecipeForm`. +- Improve accessibility. +- Add unit tests. +- Improve responsive layouts and card grid behaviour on narrow screens. +- Add deployment docs and production environment variables. + +--- \ No newline at end of file diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..ad90ba5 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,50 @@ +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'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + + // TypeScript specific + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + '@typescript-eslint/no-explicit-any': 'warn', + + // Import organization + 'sort-imports': ['warn', { + ignoreCase: true, + ignoreDeclarationSort: true, + }], + + // React best practices + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + // Code quality + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'prefer-const': 'warn', + 'no-var': 'error', + }, + }, +); \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..85b717b --- /dev/null +++ b/web/index.html @@ -0,0 +1,17 @@ + + + + + + + coopcloud-frontend + + + + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..66495db --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,5178 @@ +{ + "name": "coopcloud-front", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "coopcloud-front", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^1.47.0", + "niceware": "^4.0.0", + "niceware-ts": "^0.0.5", + "prismjs": "^1.30.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-icons": "^5.6.0", + "react-router-dom": "^7.9.5", + "react-simple-code-editor": "^0.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "sass": "^1.93.3", + "stylelint": "^16.25.0", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz", + "integrity": "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.0", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.0.tgz", + "integrity": "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-search": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", + "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==", + "license": "CC0-1.0" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.3.tgz", + "integrity": "sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.6.0" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.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/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "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/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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/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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "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/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/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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "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/hashery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz", + "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "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" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "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/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "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/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.54.0.tgz", + "integrity": "sha512-kVmnVCBoLztTGe3S76xu8utwxijRLOinknNq9e8btZ0R2fm6WCgxY+weaLiA50odo/yi+apm/4B2A/LsFZhTrw==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/niceware": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/niceware/-/niceware-4.0.0.tgz", + "integrity": "sha512-7uFcxHr5zwNZO4NEMwxK0N+GbqVMGp2YLZ8x/0dx/nxiT0qCt1zL3hZfqawQoq4XiP2OWa665Xid0g/LyoMfQA==", + "license": "MIT", + "dependencies": { + "binary-search": "^1.3.6", + "randombytes": "^2.0.6" + } + }, + "node_modules/niceware-ts": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/niceware-ts/-/niceware-ts-0.0.5.tgz", + "integrity": "sha512-50NqCnl/kT8wABYC8OnXPQxZ4iMGlU0Cf0lbqwkz1rqKpugDU2y/SruWOainL3Pr0ijniSjRAEZlJhQ2jqfWMg==", + "license": "MIT License", + "dependencies": { + "typescript": "^4.1.0" + } + }, + "node_modules/niceware-ts/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/qified": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", + "integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "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.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "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/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-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/stylelint": { + "version": "16.26.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz", + "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-syntax-patches-for-csstree": "^1.0.19", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.1", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-prettier-scss": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-prettier-scss/-/stylelint-config-prettier-scss-1.0.0.tgz", + "integrity": "sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==", + "dev": true, + "license": "MIT", + "bin": { + "stylelint-config-prettier-scss": "bin/check.js", + "stylelint-config-prettier-scss-check": "bin/check.js" + }, + "engines": { + "node": "14.* || 16.* || >= 18" + }, + "peerDependencies": { + "stylelint": ">=15.0.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", + "integrity": "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.23.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-16.0.2.tgz", + "integrity": "sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^17.0.0", + "stylelint-scss": "^6.12.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.24.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-config-standard": { + "version": "39.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-39.0.1.tgz", + "integrity": "sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^17.0.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.23.0" + } + }, + "node_modules/stylelint-config-standard-scss": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-16.0.0.tgz", + "integrity": "sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stylelint-config-recommended-scss": "^16.0.1", + "stylelint-config-standard": "^39.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.23.1" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-scss": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.14.0.tgz", + "integrity": "sha512-ZKmHMZolxeuYsnB+PCYrTpFce0/QWX9i9gh0hPXzp73WjuIMqUpzdQaBCrKoLWh6XtCFSaNDErkMPqdjy1/8aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mdn-data": "^2.25.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.8.2" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", + "integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.20" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.20.tgz", + "integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.2", + "flatted": "^3.3.3", + "hookified": "^1.15.0" + } + }, + "node_modules/stylelint/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/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/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.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..bf3e2ef --- /dev/null +++ b/web/package.json @@ -0,0 +1,49 @@ +{ + "name": "coopcloud-front", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "start:prod": "VITE_MOCK_AUTH=false pnpm start", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx --fix", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix", + "format": "prettier --write 'src/**/*.{ts,tsx,scss,css,json}'", + "format:check": "prettier --check 'src/**/*.{ts,tsx,scss,css,json}'" + }, + "dependencies": { + "lucide-react": "^1.47.0", + "niceware": "^4.0.0", + "niceware-ts": "^0.0.5", + "prismjs": "^1.30.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-icons": "^5.6.0", + "react-router-dom": "^7.9.5", + "react-simple-code-editor": "^0.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "sass": "^1.93.3", + "stylelint": "^16.25.0", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } +} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..6c7d358 --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + '@parcel/watcher': true + esbuild: true + inotify: false diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..6a2cec6 Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/fonts/Lora-Italic.woff2 b/web/public/fonts/Lora-Italic.woff2 new file mode 100644 index 0000000..36bea07 Binary files /dev/null and b/web/public/fonts/Lora-Italic.woff2 differ diff --git a/web/public/fonts/manrope.bold.woff2 b/web/public/fonts/manrope.bold.woff2 new file mode 100644 index 0000000..c3f133c Binary files /dev/null and b/web/public/fonts/manrope.bold.woff2 differ diff --git a/web/public/fonts/manrope.light.woff2 b/web/public/fonts/manrope.light.woff2 new file mode 100644 index 0000000..0efa5d8 Binary files /dev/null and b/web/public/fonts/manrope.light.woff2 differ diff --git a/web/public/fonts/manrope.medium.woff2 b/web/public/fonts/manrope.medium.woff2 new file mode 100644 index 0000000..19b8b23 Binary files /dev/null and b/web/public/fonts/manrope.medium.woff2 differ diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..6d37791 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,28 @@ +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; +import { Dashboard } from './routes/Dashboard/Dashboard'; +import { Apps } from './routes/Apps/Apps'; +import { AppDetail } from './routes/Apps/App'; +import { Servers } from './routes/Servers/Servers'; +import { Server } from './routes/Servers/Server'; +import { Recipes } from './routes/Recipes/Recipes'; + +function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* 404 catch-all */} + } /> + + + ); +} + +export default App; diff --git a/web/src/assets/coopcloud_logo_grey.svg b/web/src/assets/coopcloud_logo_grey.svg new file mode 100644 index 0000000..b55fcb5 --- /dev/null +++ b/web/src/assets/coopcloud_logo_grey.svg @@ -0,0 +1,140 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/scss/_fonts.scss b/web/src/assets/scss/_fonts.scss new file mode 100644 index 0000000..0d5ca7e --- /dev/null +++ b/web/src/assets/scss/_fonts.scss @@ -0,0 +1,31 @@ +@font-face { + font-family: 'Lora'; + src: url('/fonts/Lora-Italic.woff2') format('woff2'); + font-style: italic; + font-weight: 400; + font-display: swap; +} + +@font-face { + font-family: 'Manrope'; + src: url('/fonts/manrope.light.woff2') format('woff2'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Manrope'; + src: url('/fonts/manrope.medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Manrope'; + src: url('/fonts/manrope.bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} diff --git a/web/src/assets/scss/_global.scss b/web/src/assets/scss/_global.scss new file mode 100644 index 0000000..612f936 --- /dev/null +++ b/web/src/assets/scss/_global.scss @@ -0,0 +1,287 @@ +@use './variables' as *; +@use './mixins' as *; + + +body { + margin: 0; + padding: 0; + width: 100%; + min-height: 100vh; +} + +#root { + width: 100%; + min-height: 100vh; +} + +// global page layout styles +.page-wrapper { + min-height: 100vh; + background-color: $bg-secondary; +} + +.page-content { + max-width: 1600px; + margin: 0 auto; + padding: $spacing-2xl $spacing-xl; + + @media (max-width: 768px) { + padding: $spacing-xl $spacing-md; + } +} + +.page-header { + margin-bottom: $spacing-2xl; + + h1 { + font-size: $font-size-3xl; + margin: 0 0 $spacing-sm; + color: $text-primary; + } + + .subtitle { + font-size: $font-size-lg; + color: $text-secondary; + margin: 0; + } +} + +// global state styles +.loading, +.error { + text-align: center; + padding: $spacing-3xl; + font-size: $font-size-lg; +} + +.loading { + color: $text-secondary; +} + +.error { + color: $error; +} + +// Stats grid and cards +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: $spacing-lg; + margin-bottom: $spacing-2xl; +} + +.stat-card { + @include card; + display: flex; + align-items: center; + gap: $spacing-lg; + padding: $spacing-xl; + transition: transform $transition-base, box-shadow $transition-base; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-lg; + } + + // Modifier classes for colored borders + &.upgrade { + border-left: 4px solid $primary-light; + cursor: none; + } + + &.chaos { + border-left: 4px solid $primary-dark; + } + + &.primary { + border-left: 4px solid $primary; + } + + .stat-icon { + font-size: 2rem; + } + + .stat-info { + flex: 1; + + .stat-number { + font-size: $font-size-3xl; + font-weight: $font-weight-bold; + color: $text-primary; + margin: 0; + line-height: 1; + } + + .stat-label { + font-size: $font-size-sm; + color: $text-secondary; + margin: $spacing-xs 0 0; + text-transform: uppercase; + letter-spacing: 0.5px; + } + } +} + +// Filters component +.filters { + @include card; + display: flex; + gap: $spacing-md; + flex-wrap: wrap; + margin-bottom: $spacing-xl; + + @media (max-width: 768px) { + flex-direction: column; + } + + .search-input { + background-color: $bg-primary; + flex: 1; + min-width: 250px; + padding: $spacing-sm $spacing-md; + border: 2px solid $border-color; + border-radius: $radius-md; + font-size: $font-size-base; + transition: border-color $transition-base; + color: $text-primary; + + &::placeholder { + color: $text-muted; + } + + &:focus { + outline: none; + border-color: $primary; + } + } + + select { + padding: $spacing-sm $spacing-md; + border: 2px solid $border-color; + border-radius: $radius-md; + font-size: $font-size-base; + background-color: $bg-primary; + color: $text-primary; + cursor: pointer; + transition: border-color $transition-base; + + &:focus { + outline: none; + border-color: $primary; + } + } + + .checkbox-filter { + display: flex; + align-items: center; + gap: $spacing-sm; + cursor: pointer; + user-select: none; + + input[type="checkbox"] { + cursor: pointer; + width: 18px; + height: 18px; + } + + span { + font-size: $font-size-sm; + color: $text-primary; + } + } +} + +// Status badges +.status-badge { + display: inline-block; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: capitalize; + + &.status-deployed { + background-color: rgba($success, 0.1); + color: $success; + } + + &.status-stopped { + background-color: rgba($text-muted, 0.1); + color: $text-muted; + } + + &.status-error { + background-color: rgba($error, 0.1); + color: $error; + } +} + +// global badge styles +.recipe-badge, +.server-badge { + display: inline-block; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + font-size: $font-size-sm; + font-weight: $font-weight-medium; +} + +.recipe-badge { + background-color: rgba($info, 0.1); + color: $info; +} + +.server-badge { + background-color: rgba($text-secondary, 0.1); + color: $text-secondary; +} + +// Ensure stat-chip shows primary-dark outline on hover and when active +.stat-chip { + &:hover:not(:disabled), + &.active { + border-color: $primary-dark; + box-shadow: 0 0 0 3px rgba($primary-dark, 0.08); + } +} +.filter-chip { + cursor: default; +} + +// Results count +.results-count { + text-align: center; + color: $text-secondary; + font-size: $font-size-sm; + padding: $spacing-md; +} + +// No results message +.no-results { + @include card; + text-align: center; + padding: $spacing-3xl; + color: $text-secondary; + grid-column: 1 / -1; +} + +// Navigation link button (for clickable cards) +.nav-link { + all: unset; + cursor: pointer; + width: 100%; + display: block; + + &:focus-visible { + outline: 2px solid $primary; + outline-offset: 2px; + border-radius: $radius-md; + } +} + +.bland-button { + background: none; + border: none; + padding: 0; + margin: 0; +} diff --git a/web/src/assets/scss/_mixins.scss b/web/src/assets/scss/_mixins.scss new file mode 100644 index 0000000..2653d6a --- /dev/null +++ b/web/src/assets/scss/_mixins.scss @@ -0,0 +1,212 @@ +@use 'variables' as *; + +// Flexbox center +@mixin flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +// Responsive breakpoints +@mixin respond-to($breakpoint) { + @if $breakpoint == 'sm' { + @media (min-width: $breakpoint-sm) { @content; } + } @else if $breakpoint == 'md' { + @media (min-width: $breakpoint-md) { @content; } + } @else if $breakpoint == 'lg' { + @media (min-width: $breakpoint-lg) { @content; } + } @else if $breakpoint == 'xl' { + @media (min-width: $breakpoint-xl) { @content; } + } +} + +// Card style +@mixin card { + background: $bg-primary; + box-shadow: $shadow-md; + padding: $spacing-xl; +} + +// Ensure cards occupy consistent vertical space and can stretch horizontally +@mixin card-dimensions($min-width: 320px, $min-height: 180px) { + min-width: $min-width; + min-height: $min-height; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/// Standard vertical stack for card-style list rows (dashboard recent apps, server detail apps, etc.) +@mixin card-list-vertical($gap: $spacing-md) { + display: flex; + flex-direction: column; + gap: $gap; +} + +/// Hover lift used by dashboard list cards, server grid cards, and similar surfaces +@mixin card-hover-lift($translate-y: -2px, $hover-shadow: $shadow-lg) { + transition: transform $transition-base, box-shadow $transition-base; + + &:hover { + transform: translateY($translate-y); + box-shadow: $hover-shadow; + } +} + +/// Card shell with horizontal scroll (e.g. data tables) +@mixin scrollable-card { + @include card; + overflow-x: auto; +} + +/// Header block inside a card: title area with a bottom rule (server cards, etc.) +@mixin card-header-rule { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: $spacing-lg; + padding-bottom: $spacing-md; + border-bottom: 2px solid $bg-secondary; +} + +/// Primary title + muted monospace/meta line (server cards, resource headers) +@mixin card-title-stack( + $title-size: $font-size-xl, + $title-weight: $font-weight-bold, + $meta-size: $font-size-sm +) { + h3 { + margin: 0 0 $spacing-xs; + font-size: $title-size; + color: $text-primary; + font-weight: $title-weight; + } + + .server-host { + font-size: $meta-size; + color: $text-muted; + font-family: monospace; + } +} + +/// Label + value row inside a card body (server stats, etc.) +@mixin card-stat-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: $spacing-sm 0; + border-bottom: 1px solid $bg-secondary; + + &:last-child { + border-bottom: none; + } +} + +/// Full-width edge bleed for highlighted rows inside padded cards (matches card horizontal padding) +@mixin card-row-highlight-bleed($clear-bottom-border: true) { + padding: $spacing-sm $spacing-md; + margin: $spacing-sm (-$spacing-xl); + padding-left: calc($spacing-xl + $spacing-md); + padding-right: calc($spacing-xl + $spacing-md); + + @if $clear-bottom-border { + border-bottom: none; + } +} + +// Button base +@mixin button-base { + padding: $spacing-sm $spacing-lg; + border: none; + border-radius: $radius-md; + font-weight: $font-weight-semibold; + cursor: pointer; + transition: all $transition-base; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +// Gradient background +@mixin gradient-primary { + background: linear-gradient(135deg, $primary 0%, $primary-light 100%); +} + +// Truncate text +@mixin truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// Status badge +@mixin status-badge($color) { + padding: $spacing-xs $spacing-md; + border-radius: $radius-full; + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + background-color: rgba($color, 0.1); + // color: darken($color, 20%); +} + +// Compact chip used for stat chips and small interactive chips +@mixin chip( + $bg: white, + $border-color: $border-color, + $radius: $radius-md, + $padding: $spacing-md $spacing-lg, + $gap: $spacing-md, + $hover-translate: -2px, + $hover-shadow: $shadow-sm, + $font-size: $font-size-base +) { + display: flex; + align-items: center; + gap: $gap; + padding: $padding; + background: $bg; + border: 2px solid $border-color; + border-radius: $radius; + cursor: pointer; + transition: all $transition-base; + font-size: $font-size; + + &:hover:not(:disabled) { + border-color: $primary; + transform: translateY($hover-translate); + box-shadow: $hover-shadow; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +// Reusable action button styles; accepts border width and radius +@mixin action-btn($border-width: 1px, $radius: $radius-sm, $padding: $spacing-xs $spacing-md, $font-size: $font-size-sm) { + background: none; + border: $border-width solid $border-color; + padding: $padding; + border-radius: $radius; + cursor: pointer; + font-size: $font-size; + color: $text-primary; + font-weight: $font-weight-medium; + transition: all $transition-base; + + &:hover { + background-color: rgba($primary, 0.05); + color: $text-primary; + transform: translateY(-1px); + border-color: $primary; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} \ No newline at end of file diff --git a/web/src/assets/scss/_variables.scss b/web/src/assets/scss/_variables.scss new file mode 100644 index 0000000..a61020f --- /dev/null +++ b/web/src/assets/scss/_variables.scss @@ -0,0 +1,76 @@ +// Colors +$primary: #EFEFEF; +$primary-dark: #6A9CFF; +$primary-light: #ff4f88; +$secondary: #363636; + +$success: #10b981; +$warning: #f59e0b; +$error: #ef4444; +$info: #3b82f6; + +$text-primary: #363636; +$text-secondary: #4a4a4a +; +$text-muted: #999; + +$bg-primary: #ffffff; +$bg-secondary: #f5f5f5; +$bg-tertiary: #e0e0e0; + +$border-color: #e0e0e0; + +// Spacing +$spacing-2xs: 0.1rem; +$spacing-xs: 0.25rem; +$spacing-sm: 0.5rem; +$spacing-md: 1rem; +$spacing-lg: 1.5rem; +$spacing-xl: 2rem; +$spacing-2xl: 3rem; +$spacing-3xl: 4rem; + +// Typography + +$font-family-body: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', +'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', +'Helvetica Neue', sans-serif; + +$font-family-heading: 'Lora', serif; + +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-base: 1rem; +$font-size-lg: 1.125rem; +$font-size-xl: 1.25rem; +$font-size-2xl: 1.5rem; +$font-size-3xl: 2rem; + +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; +$font-weight-bold: 700; + +// Border radius +$radius-sm: 4px; +$radius-md: 6px; +$radius-lg: 8px; +$radius-xl: 12px; +$radius-full: 9999px; + +// Shadows +$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); +$shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1); +$shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); +$shadow-xl: 0 8px 16px rgba(0, 0, 0, 0.2); + +// Transitions +$transition-fast: 0.15s ease; +$transition-base: 0.2s ease; +$transition-slow: 0.3s ease; + +// Breakpoints +$breakpoint-sm: 640px; +$breakpoint-md: 768px; +$breakpoint-lg: 1024px; +$breakpoint-xl: 1280px; \ No newline at end of file diff --git a/web/src/assets/scss/main.scss b/web/src/assets/scss/main.scss new file mode 100644 index 0000000..28c39d1 --- /dev/null +++ b/web/src/assets/scss/main.scss @@ -0,0 +1,7 @@ +// 1. Configuration & helpers (no CSS output) +@use 'variables'; +@use 'mixins'; +@use 'fonts'; + +// 2. Global base styles +@use 'global'; diff --git a/web/src/components/Editor/Editor.tsx b/web/src/components/Editor/Editor.tsx new file mode 100644 index 0000000..8e43ded --- /dev/null +++ b/web/src/components/Editor/Editor.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState, useRef } from "react"; +import { apiService } from '../../services/api'; +import Editor from "react-simple-code-editor"; +import Prism from "prismjs"; + +// import a theme + language +import "prismjs/themes/prism.css"; +import "prismjs/components/prism-javascript"; + +interface EditorProps { + appName: string; +} + +export const EditorPage: React.FC = ({ appName }) => { + const [code, setCode] = useState(""); + useEffect(() => { + document.body.classList.add("modal-open"); + + return () => { + document.body.classList.remove("modal-open"); + }; + }, []); + useEffect(() => { + const fetchCode = async () => { + const config = await apiService.getConfig(appName); + setCode(config); + } + fetchCode(); + }, []); + + const saveFile = async () => { + await apiService.writeConfig(appName, code) + }; + + return ( +
+ setCode(code)} + highlight={code => Prism.highlight(code, Prism.languages.javascript, "javascript")} + padding={10} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 14, + border: "1px solid #ccc", + minHeight: "300px", + }} + /> + +
+ ); +}; \ No newline at end of file diff --git a/web/src/components/Editor/Secrets.scss b/web/src/components/Editor/Secrets.scss new file mode 100644 index 0000000..1d194f9 --- /dev/null +++ b/web/src/components/Editor/Secrets.scss @@ -0,0 +1,150 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; +@use "sass:color"; +.secrets { + color: $text-primary +} +.status { + white-space: nowrap; + min-width: 350px; + .manage-secret { + display: flex; + align-items: center; + gap: $spacing-sm; + &.created { + color: $success; + } + &.not-created { + color: $error; + } + } +} + +.gen-btn { + @include action-btn(1px, $radius-md, $spacing-xs $spacing-md, $font-size-sm); + white-space: nowrap; + &.auto { + + padding: $spacing-xs; + border: 1px solid #d0d7de; + border-radius: 6px; + + background: #fff; + cursor: pointer; + + transition: background-color 0.15s ease; + + &:hover:not(:disabled) { + background: #f3f4f6; + } + } + &.create { + margin-left: auto; + display: flex; + gap: 8px; + background: $success; + color: white; + border-color: $success; + + &:hover:not(:disabled) { + background: color.adjust($success, $lightness: -10%); + } + } + &.modify { + margin-left: auto; + display: flex; + gap: 8px; + background: $warning; + color: white; + border-color: $warning; + + &:hover:not(:disabled) { + background: color.adjust($warning, $lightness: -10%); + } + } + &.cancel { + background: $error; + color: white; + border-color: $error; + + &:hover:not(:disabled) { + background: color.adjust($error, $lightness: -10%); + } + } + +} +.secret-input-wrapper { + position: relative; + display: flex; + align-items: center; + width: 100%; + max-width: 420px; +} +.secret-input { + flex: 1; + min-width: 350px; + padding: $spacing-xs; + padding-right: calc( + 16px + + 16px + + 6px + + 16px + ); + font-size: $font-size-sm; + + border: 1px solid #d0d7de; + border-radius: $radius-md; + + background: #fff; + color: #24292f; + + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.secret-input::placeholder { + color: #8b949e; +} + +.secret-input:focus { + outline: none; + border-color: #2f81f7; + box-shadow: 0 0 0 3px rgba(47, 129, 247, 0.15); +} +.right-icons { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + + display: flex; + align-items: center; + gap: 6px; + + z-index: 1; +} + +.right-icons button { + all: unset; + display: flex; + align-items: center; + justify-content: center; + + width: 16px; + height: 16px; + + border: none; + background: transparent; + cursor: pointer; + + color: #6b7280; + border-radius: 4px; + + transition: background-color 0.15s ease; +} + +.right-icons button:hover { + color: #37393b; +} \ No newline at end of file diff --git a/web/src/components/Editor/Secrets.tsx b/web/src/components/Editor/Secrets.tsx new file mode 100644 index 0000000..3211ef4 --- /dev/null +++ b/web/src/components/Editor/Secrets.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState, useRef } from "react"; +import { apiService } from '../../services/api'; +import type { AbraAppSecret } from '../../types' +import './Secrets.scss'; +import {generatePassphrase} from "niceware-ts"; +import { FaCopy, FaEye, FaEyeSlash, FaDice } from "react-icons/fa"; + + + + +interface SecretProps { + appName: string; +} + +export const SecretsPage: React.FC = ({ appName }) => { + const [secrets, setSecrets] = useState(); + const [editingSecrets, setEditingSecrets] = useState(new Set()); + const [secretValues, setSecretValues] = useState({}); + const [hidden, setHidden] = useState(new Set()); + const [loadingSecrets, setLoadingSecrets] = useState(new Set()); + const [ellipses, setEllipses] = useState(""); + + useEffect(() => { + const interval = setInterval(() => { + setEllipses(prev => { + if (prev.length >= 3) return ""; + return prev + "."; + }); + }, 500); + + return () => clearInterval(interval); + }, []); + + const copyToClipboard = async (value: string) => { + if (!value) return; + await navigator.clipboard.writeText(value); + }; + + useEffect(() => { + document.body.classList.add("modal-open"); + + return () => { + document.body.classList.remove("modal-open"); + }; + }, []); + useEffect(() => { + const fetchSecrets = async () => { + const secretInfo = await apiService.getSecrets(appName); + setSecrets(secretInfo); + } + fetchSecrets(); + }, []); + const generateSecret = (name: string) => { + setSecretValues(prev => ({ + ...prev, + [name]: generatePassphrase(12).join("-"), + })) + }; + const generateOrUpdateSecret = async (name: string, version: string, value: string) => { + // TODO: handle updating secret instead of generating + console.log(appName, name, version, value); + setEditingSecrets(prev => { + const next = new Set(prev); + next.delete(name); + return next; + }); + setSecretValues(prev => { + const next = { ...prev }; + delete next[name]; + return next; + }); + if (hidden.has(name)) { + setHidden(prev => { + const next = new Set(prev); + next.delete(name); + return next; + }); + } + setLoadingSecrets(prev => new Set(prev).add(name)); + try { + await apiService.writeSecret(appName, name, version, value); + const secretInfo = await apiService.getSecrets(appName); + setSecrets(secretInfo); + } catch (err) { + // TODO: better error handling + console.log("Error in setting new secret") + } + setLoadingSecrets(prev => { + const next = new Set(prev); + next.delete(name); + return next; + }); + }; + + return ( +
+ {(secrets == null &&

Fetching Secrets...

)} + {(secrets != null && secrets.length == 0 &&

Found no secrets

)} + {(secrets != null && secrets.length > 0 && +
+ + + + + + + + + + {secrets.map((secret) => ( + + + + + + + ))} + +
Secret Name Version Status
{secret.name}{secret.version} + {loadingSecrets.has(secret.name) &&
modifying secret{ellipses}
} + {!loadingSecrets.has(secret.name) && (
+ {!editingSecrets.has(secret.name) && !loadingSecrets.has(secret.name) && {secret.created ? "created" : "not created"} } + {editingSecrets.has(secret.name) && +
+ + + setSecretValues(prev => ({ + ...prev, + [secret.name]: e.target.value, + })) + } + /> +
+ + +
+
+ } + + + { editingSecrets.has(secret.name) && + + } +
)} +
+ {secret.value} + +
+
)} +
+ ); +}; \ No newline at end of file diff --git a/web/src/components/Header/Header.tsx b/web/src/components/Header/Header.tsx new file mode 100644 index 0000000..3cc8ff2 --- /dev/null +++ b/web/src/components/Header/Header.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import './_Header.scss'; +import logo from '../../assets/coopcloud_logo_grey.svg'; + + +interface HeaderProps { + children: React.ReactNode; +} + +export const Header: React.FC = ({ children }) => { + const navigate = useNavigate(); +return( +
+
+

navigate('/dashboard')} className="logo"> + +

+ +
+
+)} \ No newline at end of file diff --git a/web/src/components/Header/_Header.scss b/web/src/components/Header/_Header.scss new file mode 100644 index 0000000..480d9ec --- /dev/null +++ b/web/src/components/Header/_Header.scss @@ -0,0 +1,83 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; + +.layout-header { + background-color: $primary-light; + color: $text-primary; + padding: $spacing-lg 0; + box-shadow: $shadow-lg; + + .header-content { + max-width: 1400px; + margin: 0 auto; + padding: 0 $spacing-xl; + display: flex; + justify-content: space-between; + align-items: center; + gap: $spacing-xl; + + @media (max-width: 768px) { + flex-direction: column; + gap: $spacing-md; + } + } + + .logo { + height: 100px; + margin: 0; + display: inline-block; + max-width: 100%; + cursor: pointer; + } + + .nav { + display: flex; + gap: $spacing-sm; + flex: 1; + text-align: center; + + @media (max-width: 768px) { + width: 100%; + justify-content: center; + } + } + + .nav-link { + background: none; + border: none; + color: $text-primary; + padding: $spacing-sm $spacing-lg; + cursor: pointer; + border-radius: $radius-md; + transition: all $transition-base; + font-size: $font-size-xl; + font-weight: $font-weight-medium; + position: relative; + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } + + // Active indicator + &.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 80%; + height: 2px; + background-color: $bg-primary; + } + + @media (max-width: 768px) { + padding: $spacing-sm $spacing-md; + font-size: $font-size-xl; + } + } +} \ No newline at end of file diff --git a/web/src/components/Loader/Loader.scss b/web/src/components/Loader/Loader.scss new file mode 100644 index 0000000..7e1e2a7 --- /dev/null +++ b/web/src/components/Loader/Loader.scss @@ -0,0 +1,20 @@ +@use '../../assets/scss/variables' as *; + +.loader { + display: inline-flex; // instead of block + align-items: center; // center SVG inside + line-height: 1; // prevents extra vertical space + svg { + display: block; + } + + rect { + fill: $primary-light; + } + + &--style7 { + rect { + transform-origin: center bottom; + } + } +} \ No newline at end of file diff --git a/web/src/components/Loader/Loader.tsx b/web/src/components/Loader/Loader.tsx new file mode 100644 index 0000000..1352508 --- /dev/null +++ b/web/src/components/Loader/Loader.tsx @@ -0,0 +1,35 @@ +import "./Loader.scss"; + +export const Loader = () => { + return ( +
+ + + + + + + + + + + + + + +
+ ); +}; diff --git a/web/src/components/Spinner/Spinner.scss b/web/src/components/Spinner/Spinner.scss new file mode 100644 index 0000000..6ceb45e --- /dev/null +++ b/web/src/components/Spinner/Spinner.scss @@ -0,0 +1,43 @@ +// magic spinner stuff + +$offset: 187; +$duration: 1.4s; + +.spinner { + margin-top: 10px; + animation: rotator $duration linear infinite; +} + +@keyframes rotator { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(270deg); } +} + +.path { + stroke-dasharray: $offset; + stroke-dashoffset: 0; + transform-origin: center; + animation: + dash $duration ease-in-out infinite, + colors ($duration*4) ease-in-out infinite; +} + +@keyframes colors { + 0% { stroke: #4285F4; } + 25% { stroke: #DE3E35; } + 50% { stroke: #F7C223; } + 75% { stroke: #1B9A59; } + 100% { stroke: #4285F4; } +} + +@keyframes dash { + 0% { stroke-dashoffset: $offset; } + 50% { + stroke-dashoffset: $offset/4; + transform:rotate(135deg); + } + 100% { + stroke-dashoffset: $offset; + transform:rotate(450deg); + } +} \ No newline at end of file diff --git a/web/src/components/Spinner/Spinner.tsx b/web/src/components/Spinner/Spinner.tsx new file mode 100644 index 0000000..c345fe8 --- /dev/null +++ b/web/src/components/Spinner/Spinner.tsx @@ -0,0 +1,9 @@ +import "./Spinner.scss" + +export const Spinner = () => { + return ( + + + + ); +}; diff --git a/web/src/components/Terminal/Terminal.tsx b/web/src/components/Terminal/Terminal.tsx new file mode 100644 index 0000000..9fd1b6a --- /dev/null +++ b/web/src/components/Terminal/Terminal.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useRef, useState } from 'react'; +import type { LogEntry } from '../../services/mockApi'; // Import from mockApi (or api for real) +import './_Terminal.scss'; + +interface TerminalProps { + logs: LogEntry[]; + isActive: boolean; + onClose?: () => void; +} + +export const Terminal: React.FC = ({ logs, isActive, onClose }) => { + const terminalRef = useRef(null); + const indexRef = useRef(0); + const [displayedLogs, setDisplayedLogs] = useState([]); + + // Stream logs in with delays for realistic effect + useEffect(() => { + if (!isActive || logs.length === 0) { + indexRef.current = 0; + setDisplayedLogs([]); + return; + } + + const streamLogs = () => { + if (indexRef.current < logs.length) { + setDisplayedLogs(prev => [...prev, logs[indexRef.current]]); + indexRef.current++; + + // Variable delay based on log type + const delay = logs[indexRef.current - 1]?.type === 'command' ? 200 : + logs[indexRef.current - 1]?.type === 'output' ? 100 : 300; + + setTimeout(streamLogs, delay); + } + }; + + streamLogs(); + }, [logs, isActive]); + // Auto-scroll to bottom + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + } + }, [displayedLogs]); + + if (!isActive && displayedLogs.length === 0) { + return null; + } + + const formatTime = (date: Date) => { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + }; + + return ( +
+
+
+ + + +
+
abra CLI
+
+
+ +
+ {displayedLogs.filter(log => log && log.type).map((log, index) => ( +
+ {log.text} +
+ ))} + {isActive && displayedLogs.length > 0 && ( +
▊
+ )} +
+
+ ); +}; diff --git a/web/src/components/Terminal/_Terminal.scss b/web/src/components/Terminal/_Terminal.scss new file mode 100644 index 0000000..fc1579e --- /dev/null +++ b/web/src/components/Terminal/_Terminal.scss @@ -0,0 +1,204 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; + +.terminal-container { + background: #1e1e1e; + border-radius: $radius-md; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace; + margin-bottom: $spacing-xl; + animation: terminal-appear 0.3s ease-out; +} + +@keyframes terminal-appear { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.terminal-header { + display: flex; + align-items: center; + padding: $spacing-sm $spacing-md; + background: #323232; + border-bottom: 1px solid #404040; + user-select: none; + + .terminal-controls { + display: flex; + gap: $spacing-xs; + min-width: 60px; + } + + .terminal-control { + width: 12px; + height: 12px; + border-radius: 50%; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { + opacity: 0.8; + } + + &.close { + background: #ff5f56; + } + + &.minimize { + background: #ffbd2e; + } + + &.maximize { + background: #27c93f; + } + } + + .terminal-title { + flex: 1; + text-align: center; + color: #a0a0a0; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + } + + .terminal-spacer { + min-width: 60px; + } +} + +.terminal-content { + padding: $spacing-md; + background: #1e1e1e; + color: #d4d4d4; + font-size: 13px; + line-height: 1.6; + max-height: 400px; + overflow-y: auto; + overflow-x: hidden; + + /* Custom scrollbar */ + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: #2d2d2d; + } + + &::-webkit-scrollbar-thumb { + background: #4a4a4a; + border-radius: 4px; + + &:hover { + background: #5a5a5a; + } + } +} + +.terminal-line { + display: flex; + margin-bottom: 2px; + animation: terminal-line-appear 0.2s ease-out; + white-space: pre-wrap; + word-break: break-word; + + @keyframes terminal-line-appear { + from { + opacity: 0; + transform: translateX(-4px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + .terminal-timestamp { + color: #6a6a6a; + margin-right: $spacing-sm; + flex-shrink: 0; + font-size: 11px; + } + + .terminal-text { + flex: 1; + } + + /* Line type styles */ + &.terminal-info { + .terminal-text { + color: #4fc3f7; + } + } + + &.terminal-success { + .terminal-text { + color: #81c784; + font-weight: $font-weight-medium; + } + } + + &.terminal-error { + .terminal-text { + color: #e57373; + font-weight: $font-weight-medium; + } + } + + &.terminal-warning { + .terminal-text { + color: #ffb74d; + } + } + + &.terminal-command { + .terminal-text { + color: #ba68c8; + font-weight: $font-weight-semibold; + } + } + + &.terminal-output { + .terminal-text { + color: #d4d4d4; + opacity: 0.9; + } + } +} + +.terminal-cursor { + display: inline-block; + color: #81c784; + margin-left: 4px; + animation: terminal-cursor-blink 1s step-end infinite; +} + +@keyframes terminal-cursor-blink { + 0%, 50% { + opacity: 1; + } + 51%, 100% { + opacity: 0; + } +} + +/* Responsive */ +@media (max-width: 768px) { + .terminal-content { + font-size: 12px; + padding: $spacing-sm; + } + + .terminal-line { + .terminal-timestamp { + display: none; + } + } +} \ No newline at end of file diff --git a/web/src/index.scss b/web/src/index.scss new file mode 100644 index 0000000..f13b8e4 --- /dev/null +++ b/web/src/index.scss @@ -0,0 +1,93 @@ +@use './assets/scss/variables' as *; +@use './assets/scss/mixins' as *; +@use './assets/scss/global' as *; + +// Global root styles +:root { + line-height: 1.5; + font-weight: 400; + font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', + 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + + color-scheme: light dark; + color: $text-primary; + background-color: $bg-primary; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Global element resets +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +// Global link styles +a { + font-weight: 500; + color: $primary; + text-decoration: none; + transition: color $transition-base; + + &:hover { + color: $primary-light; + } +} + +// Global heading styles +h1, h2, h3, h4, h5, h6 { + color: $text-primary; + margin: 0; +} + +h1 { + font-size: $font-size-3xl; + line-height: 1.1; +} + +h2 { + font-size: $font-size-2xl; + line-height: 1.2; +} + +h3 { + font-size: $font-size-xl; + line-height: 1.3; +} + +// Global button styles +button { + border-radius: $radius-md; + border: 1px solid transparent; + padding: $spacing-sm $spacing-lg; + font-size: $font-size-base; + font-weight: $font-weight-medium; + font-family: inherit; + background-color: $primary; + color: white; + cursor: pointer; + transition: all $transition-base; + + &:hover { + background-color: $primary-light; + } + + &:focus-visible { + outline: 2px solid $primary; + outline-offset: 2px; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..46c0846 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.scss' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/web/src/routes/Apps/App.scss b/web/src/routes/Apps/App.scss new file mode 100644 index 0000000..09ea4eb --- /dev/null +++ b/web/src/routes/Apps/App.scss @@ -0,0 +1,591 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; +@use "sass:color"; + + +.app-detail-page { + @extend .page-wrapper; +} + +.app-detail-content { + @extend .page-content; +} + +// App header section +.app-header { + @include card; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: $spacing-xl; + margin-bottom: $spacing-xl; + + .app-title-section { + flex: 1; + min-width: 300px; + + h1 { + margin: 0 0 $spacing-md 0; + font-size: $font-size-3xl; + color: $text-primary; + font-weight: $font-weight-bold; + } + + .app-meta { + display: flex; + gap: $spacing-sm; + flex-wrap: wrap; + } + } + + .app-actions { + display: flex; + gap: $spacing-md; + flex-wrap: wrap; + } +} + +// Action buttons +.action-btn { + @include action-btn(2px, $radius-md, $spacing-sm $spacing-lg, $font-size-sm); + + &.primary { + background: $primary; + color: white; + border-color: $primary-light; + + &:hover:not(:disabled) { + background: $primary-light; + border-color: $primary-light; + } + } + &.service { + &:hover:not(:disabled) { + background: #3fff5c9d; + + } + } + &.secondary { + background: $bg-secondary; + color: $text-primary; + border-color: $border-color; + } + + &.danger { + background: $error; + color: white; + border-color: $error; + + &:hover:not(:disabled) { + background: color.adjust($error, $lightness: -10%); + } + } + &.modify { + background: $warning; + color: white; + border-color: $warning; + white-space: nowrap; + + &:hover:not(:disabled) { + background: color.adjust($warning, $lightness: -10%); + } + } +} + +.back-button { + @extend .action-btn; + @extend .secondary; +} + +// Content grid layout +.content-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: $spacing-xl; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } +} +.backup-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} +// Info cards +.info-card { + @include card; + margin-bottom: $spacing-xl; + + &:last-child { + margin-bottom: 0; + } + + h2 { + margin: 0 0 $spacing-lg 0; + font-size: $font-size-xl; + color: $text-primary; + font-weight: $font-weight-bold; + } +} + +// service grid +.service-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 2fr)); + gap: $spacing-md; +} + +.service-card { + @include card; + transition: transform $transition-base, box-shadow $transition-base; + position: relative; + overflow: hidden; + padding: $spacing-md; + .service-name { + h3 { + font-size: $font-size-lg; + color: $text-primary; + font-weight: $font-weight-bold; + } + } + .service-row { + flex-grow: 1; + flex: 1; + + .service-value { + font-size: $font-size-sm; + display: -webkit-box; + } + } + &.dark-green { + background-color: #3fff5c; + } + &.green { + background-color: #3fff5c9d; + } + &.red { + background-color: #ff1313; + } + &.gray { + background-color: rgb(187, 187, 187) + } +} + +// Info grid +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: $spacing-lg; +} + +.info-item { + display: flex; + flex-direction: column; + gap: $spacing-xs; + + label { + font-size: $font-size-xs; + color: $text-secondary; + font-weight: $font-weight-medium; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + span { + font-size: $font-size-base; + color: $text-primary; + } + + .no-value { + color: $text-muted; + font-style: italic; + } + + .chaos-active { + background: color.adjust($info, $lightness: -10%); + font-weight: $font-weight-medium; + } +} + +.domain-link { + color: $primary-light; + text-decoration: none; + font-size: $font-size-base; + transition: color $transition-base; + + &:hover { + text-decoration: underline; + } +} + +.server-link { + background: none; + border: none; + color: $primary-light; + cursor: pointer; + padding: 0; + font-size: $font-size-base; + text-align: left; + transition: color $transition-base; + + &:hover { + background: none; + color: $primary-light; + text-decoration: underline; + } +} + +.server-link { + background: none; + border: none; + color: $primary-dark; + cursor: pointer; + padding: 0; + font-size: $font-size-base; + text-align: left; + transition: color $transition-base; + + &:hover { + color: $primary-light; + text-decoration: underline; + } +} + +// Version information +.version-info { + display: flex; + flex-direction: column; + gap: $spacing-lg; + + .version-current { + label { + display: block; + font-size: $font-size-xs; + color: $text-secondary; + font-weight: $font-weight-medium; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: $spacing-sm; + } + + code { + display: inline-block; + background: $bg-secondary; + padding: $spacing-sm $spacing-md; + border-radius: $radius-sm; + font-size: $font-size-sm; + color: $text-primary; + font-family: monospace; + } + } + + .version-upgrades { + label { + display: flex; + align-items: center; + gap: $spacing-sm; + font-size: $font-size-xs; + color: $text-secondary; + font-weight: $font-weight-medium; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: $spacing-md; + + .upgrade-count { + background: $warning; + color: white; + padding: 2px 8px; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + } + } + + .upgrade-list { + display: flex; + flex-direction: column; + gap: $spacing-sm; + } + + .upgrade-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-md; + padding: $spacing-md; + background: $bg-secondary; + border-radius: $radius-md; + border: 2px solid transparent; + transition: border-color $transition-base; + + &:hover { + border-color: $warning; + } + + code { + flex: 1; + font-size: $font-size-sm; + color: $text-primary; + font-family: monospace; + } + + .upgrade-btn { + padding: $spacing-xs $spacing-md; + background: $warning; + color: white; + border: none; + border-radius: $radius-sm; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + cursor: pointer; + transition: all $transition-base; + + &:hover:not(:disabled) { + background: color.adjust($warning, $lightness: -10%); + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + } + } + + .version-latest { + padding: $spacing-md; + background: rgba($success, 0.1); + background: color.adjust($success, $lightness: -10%); + border-radius: $radius-md; + text-align: center; + font-size: $font-size-base; + font-weight: $font-weight-medium; + } +} +.backup-info { + display: grid; + align-items: center; + text-align: left; + grid-template-columns: 2fr 1fr 2fr 100px; + gap: $spacing-md; + +} +.backup-list { + max-height: 250px; + overflow-y: auto; + + .create-btn { + width: 100%; + background: none; + color: $text-secondary; + border: 2px solid $text-muted; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + cursor: pointer; + transition: all $transition-base; + + &:hover:not(:disabled) { + border-color: $warning; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + .backup-item { + display: grid; + align-items: center; + text-align: left; + grid-template-columns: 2fr 1fr 2fr 100px; + gap: $spacing-md; + background: $bg-secondary; + border-radius: $radius-md; + border: 2px solid transparent; + transition: border-color $transition-base; + + &:hover { + border-color: $warning; + } + + code { + font-size: $font-size-sm; + color: $text-primary; + font-family: monospace; + } + .backup-btn { + background: none; + color: $text-secondary; + border: none; + padding: $spacing-sm $spacing-md; + border-radius: $radius-sm; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + cursor: pointer; + transition: all $transition-base; + + &.delete-btn { + color: $error; + } + + &:hover:not(:disabled) { + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + } +} +// Quick actions list +.action-list { + display: flex; + flex-direction: column; + gap: $spacing-sm; + + .action-list-item { + display: flex; + align-items: center; + gap: $spacing-md; + padding: $spacing-md; + background: $bg-secondary; + border: 2px solid transparent; + border-radius: $radius-md; + cursor: pointer; + transition: all $transition-base; + font-size: $font-size-sm; + color: $text-primary; + text-align: left; + width: 100%; + + &:hover:not(:disabled) { + background: white; + border-color: $primary; + transform: translateX(2px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.danger { + &:hover:not(:disabled) { + border-color: $error; + background: rgba($error, 0.05); + } + } + .action-text { + flex: 1; + font-weight: $font-weight-medium; + } + } +} + +// Health status +.health-status { + display: flex; + flex-direction: column; + gap: $spacing-md; + + .health-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: $spacing-md; + border-bottom: 1px solid $border-color; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + .health-label { + font-size: $font-size-sm; + color: $text-secondary; + } + + .health-value { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-primary; + } + } +} + +// editor modal overlay +// locks scrolling +body.modal-open { + overflow: hidden; +} +.editor-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + .secrets { + height: auto; + width: auto; + } +} + +.editor-modal { + width: 90%; + height: 90%; + background: white; + display: flex; + flex-direction: column; + border-radius: 8px; + overflow: hidden; + + .editor-header { + padding: 10px; + border-bottom: 1px solid #ddd; + } + + .editor-body { + flex: 1; + overflow: auto; // 👈 THIS is key + display: flex; + + // make your code editor stretch + > * { + flex: 1; + } + } + .secrets { + height: auto; + width: auto; + } +} + +// navigation back to apps page +.back-navigation { + margin-bottom: 10px; +} +.rollback { + display: flex; + align-items: center; + gap: 8px; +} +.select-input { + padding: 8px 12px; + border: 1px solid #d0d7de; + border-radius: 6px; + background-color: #fff; + color: #24292f; + font-size: 14px; +} diff --git a/web/src/routes/Apps/App.tsx b/web/src/routes/Apps/App.tsx new file mode 100644 index 0000000..80be5e3 --- /dev/null +++ b/web/src/routes/Apps/App.tsx @@ -0,0 +1,760 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { Terminal } from '../../components/Terminal/Terminal'; +import { EditorPage } from '../../components/Editor/Editor'; +import { SecretsPage } from '../../components/Editor/Secrets'; +import { apiService } from '../../services/api'; +import { type AppSnapshot, type AbraApp, type AbraAppService, type AbraServiceState } from '../../types'; +import type { LogEntry } from '../../services/mockApi'; +import { createPortal } from "react-dom"; +import { ArchiveRestore, Download, Trash2, SquarePlus } from "lucide-react"; + +import './App.scss'; + +export const AppDetail: React.FC = () => { + const { server, appName } = useParams<{ server: string; appName: string }>(); + const navigate = useNavigate(); + + const [app, setApp] = useState(null); + const [deployState, setDeployState] = useState("undeployed | deploying | deployed | failed"); + const [serviceState, setServiceState] = useState>({}); + const [services, setServices] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [actionLoading, setActionLoading] = useState(null); + const [openEditor, setOpenEditor] = useState(false); + const [openSecrets, setOpenSecrets] = useState(false); + const [backupEnabled, setBackupEnabled] = useState(false); + const [backupSnapshots, setBackupSnapshots] = useState(); + const [rbVersion, setRbVersion] = useState("Auto"); + + // Terminal state + const [terminalLogs, setTerminalLogs] = useState([]); + const [terminalActive, setTerminalActive] = useState(false); + + // Stream state + const stopRef = useRef void)>(null); + const deployRef = useRef void)>(null); + + // App Information + const [versions, setVersions] = useState([]); + // Use to refresh page + const [refreshKey, setRefreshKey] = useState(0); + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + const getServiceClass = (state: string, status: string) => { + if (state === "running" && status.includes("\(healthy\)")) return "service-card dark-green"; + if (status.includes("unhealthy")) return "service-card red" + if (state === "running") return "service-card green"; + return "service-card gray"; + }; + const getServiceClassDeploying = (state: string, status: string) => { + if (state.includes("converged") && status.includes("\(healthy\)")) return "service-card dark-green"; + if (status.includes("unhealthy")) return "service-card red" + if (state.includes("converged")) return "service-card green"; + return "service-card gray"; + }; + useEffect(() => { + const fetchApp = async () => { + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + const appsData = await mockApiService.getAppsGrouped(); + + const serverApps = appsData[server || '']; + const foundApp = serverApps?.apps.find(a => a.appName === appName); + + if (foundApp) { + setApp(foundApp); + } else { + setError('App not found'); + } + } else { + console.log('fetching app...'); + const appsData = await apiService.getAppsGrouped(); + if (appsData[server || ''].backupEnabled) { + setBackupEnabled(true); + } + const serverApps = appsData[server || '']; + const foundApp = serverApps?.apps.find(a => a.appName === appName); + + if (foundApp) { + setApp(foundApp); + const versionData = await apiService.getAppVersions(foundApp.appName); + if (versionData) { + setVersions(versionData); + } + // when the app is deploying it should handle setting the deploy state itself after success/failure. + if (deployState !== "deploying") { + setDeployState(foundApp.status === "deployed" ? "deployed" : "undeployed"); + } + } else { + setError('App not found'); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load app'); + } finally { + setLoading(false); + } + }; + + fetchApp(); + }, [server, appName, isMockMode, refreshKey]); + // checks status of app containers + useEffect(() => { + let isMounted = true; + const heartbeat = async () => { + while (isMounted) { + if (deployState === "deployed" && appName) { + const services = await apiService.getServices(appName); + if (services) { + setServices(services); + } else { + setServices([]); + } + } + + await new Promise(resolve => setTimeout(resolve, 10000)); + } + }; + heartbeat(); + return () => { + isMounted = false; + }; + }, [deployState, appName]); + useEffect(() => { + const fetchSnapshots = async () => { + if (backupEnabled && app) { + console.log('test'); + // grab backupSnapshots here + const snapshots = await apiService.getBackupList(app.appName); + setBackupSnapshots(snapshots); + console.log(snapshots); + } + } + fetchSnapshots(); + }, [backupEnabled]) + function EditorModal({ onClose }) { + return createPortal( +
+
e.stopPropagation()} + > +
+ +
+ +
+ +
+
+
, + document.body + ); + } + function SecretsModal({ onClose }) { + return createPortal( +
+
e.stopPropagation()} + > +
+ +
+ +
+ +
+
+
, + document.body + ); + } + const waitForLogs = (appName: string) => { + setDeployState("deploying"); + console.log("deploying"); + deployRef.current = apiService.deployLogs(appName, + (update) => { + if (update.type === "service") { + console.log(update.data.name) + const serviceName = update.data.name.slice(appName.length+1) + setServiceState(prev => ({ + ...prev, + [serviceName]: { + ...prev[serviceName] ?? {}, + ...update.data + } + })) + } + if (update.type === "done") { + if (update.data.failed) { + setDeployState('failed'); + } else { + setDeployState("deployed"); + } + setRefreshKey(prev => prev + 1); + } + } + ) + } + // TODO + const handleDeleteSnapshot = async () => {} + const handleDownloadSnapshot = async (appName: string) => { + const a = document.createElement('a') + a.href = `http://localhost:3000/api/apps/${appName}/backup/download` + a.click() + // console.log("downloading...") + // const blob = await apiService.downloadSnapshot(appName) + + // const url = URL.createObjectURL(blob) + + // const a = document.createElement('a') + // a.href = url + // a.download = `${appName}.tar.gz` + // a.click() + + // URL.revokeObjectURL(url) + } + // RESTORE UNTESTED + const handleRestoreSnapshot = async (appName: string, snapshot?: string) => { + apiService.restoreBackup(appName, snapshot) + } + + const handleAction = async (action: string, version?: string) => { + if (!app) return; + + setActionLoading(action); + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + + const onLog = (log: LogEntry) => { + setTerminalLogs(prev => [...prev, log]); + }; + + switch (action) { + case 'deploy': + await mockApiService.deployApp(app.appName, onLog); + break; + case 'stop': + await mockApiService.stopApp(app.appName, onLog); + break; + case 'upgrade': + if (version) { + await mockApiService.upgradeApp(app.appName, version, onLog); + } + break; + case 'remove': + await mockApiService.removeApp(app.appName, onLog); + break; + } + } else { + // Real API calls + switch (action) { + case 'stop': + await apiService.stopApp(app.appName); + setRefreshKey(prev => prev + 1); + break; + case 'upgrade': + await apiService.upgradeApp(app.appName); + waitForLogs(app.appName); + break; + case 'deploy': + await apiService.deployApp(app.appName); + waitForLogs(app.appName); + break; + case 'logs': + setTerminalActive(true); + setTerminalLogs([]); + if (version) { + stopRef.current = apiService.getLogs(app.appName, version, + (line) => { + setTerminalLogs(prev => [...prev, { + type: 'info', + text: `${line}`, + timestamp: new Date() + }]); + } + ) + } + break; + case 'remove': + await apiService.removeApp(app.appName); + setApp(null); + break; + case 'config': + setOpenEditor(true); + break; + case 'secrets': + setOpenSecrets(true); + break; + case 'rollback': + if (version) { + console.log(version); + apiService.rollbackApp(app.appName, version); + waitForLogs(app.appName); + } else { + console.log("no version for rollback"); + } + } + } + } catch (err) { + console.error('Action failed:', err); + setTerminalLogs(prev => [...prev, { + type: 'error', + text: `❌ Error: ${err instanceof Error ? err.message : 'Action failed'}`, + timestamp: new Date() + }]); + } finally { + setActionLoading(null); + } + }; + + if (loading) { + return ( +
+
+
+
+ +
Loading application
+
+
+
+ ); + } + + if (error || !app) { + return ( +
+
+
+
{error || 'App not found'}
+ +
+
+ ); + } + // TODO: make sure this makes sense when app.upgrade is unknown + const upgradeVersions = (app.upgrade !== 'latest' && app.upgrade !== 'unknown') ? app.upgrade.split('\n') : []; + + return ( +
+ {openEditor && ( + setOpenEditor(false)} /> + )} + {openSecrets && ( + setOpenSecrets(false)} /> + )} +
+
+
+ +
+ +
+
+

{app.appName}

+
+ {app.recipe} + {app.status} + {app.chaos === 'true' && ( + Chaos + )} +
+
+ +
+ + +
+
+ + {/* Terminal Component */} + { + stopRef.current?.(); + stopRef.current = null; + setTerminalActive(false) + }} + /> + +
+ {/* Left Column - Main Info */} +
+
+

Application Details

+ +
+
+ + {app.appName} +
+ +
+ + {app.domain ? ( + + {app.domain} ↗ + + ) : ( + No domain configured + )} +
+ +
+ + + {app.server} ↗ + +
+ +
+ + {app.recipe} +
+ +
+ + + {app.status} + +
+ +
+ + + {app.chaos === 'true' ? '☠️ Enabled' : 'Disabled'} + +
+
+
+ {(deployState === "deployed" || deployState === "failed") && ( +
+

Service Information

+
+ {services === undefined || services?.length === 0 ? ( +
Loading services...
+ ) : ( + services.map((service) => ( +
+
+

{service.service}

+
+
+ {service.state} + {service.status} +
+ + +
+ )) + )} +
+
)} + {(deployState === "deploying") && ( +
+

Service Information

+
+ {serviceState === undefined || Object.keys(serviceState).length === 0 ? ( +
Preparing services...
+ ) : ( + Object.keys(serviceState).map((name) => ( +
+
+

{name}

+
+
+ {serviceState[name].status} + {serviceState[name].health} +
+ + +
+ )) + )} +
+
)} +
+

Version Information

+ +
+
+ + {app.version} +
+ + {app.chaosVersion !== 'unknown' && ( +
+ + {app.chaosVersion} +
+ )} + + {upgradeVersions.length > 0 && ( +
+ +
+ {upgradeVersions.map((version, idx) => ( +
+ {version} + +
+ ))} +
+
+ )} + + {app.upgrade === 'unknown' && ( +
+ +
+
+ None +
+
+
+ )} + {app.upgrade === 'latest' && ( +
+ ✓ Running latest version +
+ )} +
+ + +
+
+
+
+ {backupEnabled && ( +
+
+

Backups

+ + backupbot deployed + +
+ +
+ Time Created + Snapshot ID + Version + +
+
+ + {backupSnapshots?.map((snapshot, idx) => ( +
+ {snapshot.createdAt} + {snapshot.snapshot} + {snapshot.version} +
+ + + { /* Not convinced this is necesary... + */ } +
+
+ ))} +
+
+ )} +
+
+ + {/* Right Column - Actions & Stats */} +
+
+

Quick Actions

+ +
+ + + { /* TODO: make upgrade have force option*/} + + + + + + + +
+
+
+

Health Status

+
+
+ CPU Usage + — +
+
+ Memory + — +
+
+ Uptime + — +
+
+
+
+
+
+
+ ); +}; diff --git a/web/src/routes/Apps/Apps.scss b/web/src/routes/Apps/Apps.scss new file mode 100644 index 0000000..ead1337 --- /dev/null +++ b/web/src/routes/Apps/Apps.scss @@ -0,0 +1,167 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; + +// Extend global page wrapper +.apps-page { + @extend .page-wrapper; +} + +.apps-content { + @extend .page-content; +} + +// Compact stats row +.stats-row { + display: flex; + align-items: center; + gap: $spacing-md; + margin-bottom: $spacing-xl; + flex-wrap: wrap; +} + +.stat-chip { + @include chip(); + + &.active { + border-color: $primary-dark; + background: $bg-primary; + box-shadow: 0 0 0 3px rgba($primary-dark, 0.08); + } + + .stat-label { + color: $text-secondary; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + } + + .stat-value { + color: $text-primary; + font-size: $font-size-xl; + font-weight: $font-weight-bold; + min-width: 24px; + text-align: center; + } +} + +/* reset-filters-btn removed — outline on stat-chip indicates active filters */ + +// Apps table specific styles +.apps-table-container { + @include scrollable-card; + margin-bottom: $spacing-lg; +} + +.apps-table { + width: 100%; + border-collapse: collapse; + + thead { + background-color: $bg-tertiary; + + th { + padding: $spacing-md $spacing-lg; + text-align: left; + font-weight: $font-weight-semibold; + color: $text-primary; + font-size: $font-size-sm; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid $border-color; + } + } + + tbody { + tr { + border-bottom: 1px solid $border-color; + transition: background-color $transition-base; + + &:hover { + background-color: rgba($primary, 0.05); + } + + &:last-child { + border-bottom: none; + } + } + + td { + padding: $spacing-md $spacing-lg; + font-size: $font-size-base; + color: $text-primary; + + &.no-results { + text-align: center; + padding: $spacing-3xl; + color: $text-secondary; + } + } + } +} + +// App table cell styles +.app-name-cell { + .app-name { + font-weight: $font-weight-medium; + color: $text-primary; + } + .domain-link { + color: $primary-dark; + text-decoration: none; + transition: color $transition-base; + + &:hover { + color: $primary-light; + text-decoration: underline; + } + } +} + + +.no-domain { + color: $text-muted; + font-style: italic; +} + +.version-cell { + display: flex; + align-items: center; + gap: $spacing-sm; + + .version { + font-family: monospace; + font-size: $font-size-sm; + color: $text-primary; + } + + .chaos-badge, + .upgrade-available { + font-size: $font-size-base; + } +} + +// Action buttons +.actions { + display: flex; + gap: $spacing-sm; + + .action-btn { + @include action-btn(1px, $radius-sm, $spacing-xs $spacing-md, $font-size-sm); + + &:hover { + background-color: $primary; + color: white; + border-color: $primary; + } + + &.upgrade { + border-color: $warning; + color: $warning; + + &:hover { + background-color: $warning; + color: white; + } + } + } +} \ No newline at end of file diff --git a/web/src/routes/Apps/Apps.tsx b/web/src/routes/Apps/Apps.tsx new file mode 100644 index 0000000..8c44776 --- /dev/null +++ b/web/src/routes/Apps/Apps.tsx @@ -0,0 +1,289 @@ + +// TODOS: +// make the two filters non-exlusive + + +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { apiService } from '../../services/api'; +import type { AbraApp, AppWithServer, ServerAppsResponse } from '../../types'; +import './Apps.scss'; + +export const Apps: React.FC = () => { + const navigate = useNavigate(); + const [appsData, setAppsData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [filterServer, setFilterServer] = useState('all'); + const [showUpgradesOnly, setShowUpgradesOnly] = useState(false); + const [showChaosOnly, setShowChaosOnly] = useState(false); + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + + useEffect(() => { + const fetchData = async () => { + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + const data = await mockApiService.getAppsGrouped(); + setAppsData(data); + } else { + const data = await apiService.getAppsGrouped(); + setAppsData(data); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load apps'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [isMockMode]); + + // Flatten and enrich apps data + const allApps: AppWithServer[] = useMemo(() => { + if (!appsData) return []; + + return Object.entries(appsData).flatMap(([serverName, serverData]) => + serverData.apps.map(app => ({ + ...app, + serverStats: { + appCount: serverData.appCount, + upgradeCount: serverData.upgradeCount, + }, + })) + ); + }, [appsData]); + + // Get unique servers + const servers = useMemo(() => { + if (!appsData) return []; + return Object.keys(appsData); + }, [appsData]); + + // Filter apps (additive filters: upgrades AND chaos can be applied together) + const filteredApps = useMemo(() => { + return allApps.filter(app => { + const matchesSearch = + (app.appName || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (app.recipe || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (app.domain || '').toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesServer = filterServer === 'all' || app.server === filterServer; + const matchesChaos = !showChaosOnly || app.chaos === 'true'; + const matchesUpgrade = !showUpgradesOnly || app.upgrade !== 'latest'; + + return matchesSearch && matchesServer && matchesChaos && matchesUpgrade; + }); + }, [allApps, searchTerm, filterServer, showUpgradesOnly, showChaosOnly]); + + const stats = useMemo(() => { + const total = allApps.length; + const needsUpgrade = allApps.filter(app => app.upgrade !== 'latest').length; + const chaosApps = allApps.filter(app => app.chaos === 'true').length; + const totalServers = servers.length; + + return { total, needsUpgrade, chaosApps, totalServers }; + }, [allApps, servers]); + + const toggleUpgrades = () => setShowUpgradesOnly(prev => !prev); + const toggleChaos = () => setShowChaosOnly(prev => !prev); + + if (loading) { + return ( +
+
+
+
+ +
Loading apps
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
Error: {error}
+
+
+ ); + } + + return ( +
+
+
+
+

Applications

+

{stats.total} apps across {stats.totalServers} servers

+
+ + {/* Compact Stats Overview */} +
+ + + + + + +
+ + {/* Filters */} +
+ setSearchTerm(e.target.value)} + className="search-input" + /> + + +
+ + {/* Apps Table */} +
+ + + + + + + + + + {/* */} + + + + {filteredApps.length === 0 ? ( + + + + ) : ( + filteredApps.map((app) => ( + navigate(`/apps/${app.server}/${app.appName}`)} + style={{ cursor: 'pointer' }} + > + + + + + + + {/* */} + + )) + )} + +
App NameRecipeDomainServerVersionStatusActions
+ No apps found matching your filters +
+ {app.appName} + + {app.recipe} + + {app.domain ? ( + e.stopPropagation()} + > + {app.domain} + + ) : ( + — + )} + + {app.server} + +
+ {app.version} + {app.chaos === 'true' && ( + + )} + {app.upgrade !== 'latest' && ( + + + )} +
+
+ + {app.status} + + +
+ + {app.upgrade !== 'latest' && ( + + )} +
+
+
+ +
+ Showing {filteredApps.length} of {allApps.length} apps +
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/routes/Dashboard/Dashboard.tsx b/web/src/routes/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..0294c34 --- /dev/null +++ b/web/src/routes/Dashboard/Dashboard.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { useNavigate } from 'react-router-dom'; +import { apiService } from '../../services/api'; +import type { AbraApp, AbraServer } from '../../types'; +import './_Dashboard.scss'; + +export const Dashboard: React.FC = () => { + const [apps, setApps] = useState([]); + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + + useEffect(() => { + const fetchData = async () => { + try { + if (isMockMode) { + // Use mock API in development + const { mockApiService } = await import('../../services/mockApi'); + const [appsData, serversData] = await Promise.all([ + mockApiService.getAppsGrouped(), + mockApiService.getServers(), + ]); + + // Flatten the grouped apps data + const flatApps = Object.values(appsData).flatMap(serverData => serverData.apps); + setApps(flatApps); + setServers(serversData); + } else { + // Use real API in production + const [appsData, serversData] = await Promise.all([ + apiService.getAppsGrouped(), + apiService.getServers(), + ]); + + // Flatten the grouped apps data + const flatApps = Object.values(appsData).flatMap(serverData => serverData.apps); + setApps(flatApps); + setServers(serversData); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [isMockMode]); + + // Calculate stats + const deployedAppsCount = apps.filter(a => a.status === 'deployed').length; + const serversWithAppsCount = new Set(apps.map(a => a.server)).size; + + if (loading) { + return ( +
+
+
+
+ +
Loading dashboard
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
Error: {error}
+
+
+ ); + } + + return ( +
+
+
+
+

Dashboard

+
+ +
+ + + +
+ +
+

Recent Applications

+
+ {apps.slice(0, 5).map((app, index) => ( +
navigate(`/apps/${app.server}/${app.appName}`)} + > +
+

{app.appName}

+

{app.domain || 'No domain'}

+

{app.server}

+
+ + {app.status} + +
+ ))} +
+
+
+
+ ); +}; diff --git a/web/src/routes/Dashboard/_Dashboard.scss b/web/src/routes/Dashboard/_Dashboard.scss new file mode 100644 index 0000000..dbbc5dc --- /dev/null +++ b/web/src/routes/Dashboard/_Dashboard.scss @@ -0,0 +1,59 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; + +// Extend global page wrapper +.dashboard-page { + @extend .page-wrapper; +} + +.dashboard-content { + @extend .page-content; +} + +// Dashboard-specific styles +.recent-apps { + margin-bottom: $spacing-2xl; + + h3 { + font-size: $font-size-2xl; + margin: 0 0 $spacing-lg; + color: $text-primary; + } +} + +.apps-list { + @include card-list-vertical; +} + +.app-item { + @include card; + @include card-hover-lift(-2px, $shadow-lg); + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + .app-info { + flex: 1; + + h4 { + font-size: $font-size-lg; + color: $text-primary; + font-weight: $font-weight-semibold; + } + + .app-domain { + margin: 0 0 $spacing-xs; + color: $text-muted; + font-size: $font-size-sm; + } + + .app-server { + margin: 0; + font-size: $font-size-sm; + color: $text-secondary; + font-family: monospace; + } + } +} diff --git a/web/src/routes/Recipes/Recipe.scss b/web/src/routes/Recipes/Recipe.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/src/routes/Recipes/Recipe.tsx b/web/src/routes/Recipes/Recipe.tsx new file mode 100644 index 0000000..e69de29 diff --git a/web/src/routes/Recipes/RecipeForm.scss b/web/src/routes/Recipes/RecipeForm.scss new file mode 100644 index 0000000..be60cbb --- /dev/null +++ b/web/src/routes/Recipes/RecipeForm.scss @@ -0,0 +1,159 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; + +.recipe-form { + @include card; + max-width: 680px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: $spacing-lg; + padding: $spacing-lg; + + h2 { + margin: 0; + font-size: $font-size-xl; + color: $text-primary; + } + + .field { + display: flex; + flex-direction: column; + gap: $spacing-xs; + + &.field-inline { + flex-direction: row; + align-items: center; + gap: $spacing-md; + } + + label { + color: $text-secondary; + font-size: $font-size-sm; + display: flex; + flex-direction: column; + gap: $spacing-xs; + + input[type="text"], + input[type="email"], + input:not([type]), + select { + padding: $spacing-sm $spacing-md; + border: 2px solid $border-color; + border-radius: $radius-md; + font-size: $font-size-base; + background: $bg-primary; + color: $text-primary; + transition: border-color $transition-base, box-shadow $transition-base; + } + + input:focus, + select:focus { + outline: none; + border-color: $primary; + box-shadow: 0 0 0 4px rgba($primary, 0.06); + } + } + } + + .form-actions { + display: flex; + gap: $spacing-sm; + justify-content: flex-end; + + .action-btn { + @include action-btn(2px, $radius-md, $spacing-sm $spacing-lg, $font-size-sm); + } + } +} + +.checkbox-label { + display: inline-flex; + align-items: center; + gap: $spacing-sm; + font-size: $font-size-base; + color: $text-primary; + + input[type="checkbox"] { + width: 18px; + height: 18px; + } +} + +.form-subtitle { + margin: 0; + color: $text-secondary; + font-size: $font-size-sm; +} + +.form-error { + background: rgba($error, 0.08); + color: $error; + padding: $spacing-sm $spacing-md; + border-radius: $radius-sm; +} + +.select-input { + width: 100%; +} + +.modal { + background: white; + position: relative; + padding: 24px; + border-radius: 8px; + width: min(100%, 800px); + max-height: 90vh; +} + +.modal .close-btn { + position: absolute; + top: 10px; + right: 10px; + + cursor: pointer; + font-size: 20px; + line-height: 1; + + padding: 4px 8px; + border-radius: 4px; + background: rgba(255, 0, 0, 0.308); +} + +.modal .close-btn:hover { + background: rgba(255, 0, 0, 0.797); +} + +.modal .close-btn:focus { + outline: 2px solid #888; +} + +.secrets table { + width: 90%; + border-collapse: collapse; + margin-top: 10px; +} + +.secrets th, +.secrets td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.secrets th { + background-color: #f5f5f5; + font-weight: bold; +} + +.secrets tr:nth-child(even) { + background-color: #fafafa; +} + +.copy-btn { + margin-left: 10px; + padding: 2px 6px; + font-size: 12px; + cursor: pointer; +} diff --git a/web/src/routes/Recipes/RecipeForm.tsx b/web/src/routes/Recipes/RecipeForm.tsx new file mode 100644 index 0000000..edb1f65 --- /dev/null +++ b/web/src/routes/Recipes/RecipeForm.tsx @@ -0,0 +1,158 @@ +import { useState, useEffect } from "react"; +import { apiService } from '../../services/api'; +import type { AbraServer, AbraAppSecret } from '../../types'; +import './RecipeForm.scss'; + + +function RecipeForm({ recipe, onClose }) { + const [loading, setLoading] = useState(true); + const [submitted, setSubmitted] = useState(false); + const [servers, setServers] = useState([]); + const [secretData, setSecretData] = useState([]); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + if (servers.length === 0) { + const [serversData] = await Promise.all([ + apiService.getServers(), + ]); + + setServers(serversData); + } + + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load servers'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }); + const [formData, setFormData] = useState({ + server: "", + domain: "", + chaos: false, + secrets: false, + }); + + const handleChange = (e) => { + const {name, type, value, checked} = e.target; + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })); + console.log(formData); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + console.log("Submitting:", formData); + setSubmitted(true); + const generatedSecrets = await apiService.newApp(recipe.name, formData); + setSecretData(generatedSecrets); + }; + + return ( +
+ + {(secretData.length === 0 && submitted) && ( +

Creating new app...

+ ) + } + {(secretData.length > 0 && submitted) && ( +
+ + + + + + + + + {secretData.map((secret) => ( + + + + + ))} + +
NameValue
{secret.name} + {secret.value} + +
+
+ )} + {(submitted === false) && +
+

{recipe.name}

+

Configure and deploy this recipe.

+ { error &&
{error}
} + { loading ? (

Loading servers...

+ ) : ( +
+ +
+ )} +
+ +
+
+ +
+ +
+ +
+ + +
+ } +
+ ); +} + +export default RecipeForm; \ No newline at end of file diff --git a/web/src/routes/Recipes/Recipes.scss b/web/src/routes/Recipes/Recipes.scss new file mode 100644 index 0000000..0847379 --- /dev/null +++ b/web/src/routes/Recipes/Recipes.scss @@ -0,0 +1,154 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; + +// Extend global page wrapper +.recipes-page { + @extend .page-wrapper; +} + +.recipes-content { + @extend .page-content; +} + +// Recipes grid +.recipes-grid { + display: grid; + justify-content: stretch; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: $spacing-xl; + margin-bottom: $spacing-xl; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +} + +// Recipe card +.recipe-card { + @include card; + @include card-dimensions(320px, 180px); + row-gap: 1em; + grid-template-rows: 1fr 2fr auto 0.9fr; + transition: transform $transition-base, box-shadow $transition-base; + position: relative; + overflow: hidden; + max-width: 30em; + + .recipe-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: $spacing-lg; + padding-bottom: $spacing-md; + border-bottom: 2px solid $bg-secondary; + + .recipe-title { + h3 { + margin: 0 0 $spacing-xs; + font-size: $font-size-xl; + color: $text-primary; + font-weight: $font-weight-bold; + } + + .recipe-host { + font-size: $font-size-sm; + color: $text-muted; + font-family: monospace; + } + } + img { + width: clamp(20px, 3vw, 32px); + height: clamp(20px, 3vw, 32px); + object-fit: contain; + } + } + .card-tags { + display: flex; + gap: 6px; + margin-top: $spacing-sm; + height: $spacing-md; + flex-wrap: wrap; + } + .tag-category { + font-size: 12px; + padding: 2px 8px; + border-radius: 5px; + color: rgb(255, 255, 255); + font-weight: 700; + background-color: rgb(111, 128, 255); + + } + .tag-feature { + font-size: 12px; + padding: 2px 8px; + border-radius: 5px; + color: rgb(255, 255, 255); + font-weight: 700; + background-color: rgb(22, 180, 22); + + } + .recipe-stats { + flex-grow: 1; + flex: 1; + margin-bottom: $spacing-lg; + + .stat-row { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + + } + } + + .recipe-actions { + display: flex; + gap: $spacing-sm; + .action-btn { + flex: 1; + @include action-btn(2px, $radius-md, $spacing-sm $spacing-md, $font-size-sm); + + &.primary { + background-color: $primary; + color: white; + border-color: $primary; + + &:hover { + background-color: $primary-light; + border-color: $primary-light; + } + } + } + } + + .recipe-alert { + background-color: rgba($warning, 0.1); + border-left: 4px solid $warning; + padding: $spacing-sm $spacing-md; + margin: 0 (-$spacing-xl) (-$spacing-xl); + display: flex; + align-items: center; + gap: $spacing-sm; + + .alert-icon { + font-size: $font-size-lg; + color: $warning; + } + + .alert-text { + font-size: $font-size-sm; + color: $text-primary; + font-weight: $font-weight-medium; + } + } +} +.modal-overlay { +position: fixed; +inset: 0; +background: rgba(0, 0, 0, 0.4); +display: flex; +justify-content: center; +align-items: center; +} diff --git a/web/src/routes/Recipes/Recipes.tsx b/web/src/routes/Recipes/Recipes.tsx new file mode 100644 index 0000000..eb91d33 --- /dev/null +++ b/web/src/routes/Recipes/Recipes.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { apiService } from '../../services/api'; +import type { AbraApp, AbraRecipe, AppWithServer } from '../../types'; +import RecipeForm from './RecipeForm.tsx' +import './Recipes.scss'; + +export const Recipes: React.FC = () => { + const navigate = useNavigate(); + const [recipesData, setRecipesData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [filterServer, setFilterServer] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); + const [selectedRecipe, setSelectedRecipe] = useState(null); + const [showForm, setShowForm] = useState(false); + + + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + + useEffect(() => { + const fetchData = async () => { + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + const data = await mockApiService.getRecipes(); + console.log(data) + setRecipesData(data); + } else { + const data = await apiService.getRecipes(); + setRecipesData(data); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load apps'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [isMockMode]); + + // Flatten and enrich apps data + const allRecipes: AbraRecipe[] = useMemo(() => { + if (!recipesData) return []; + + return recipesData; + }, [recipesData]); + + + // Filter recipes + const filteredRecipes = useMemo(() => { + return allRecipes.filter(recipe => { + const matchesSearch = + recipe.name.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesSearch; + }); + }, [allRecipes, searchTerm, filterServer, filterStatus]); + + const stats = useMemo(() => { + const total = allRecipes.length; + + return { total }; + }, [allRecipes]); + + if (loading) { + return ( +
+
+
+
+ +
Loading recipes
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
Error: {error}
+
+
+ ); + } + + return ( +
+
+
+
+

Recipes

+

{stats.total} recipes

+
+ + {/* Filters */} +
+ setSearchTerm(e.target.value)} + className="search-input" + /> + +
+ + {/* Server Cards */} +
+ {filteredRecipes.length === 0 ? ( +
No recipes found matching your search
+ ) : ( + filteredRecipes.map((recipe) => ( +
+
+
+

{recipe.name}

+
+
+ {recipe.icon.length > 0 ? : null } +
+
+ +
+
+ {recipe.description} +
+
+ +
+ +
+
+ {recipe.features.backups.toLowerCase().includes("yes") ? Backups : null} + {recipe.features.healthcheck.toLowerCase().includes("yes") ? Healthcheck : null} + {recipe.category.length > 0 ? {recipe.category} : null} +
+
+ )) + )} +
+ {selectedRecipe && ( +
setSelectedRecipe(null)}> +
e.stopPropagation()}> + {} + setSelectedRecipe(null)} /> +
+
+ )} + +
+ Showing {filteredRecipes.length} of {allRecipes.length} apps +
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/routes/Servers/Server.scss b/web/src/routes/Servers/Server.scss new file mode 100644 index 0000000..ea53d47 --- /dev/null +++ b/web/src/routes/Servers/Server.scss @@ -0,0 +1,340 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; +@use "sass:color"; + +.server-detail-page { + @extend .page-wrapper; +} + +.server-detail-content { + @extend .page-content; +} + +// Server header section +.server-header { + @include card; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: $spacing-xl; + margin-bottom: $spacing-xl; + + .server-title-section { + flex: 1; + min-width: 300px; + + h1 { + margin: 0 0 $spacing-md 0; + font-size: $font-size-3xl; + color: $text-primary; + font-weight: $font-weight-bold; + } + + .server-meta { + display: flex; + gap: $spacing-sm; + flex-wrap: wrap; + } + } + + .server-actions { + display: flex; + gap: $spacing-md; + flex-wrap: wrap; + } +} + +// Action buttons (shared with App view, could be moved to global) +.action-btn { + @include action-btn(2px, $radius-md, $spacing-sm $spacing-lg, $font-size-sm); + + &.primary { + background: $primary; + color: white; + border-color: $primary; + + &:hover:not(:disabled) { + background: $primary-light; + border-color: $primary-light; + } + } + + &.secondary { + background: $bg-secondary; + color: $text-primary; + border-color: $border-color; + + &:hover:not(:disabled) { + background: color.adjust($bg-secondary, $lightness: -10%); + } + } + + &.danger { + background: $error; + color: white; + border-color: $error; + + &:hover:not(:disabled) { + background: color.adjust($error, $lightness: -10%); + } + } +} + +.back-button { + @extend .action-btn; + @extend .secondary; +} + +// Badges - host badge is specific to server view +.host-badge { + display: inline-flex; + align-items: center; + gap: $spacing-xs; + padding: $spacing-xs $spacing-md; + background: $bg-secondary; + color: $text-secondary; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + font-family: monospace; +} + +.upgrade-badge { + display: inline-flex; + align-items: center; + gap: $spacing-xs; + padding: $spacing-xs $spacing-md; + background: rgba($warning, 0.1); + background: color.adjust($error, $lightness: -20%); + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; +} + +// Content grid layout +.content-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: $spacing-xl; + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + } +} + +// Info cards +.info-card { + @include card; + margin-bottom: $spacing-xl; + + &:last-child { + margin-bottom: 0; + } + + h2 { + margin: 0 0 $spacing-lg 0; + font-size: $font-size-xl; + color: $text-primary; + font-weight: $font-weight-bold; + } +} + +// Info grid +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: $spacing-lg; +} + +.info-item { + display: flex; + flex-direction: column; + gap: $spacing-xs; + + label { + font-size: $font-size-xs; + color: $text-secondary; + font-weight: $font-weight-medium; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + span { + font-size: $font-size-base; + color: $text-primary; + } + + .stat-value { + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $text-primary; + + &.warning { + background: color.adjust($warning, $lightness: -10%); + } + + &.chaos { + background: color.adjust($info, $lightness: -10%); + } + } + + .no-value { + color: $text-muted; + font-style: italic; + } +} + +// Apps list in server detail +.no-apps { + text-align: center; + padding: $spacing-2xl; + color: $text-secondary; + font-size: $font-size-base; +} + +.apps-list { + @include card-list-vertical; +} + +.app-item { + padding: $spacing-md; + background: $bg-secondary; + border-radius: $radius-md; + border: 2px solid transparent; + cursor: pointer; + transition: all $transition-base; + + &:hover { + border-color: $primary; + background: white; + transform: translateX(4px); + } + + .app-item-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: $spacing-md; + margin-bottom: $spacing-sm; + + .app-item-name { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-primary; + } + + .app-item-badges { + display: flex; + gap: $spacing-xs; + flex-wrap: wrap; + } + } + + .app-item-details { + display: flex; + align-items: center; + gap: $spacing-md; + font-size: $font-size-sm; + + .app-item-version { + color: $text-secondary; + font-family: monospace; + } + + .app-item-domain { + color: $primary; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } +} + +// Quick actions list +.action-list { + display: flex; + flex-direction: column; + gap: $spacing-sm; + + .action-list-item { + display: flex; + align-items: center; + gap: $spacing-md; + padding: $spacing-md; + background: $bg-secondary; + border: 2px solid transparent; + border-radius: $radius-md; + cursor: pointer; + transition: all $transition-base; + font-size: $font-size-sm; + color: $text-primary; + text-align: left; + width: 100%; + + &:hover:not(:disabled) { + background: white; + border-color: $primary; + transform: translateX(2px); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.danger { + &:hover:not(:disabled) { + border-color: $error; + background: rgba($error, 0.05); + } + } + + .action-text { + flex: 1; + font-weight: $font-weight-medium; + } + } +} + +// Health status +.health-status { + display: flex; + flex-direction: column; + gap: $spacing-md; + + .health-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: $spacing-md; + border-bottom: 1px solid $border-color; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + .health-label { + font-size: $font-size-sm; + color: $text-secondary; + } + + .health-value { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-primary; + + &.status-online { + color: $success; + } + + &.status-offline { + color: $error; + } + } + } +} diff --git a/web/src/routes/Servers/Server.tsx b/web/src/routes/Servers/Server.tsx new file mode 100644 index 0000000..f895a51 --- /dev/null +++ b/web/src/routes/Servers/Server.tsx @@ -0,0 +1,378 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { Terminal } from '../../components/Terminal/Terminal'; +import { apiService } from '../../services/api'; +import type { AbraServer, ServerAppsResponse } from '../../types'; +import type { LogEntry } from '../../services/mockApi'; +import './Server.scss'; + +interface ServerWithApps extends AbraServer { + apps: any[]; + appCount: number; + upgradeCount: number; + versionCount: number; + unversionedCount: number; + latestCount: number; +} + +export const Server: React.FC = () => { + const { serverName } = useParams<{ serverName: string }>(); + const navigate = useNavigate(); + + const [server, setServer] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [actionLoading, setActionLoading] = useState(null); + + // Terminal state + const [terminalLogs, setTerminalLogs] = useState([]); + const [terminalActive, setTerminalActive] = useState(false); + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + + useEffect(() => { + const fetchServer = async () => { + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + const appsData = await mockApiService.getAppsGrouped(); + const serversData = await mockApiService.getServers(); + + const foundServer = serversData.find(s => s.name === serverName); + const serverApps = appsData[serverName || '']; + + if (foundServer && serverApps) { + setServer({ + ...foundServer, + apps: serverApps.apps, + appCount: serverApps.appCount, + upgradeCount: serverApps.upgradeCount, + versionCount: serverApps.versionCount, + unversionedCount: serverApps.unversionedCount, + latestCount: serverApps.latestCount, + }); + } else { + setError('Server not found'); + } + } else { + const appsData = await apiService.getAppsGrouped(); + const serversData = await apiService.getServers(); + + const foundServer = serversData.find(s => s.name === serverName); + const serverApps = appsData[serverName || '']; + + if (foundServer && serverApps) { + setServer({ + ...foundServer, + apps: serverApps.apps, + appCount: serverApps.appCount, + upgradeCount: serverApps.upgradeCount, + versionCount: serverApps.versionCount, + unversionedCount: serverApps.unversionedCount, + latestCount: serverApps.latestCount, + }); + } else { + setError('Server not found'); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load server'); + } finally { + setLoading(false); + } + }; + + fetchServer(); + }, [serverName, isMockMode]); + + const handleAction = async (action: string) => { + if (!server) return; + + setActionLoading(action); + setTerminalActive(true); + setTerminalLogs([]); + + try { + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + + const onLog = (log: LogEntry) => { + setTerminalLogs(prev => [...prev, log]); + }; + + switch (action) { + case 'refresh': + await mockApiService.refreshServer(server.name, onLog); + break; + case 'deploy-all': + await mockApiService.deployAllApps(server.name, server.appCount, onLog); + break; + case 'upgrade-all': + await mockApiService.upgradeAllApps(server.name, server.upgradeCount, onLog); + break; + } + } else { + // Real API calls + console.log(`Action: ${action} on server ${server.name}`); + } + } catch (err) { + console.error('Action failed:', err); + setTerminalLogs(prev => [...prev, { + type: 'error', + text: `❌ Error: ${err instanceof Error ? err.message : 'Action failed'}`, + timestamp: new Date() + }]); + } finally { + setActionLoading(null); + } + }; + + if (loading) { + return ( +
+
+
+
+ +
Loading server
+
+
+
+ ); + } + + if (error || !server) { + return ( +
+
+
+
{error || 'Server not found'}
+ +
+
+ ); + } + + const chaosApps = server.apps.filter(app => app.chaos === 'true'); + const runningApps = server.apps.filter(app => app.status === 'deployed' || app.status === 'running'); + + return ( +
+
+
+
+ +
+ +
+
+

{server.name}

+
+ {server.host} + {server.upgradeCount > 0 && ( + + {server.upgradeCount} upgrade{server.upgradeCount !== 1 ? 's' : ''} + + )} +
+
+ +
+ + +
+
+ + {/* Terminal Component */} + setTerminalActive(false)} + /> + +
+ {/* Left Column - Main Info */} +
+
+

Server Details

+ +
+
+ + {server.name} +
+ +
+ + {server.host} +
+ +
+ + {server.appCount} +
+ +
+ + {runningApps.length} +
+ +
+ + 0 ? 'stat-value warning' : 'stat-value'}> + {server.upgradeCount} + +
+ +
+ + 0 ? 'stat-value chaos' : 'stat-value'}> + {chaosApps.length} + +
+ +
+ + {server.latestCount} +
+ +
+ + {server.versionCount} +
+
+
+ +
+

Applications on this Server

+ + {server.apps.length === 0 ? ( +
No applications deployed on this server
+ ) : ( +
+ {server.apps.map((app, idx) => ( +
navigate(`/apps/${server.name}/${app.appName}`)} + > +
+ {app.appName} +
+ {app.recipe} + + {app.status} + + {app.chaos === 'true' && ( + ☠️ + )} + {app.upgrade !== 'latest' && ( + ⬆️ + )} +
+
+
+ v{app.version} + {app.domain && ( + e.stopPropagation()} + > + {app.domain} + + )} +
+
+ ))} +
+ )} +
+
+ + {/* Right Column - Actions & Stats */} +
+
+

Quick Actions

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

Server Health

+
+
+ Status + 🟢 Online +
+
+ CPU Usage + — +
+
+ Memory + — +
+
+ Disk Space + — +
+
+ Uptime + — +
+
+
+
+
+
+
+ ); +}; diff --git a/web/src/routes/Servers/Servers.scss b/web/src/routes/Servers/Servers.scss new file mode 100644 index 0000000..a6ec97c --- /dev/null +++ b/web/src/routes/Servers/Servers.scss @@ -0,0 +1,157 @@ +@use '../../assets/scss/variables' as *; +@use '../../assets/scss/mixins' as *; +@use '../../assets/scss/global' as *; + +// Extend global page wrapper +.servers-page { + @extend .page-wrapper; +} + +.servers-content { + @extend .page-content; +} + +// Servers grid +.servers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: $spacing-xl; + margin-bottom: $spacing-xl; + + align-items: stretch; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +} + +// Server card +.server-card { + @include card; + @include card-hover-lift(-4px, $shadow-xl); + @include card-dimensions(320px, 180px); + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + + .server-header { + @include card-header-rule; + + .server-title { + @include card-title-stack; + } + + .server-status { + .status-indicator { + font-size: $font-size-xl; + + &.connected { + color: $success; + } + + &.disconnected { + color: $error; + } + } + } + } + + .server-stats { + flex: 1; + margin-bottom: $spacing-lg; + + .stat-row { + @include card-stat-row; + + // Highlighted rows + &.highlight { + @include card-row-highlight-bleed; + background-color: rgba($warning, 0.05); + + .stat-label { + font-weight: $font-weight-semibold; + } + + .stat-value { + font-weight: $font-weight-bold; + } + } + + &.chaos-row { + @include card-row-highlight-bleed(false); + background-color: rgba($info, 0.05); + + .stat-label { + font-weight: $font-weight-semibold; + } + + .stat-value { + font-weight: $font-weight-bold; + } + } + + .stat-label { + font-size: $font-size-sm; + color: $text-secondary; + display: flex; + align-items: center; + gap: $spacing-xs; + + .upgrade-icon, + .chaos-icon { + font-size: $font-size-base; + } + } + + .stat-value { + font-size: $font-size-lg; + color: $text-primary; + font-weight: $font-weight-semibold; + } + } + } + + .server-actions { + display: flex; + gap: $spacing-sm; + margin-bottom: $spacing-md; + + .action-btn { + flex: 1; + @include action-btn(2px, $radius-md, $spacing-sm $spacing-md, $font-size-sm); + + &.primary { + // background-color: $primary; + // color: white; + // border-color: $primary; + + &:hover { + background-color: $primary-light; + border-color: $primary-light; + } + } + } + } + + .server-alert { + background-color: rgba($warning, 0.1); + border-left: 4px solid $warning; + padding: $spacing-sm $spacing-md; + margin: 0 (-$spacing-xl) (-$spacing-xl); + display: flex; + align-items: center; + gap: $spacing-sm; + + .alert-icon { + font-size: $font-size-lg; + color: $warning; + } + + .alert-text { + font-size: $font-size-sm; + color: $text-primary; + font-weight: $font-weight-medium; + } + } +} diff --git a/web/src/routes/Servers/Servers.tsx b/web/src/routes/Servers/Servers.tsx new file mode 100644 index 0000000..6d3d84b --- /dev/null +++ b/web/src/routes/Servers/Servers.tsx @@ -0,0 +1,259 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Header } from '../../components/Header/Header'; +import { Loader } from '../../components/Loader/Loader'; +import { apiService } from '../../services/api'; +import type { AbraServer, ServerAppsResponse } from '../../types'; +import './Servers.scss'; + +interface ServerWithStats extends AbraServer { + appCount: number; + versionCount: number; + latestCount: number; + upgradeCount: number; + chaosCount: number; + backupEnabled: boolean; +} + +export const Servers: React.FC = () => { + const navigate = useNavigate(); + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState<'name' | 'apps' | 'upgrades'>('name'); + const [showUpgradesOnly, setShowUpgradesOnly] = useState(false); + const [showChaosOnly, setShowChaosOnly] = useState(false); + + const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true'; + + useEffect(() => { + const fetchData = async () => { + try { + let serversData, appsData; + + if (isMockMode) { + const { mockApiService } = await import('../../services/mockApi'); + [serversData, appsData] = await Promise.all([ + mockApiService.getServers(), + mockApiService.getAppsGrouped(), + ]); + } else { + [serversData, appsData] = await Promise.all([ + apiService.getServers(), + apiService.getAppsGrouped(), + ]); + } + + // Enrich servers with stats from apps data + const enrichedServers = serversData.map(server => { + const serverStats = appsData[server.name]; + const chaosCount = serverStats?.apps.filter(app => app.chaos === 'true').length || 0; + + return { + ...server, + appCount: serverStats?.appCount || 0, + versionCount: serverStats?.versionCount || 0, + latestCount: serverStats?.latestCount || 0, + upgradeCount: serverStats?.upgradeCount || 0, + chaosCount, + backupEnabled: serverStats?.backupEnabled + }; + }); + + setServers(enrichedServers); + } catch (err) { + console.error('Error loading servers:', err); + setError(err instanceof Error ? err.message : 'Failed to load servers'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [isMockMode]); + + // Calculate overall stats + const stats = useMemo(() => { + const totalServers = servers.length; + const totalApps = servers.reduce((sum, s) => sum + s.appCount, 0); + const totalUpgrades = servers.reduce((sum, s) => sum + s.upgradeCount, 0); + const totalChaos = servers.reduce((sum, s) => sum + s.chaosCount, 0); + + return { totalServers, totalApps, totalUpgrades, totalChaos }; + }, [servers]); + + // Filter and sort servers (additive filters allowed) + const filteredServers = useMemo(() => { + const filtered = servers.filter(server => { + const matchesSearch = + server.name.toLowerCase().includes(searchTerm.toLowerCase()) || + server.host.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesUpgrades = !showUpgradesOnly || server.upgradeCount > 0; + const matchesChaos = !showChaosOnly || server.chaosCount > 0; + return matchesSearch && matchesUpgrades && matchesChaos; + }); + + filtered.sort((a, b) => { + switch (sortBy) { + case 'apps': + return b.appCount - a.appCount; + case 'name': + default: + return a.name.localeCompare(b.name); + } + }); + + return filtered; +}, [servers, searchTerm, sortBy, showUpgradesOnly]); + + if (loading) { + return ( +
+
+
+
+ +
Loading servers
+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+
Error: {error}
+
+
+ ); + } + + return ( +
+
+
+
+

Servers

+

Managing {stats.totalServers} servers with {stats.totalApps} applications

+
+ + {/* Compact Stats Row */} +
+ + + + + + + {/* Clear filters button removed — use stat-chip outlines for active filters */} +
+ + {/* Filters */} +
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+ + {/* Server Cards */} +
+ {filteredServers.length === 0 ? ( +
No servers found matching your search
+ ) : ( + filteredServers.map((server) => ( +
navigate(`/servers/${server.name}`)} + style={{ cursor: 'pointer' }} + > +
+
+

{server.name}

+ {server.host} +
+
+ ● +
+
+ +
+
+ Applications + {server.appCount} +
+
+ Versioned + {server.versionCount} +
+
+ Latest + {server.latestCount} +
+ {server.upgradeCount > 0 && ( +
+ + Need Upgrade + + {server.upgradeCount} +
+ )} + {server.chaosCount > 0 && ( +
+ + Chaos Mode + + {server.chaosCount} +
+ )} +
+ {!server.backupEnabled && ( +
+ + No Backups Enabled + +
+ )} +
+ )) + )} +
+ +
+ Showing {filteredServers.length} of {servers.length} servers +
+
+
+ ); +}; diff --git a/web/src/services/api.ts b/web/src/services/api.ts new file mode 100644 index 0000000..171b164 --- /dev/null +++ b/web/src/services/api.ts @@ -0,0 +1,287 @@ +import type { AbraServer, AbraRecipe, ServerAppsResponse, AbraAppService, DeployEvent, AbraAppSecret, File, AppSnapshot } from '../types'; + +// Log entry type +export type LogEntry = { + type: 'info' | 'success' | 'error' | 'warning' | 'command' | 'output'; + text: string; + timestamp: Date; +}; + +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; + +// Helper to process log JSON from API response +const processLogResponse = (logData: any[]): LogEntry[] => { + return logData.map(log => ({ + type: log.type, + text: log.text, + timestamp: new Date(log.timestamp || Date.now()), + })); +}; + +class ApiService { + private async request( + endpoint: string, + options: RequestInit = {}, + URLQueryString?: string + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + const queryString = URLQueryString ?? "" + const response = await fetch(`${API_BASE_URL}${endpoint}` + queryString, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'An error occurred' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + if (response.status === 204) { + return undefined as T; + } + + const contentType = response.headers.get('content-type'); + + if (contentType?.includes('application/json')) { + return response.json(); + } + return response.text() as unknown as T; + } + + private stream( + endpoint: string, + handlers: { + onMessage: (data: T) => void; + onError?: (err: any) => void; + onOpen?: () => void; + parser?: (raw: string) => T; + } + ) { + const es = new EventSource(`${API_BASE_URL}${endpoint}`); + + es.onopen = () => { + handlers.onOpen?.(); + }; + es.onmessage = (event) => { + try { + const data = handlers.parser + ? handlers.parser(event.data) + : (event.data as unknown as T); + + handlers.onMessage(data); + } catch (err) { + handlers.onError?.(err); + } + }; + es.onerror = (err) => { + handlers.onError?.(err); + es.close(); + }; + return () => es.close(); + } + private async file( + endpoint: string, + options: RequestInit = {} + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'An error occurred' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + return response.blob() + } + // Get Logs for service + getLogs(appName: string, serviceName: string, msgHandler: (data: String) => void) { + return this.stream(`/apps/${appName}/${serviceName}/logs`, {onMessage: msgHandler}) + } + + async getConfig(appName: string): Promise { + const file = await this.request(`/apps/${appName}/config`) + return file.content + } + + async writeConfig(appName: string, code: string): Promise { + const file : File = { content: code }; + return this.request(`/apps/${appName}/config`, { + method: 'POST', + headers: { + 'Accept': 'application/json' + }, + body: JSON.stringify(file), + }) + } + + async getSecrets(appName: string): Promise { + return this.request(`/apps/${appName}/secret`); + } + + async writeSecret(appName: string, secretName: string, version: string, value: string): Promise { + console.log("writing secret ", appName, secretName, version, value); + return this.request(`/apps/${appName}/secret`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify( { + 'name': secretName, + 'version': version, + 'value': value, + }) + }); + } + async downloadSnapshot(appName: string): Promise { + return this.file(`/apps/${appName}/backup/download`) + } + + // Get all apps grouped by server + async getAppsGrouped(): Promise { + return this.request('/apps'); + } + + // Get all servers + async getServers(): Promise { + return this.request('/servers'); + } + // Get services for app + async getServices(appName: string): Promise { + return this.request(`/apps/${appName}/services`); + } + + // Get all backups for an app + async getBackupList(appName: string): Promise { + return this.request(`/apps/${appName}/backup/list`); + } + // Create backup for an app + async createBackup (appName: string): Promise { + return this.request(`/apps/${appName}/backup/create`, { + method: 'POST', + }); + } + // restore backup for app + async restoreBackup (appName: string, snapshot?: string): Promise { + const response = await this.request(`/apps/${appName}/backup/restore`, { + method: 'POST', + body: JSON.stringify({snapshot: snapshot}), + }); + return response + } + // App actions with log streaming (websocket future) + async deployApp(appName: string): Promise { + return this.request(`/apps/${appName}/deploy`) + } + deployLogs(appName: string, msgHandler: (data: DeployEvent) => void) { + return this.stream(`/apps/${appName}/deploy`, {parser: JSON.parse, onMessage: msgHandler}) + } + + async stopApp(appName: string, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/apps/${appName}/stop`, { + method: 'POST', + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + + async upgradeApp(appName: string, version?: string, force?: boolean, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/apps/${appName}/upgrade`, { + method: 'POST', + body: JSON.stringify({ version: version }), + }, + "?" + new URLSearchParams({force: force ? 'true' : 'false'}) + ); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + + async removeApp(appName: string, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/apps/${appName}/remove`, { + method: 'POST', + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + async rollbackApp(appName: string, version: string, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/apps/${appName}/rollback`, { + method: 'POST', + body: JSON.stringify({ version: version }), + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + async newApp(appName: string, formData: Object): Promise { + const response = await this.request(`/apps/${appName}/new`, { + method: 'POST', + headers: { + 'Accept': 'application/json' + }, + body: JSON.stringify(formData) + }); + return response + } + async getAppVersions(app: string): Promise { + return this.request(`/apps/${app}/versions`); + } + + // Server actions with log streaming + async refreshServer(serverName: string, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/servers/${serverName}/refresh`, { + method: 'POST', + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + + async deployAllApps(serverName: string, appCount: number, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/servers/${serverName}/deploy-all`, { + method: 'POST', + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + + async upgradeAllApps(serverName: string, upgradeCount: number, onLog?: (log: LogEntry) => void): Promise { + const response = await this.request<{ logs: any[] }>(`/servers/${serverName}/upgrade-all`, { + method: 'POST', + }); + + if (onLog && response.logs) { + const logs = processLogResponse(response.logs); + logs.forEach(log => onLog(log)); + } + } + // recipe catalog imports + async getRecipes(): Promise { + return this.request('/catalogue'); + } +} + + +export const apiService = new ApiService(); \ No newline at end of file diff --git a/web/src/services/mock-apps.json b/web/src/services/mock-apps.json new file mode 100644 index 0000000..c3edd8f --- /dev/null +++ b/web/src/services/mock-apps.json @@ -0,0 +1,593 @@ +{ + "mydomain.com": { + "apps": [ + { + "server": "mydomain.com", + "recipe": "authentik", + "appName": "accounts.mydomain.com", + "domain": "accounts.mydomain.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "7.4.0+2025.6.3", + "upgrade": "9.0.1+2025.8.1\n9.0.0+2025.8.1\n8.0.0+2025.8.1\n7.4.1+2025.6.4" + }, + { + "server": "mydomain.com", + "recipe": "backup-bot-two", + "appName": "backup.mydomain.com", + "domain": "", + "status": "deployed", + "chaos": "false", + "chaosVersion": "2.3.0+2.3.0-beta", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "mydomain.com", + "recipe": "collabora", + "appName": "docs.mydomain.com", + "domain": "docs.mydomain.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "3.2.0+24.04.12.3.1", + "version": "3.2.0+24.04.12.3.1", + "upgrade": "4.0.0+25.04.4.1.1\n3.3.0+24.04.13.3.1" + }, + { + "server": "mydomain.com", + "recipe": "nextcloud", + "appName": "nc.mydomain.com", + "domain": "nc.mydomain.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "12.0.1+31.0.6-fpm", + "upgrade": "latest" + }, + { + "server": "mydomain.com", + "recipe": "traefik", + "appName": "traefik.mydomain.com", + "domain": "traefik.mydomain.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "3.4.2+v3.4.5", + "upgrade": "3.6.2+v3.4.5\n3.6.1+v3.4.5\n3.6.0+v3.4.5\n3.5.0+v3.4.5" + } + ], + "appCount": 5, + "versionCount": 5, + "unversionedCount": 0, + "latestCount": 2, + "upgradeCount": 3 + }, + "coop.test.org": { + "apps": [ + { + "server": "coop.test.org", + "recipe": "cryptpad", + "appName": "cryptpad.coop.test.org", + "domain": "cryptpad.coop.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "cb2a47fb", + "version": "0.4.0+version-2024.3.0", + "upgrade": "latest" + }, + { + "server": "coop.test.org", + "recipe": "garage", + "appName": "garage.coop.test.org", + "domain": "garage.coop.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "3a729d56+U", + "version": "3a729d56+U", + "upgrade": "latest" + }, + { + "server": "coop.test.org", + "recipe": "mobilizon", + "appName": "mobilizon.test.org", + "domain": "mobilizon.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "f8f874a5 + unstaged changes", + "version": "0.2.1+5.1.2", + "upgrade": "latest" + }, + { + "server": "coop.test.org", + "recipe": "owncast", + "appName": "tv.test.org", + "domain": "tv.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "0.3.0+0.2.1", + "version": "0.3.0+0.2.1", + "upgrade": "latest" + }, + { + "server": "coop.test.org", + "recipe": "privatebin", + "appName": "paste.test.org", + "domain": "paste.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "0.6.1+0.17.4", + "version": "0.6.0+1.7.6", + "upgrade": "0.6.1+0.17.4" + }, + { + "server": "coop.test.org", + "recipe": "traefik", + "appName": "traefik.coop.test.org", + "domain": "traefik.coop.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "27d5c092", + "version": "27d5c092", + "upgrade": "latest" + } + ], + "appCount": 6, + "versionCount": 6, + "unversionedCount": 0, + "latestCount": 5, + "upgradeCount": 1 + }, + "internal.website.com": { + "apps": [ + { + "server": "internal.website.com", + "recipe": "backup-bot-two", + "appName": "backup.internal.website.com", + "domain": "backup.internal.website.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "internal.website.com", + "recipe": "baserow", + "appName": "base.website.com", + "domain": "base.website.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "1.1.0+1.31.1", + "upgrade": "1.2.1+1.35.3\n1.2.0+1.35.3" + }, + { + "server": "internal.website.com", + "recipe": "traefik", + "appName": "traefik.internal.website.com", + "domain": "traefik.internal.website.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "3.6.2+v3.4.5", + "upgrade": "latest" + } + ], + "appCount": 3, + "versionCount": 3, + "unversionedCount": 0, + "latestCount": 2, + "upgradeCount": 1 + }, + "internal.server.net": { + "apps": [ + { + "server": "internal.server.net", + "recipe": "authentik", + "appName": "accounts.server.net", + "domain": "accounts.server.net", + "status": "deployed", + "chaos": "false", + "chaosVersion": "6.11.0+2024.10.5", + "version": "6.11.0+2024.10.5", + "upgrade": "9.0.1+2025.8.1\n9.0.0+2025.8.1\n8.0.0+2025.8.1\n7.4.1+2025.6.4\n7.4.0+2025.6.3\n7.3.2+2025.6.2\n7.3.1+2025.6.1\n7.3.0+2025.6.0\n7.2.0+2025.4.1\n7.1.0+2025.2.4\n7.0.3+2025.2.3\n7.0.2+2025.2.2\n7.0.1+2025.2.0\n7.0.0+2025.2.0\n6.12.0+2024.12.3\n6.11.1+2024.10.5" + }, + { + "server": "internal.server.net", + "recipe": "authentik", + "appName": "accounts.test.server.net", + "domain": "accounts.test.server.net", + "status": "deployed", + "chaos": "false", + "chaosVersion": "7.1.0+2025.2.4", + "version": "7.1.0+2025.2.4", + "upgrade": "9.0.1+2025.8.1\n9.0.0+2025.8.1\n8.0.0+2025.8.1\n7.4.1+2025.6.4\n7.4.0+2025.6.3\n7.3.2+2025.6.2\n7.3.1+2025.6.1\n7.3.0+2025.6.0\n7.2.0+2025.4.1" + }, + { + "server": "internal.server.net", + "recipe": "backup-bot-two", + "appName": "backup.server.net", + "domain": "", + "status": "deployed", + "chaos": "false", + "chaosVersion": "2.3.0+2.3.0-beta", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "custom-html", + "appName": "counterspy.zip", + "domain": "counterspy.zip", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "1.11.0+1.29.0", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "garage", + "appName": "garage.server.net", + "domain": "garage.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "20bfd2c6+U", + "version": "20bfd2c6+U", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "gitlab", + "appName": "git.server.net", + "domain": "git.server.net", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "0.2.2+18.3.0-ce.0", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "jitsi", + "appName": "jitsi.server.net", + "domain": "jitsi.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "d3dd84ab + unstaged changes", + "version": "0.0.1+8719", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "liberaforms", + "appName": "forms.test.server.net", + "domain": "forms.test.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "b3e74fa7+U", + "version": "b3e74fa7+U", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "mattermost", + "appName": "mattermost-demo.server.net", + "domain": "mattermost-demo.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "52580808", + "version": "52580808", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "openemr", + "appName": "openemr.server.net", + "domain": "openemr.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "6b62e331", + "version": "6b62e331", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "outline", + "appName": "wiki.server.net", + "domain": "wiki.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "ab70b3c4+U", + "version": "ab70b3c4+U", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "statping", + "appName": "status.server.net", + "domain": "status.server.net", + "status": "deployed", + "chaos": "true", + "chaosVersion": "bd875e08", + "version": "0.1.1+v0.90.78", + "upgrade": "latest" + }, + { + "server": "internal.server.net", + "recipe": "traefik", + "appName": "traefik.server.net", + "domain": "traefik.server.net", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "3.6.2+v3.4.5", + "upgrade": "latest" + } + ], + "appCount": 13, + "versionCount": 13, + "unversionedCount": 0, + "latestCount": 11, + "upgradeCount": 2 + }, + "internal.intranet.site.com": { + "apps": [ + { + "server": "internal.intranet.site.com", + "recipe": "backup-bot-two", + "appName": "backup.intranet.site.com", + "domain": "", + "status": "deployed", + "chaos": "false", + "chaosVersion": "2.3.0+2.3.0-beta", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "internal.intranet.site.com", + "recipe": "baserow", + "appName": "base.intranet.site.com", + "domain": "base.intranet.site.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "1.1.0+1.31.1", + "upgrade": "1.2.1+1.35.3\n1.2.0+1.35.3" + }, + { + "server": "internal.intranet.site.com", + "recipe": "traefik", + "appName": "traefik.intranet.site.com", + "domain": "traefik.intranet.site.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "3.4.2+v3.4.5", + "upgrade": "3.6.2+v3.4.5\n3.6.1+v3.4.5\n3.6.0+v3.4.5\n3.5.0+v3.4.5" + }, + { + "server": "internal.intranet.site.com", + "recipe": "vaultwarden", + "appName": "pass.intranet.site.com", + "domain": "pass.intranet.site.com", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "2.0.0+1.33.2", + "upgrade": "2.1.0+1.34.1" + } + ], + "appCount": 4, + "versionCount": 4, + "unversionedCount": 0, + "latestCount": 1, + "upgradeCount": 3 + }, + "internal.test.org": { + "apps": [ + { + "server": "internal.test.org", + "recipe": "authentik", + "appName": "accounts.test.org", + "domain": "accounts.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "9.0.0+2025.8.1", + "upgrade": "9.0.1+2025.8.1" + }, + { + "server": "internal.test.org", + "recipe": "backup-bot-two", + "appName": "backup.test.org", + "domain": "", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "internal.test.org", + "recipe": "collabora", + "appName": "docs.test.org", + "domain": "docs.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "3.1.0+24.04.10.2.1", + "version": "3.1.0+24.04.10.2.1", + "upgrade": "4.0.0+25.04.4.1.1\n3.3.0+24.04.13.3.1\n3.2.0+24.04.12.3.1" + }, + { + "server": "internal.test.org", + "recipe": "custom-html", + "appName": "test.org", + "domain": "test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "ba1c2269", + "version": "1.7.1+1.27.2", + "upgrade": "latest" + }, + { + "server": "internal.test.org", + "recipe": "garage", + "appName": "garage.test.org", + "domain": "garage.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "9ca66f0f", + "version": "9ca66f0f", + "upgrade": "latest" + }, + { + "server": "internal.test.org", + "recipe": "nextcloud", + "appName": "nc.test.org", + "domain": "nc.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "11.0.0+30.0.4-fpm", + "version": "11.0.0+30.0.4-fpm", + "upgrade": "12.0.1+31.0.6-fpm\n12.0.0+31.0.6-fpm\n11.4.0+30.0.10-fpm\n11.4.0+30.0.6-fpm\n11.3.0+30.0.6-fpm\n11.2.0+30.0.6-fpm\n11.1.0+30.0.6-fpm\n11.0.1+30.0.4-fpm" + }, + { + "server": "internal.test.org", + "recipe": "outline", + "appName": "wiki.test.org", + "domain": "wiki.test.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "2.8.1+0.81.1", + "version": "2.8.1+0.81.1", + "upgrade": "2.12.1+0.87.4\n2.12.0+0.87.3\n2.11.0+0.86.1\n2.10.0+0.84.0\n2.9.1+0.82.0\n2.9.0+0.82.0\n2.8.2+0.81.1" + }, + { + "server": "internal.test.org", + "recipe": "traefik", + "appName": "traefik.test.org", + "domain": "traefik.test.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "993ed9cf", + "version": "993ed9cf", + "upgrade": "latest" + } + ], + "appCount": 8, + "versionCount": 8, + "unversionedCount": 0, + "latestCount": 4, + "upgradeCount": 4 + }, + "orgsite.org": { + "apps": [ + { + "server": "orgsite.org", + "recipe": "authentik", + "appName": "accounts.orgsite.org", + "domain": "accounts.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "6.11.0+2024.10.5", + "version": "6.11.0+2024.10.5", + "upgrade": "9.0.1+2025.8.1\n9.0.0+2025.8.1\n8.0.0+2025.8.1\n7.4.1+2025.6.4\n7.4.0+2025.6.3\n7.3.2+2025.6.2\n7.3.1+2025.6.1\n7.3.0+2025.6.0\n7.2.0+2025.4.1\n7.1.0+2025.2.4\n7.0.3+2025.2.3\n7.0.2+2025.2.2\n7.0.1+2025.2.0\n7.0.0+2025.2.0\n6.12.0+2024.12.3\n6.11.1+2024.10.5" + }, + { + "server": "orgsite.org", + "recipe": "backup-bot-two", + "appName": "backup.orgsite.org", + "domain": "", + "status": "deployed", + "chaos": "false", + "chaosVersion": "2.3.0+2.3.0-beta", + "version": "2.3.0+2.3.0-beta", + "upgrade": "latest" + }, + { + "server": "orgsite.org", + "recipe": "baserow", + "appName": "base.orgsite.org", + "domain": "base.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "1.1.0+1.31.1", + "upgrade": "1.2.1+1.35.3\n1.2.0+1.35.3" + }, + { + "server": "orgsite.org", + "recipe": "collabora", + "appName": "docs.orgsite.org", + "domain": "docs.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "3.1.0+24.04.10.2.1", + "version": "3.1.0+24.04.10.2.1", + "upgrade": "4.0.0+25.04.4.1.1\n3.3.0+24.04.13.3.1\n3.2.0+24.04.12.3.1" + }, + { + "server": "orgsite.org", + "recipe": "jitsi", + "appName": "meet.orgsite.org", + "domain": "meet.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "d3dd84ab", + "version": "0.0.1+8719", + "upgrade": "latest" + }, + { + "server": "orgsite.org", + "recipe": "mattermost", + "appName": "chat.orgsite.org", + "domain": "chat.orgsite.org", + "status": "deployed", + "chaos": "true", + "chaosVersion": "5f0295b9", + "version": "2.1.0+10.5.0", + "upgrade": "latest" + }, + { + "server": "orgsite.org", + "recipe": "nextcloud", + "appName": "nc.orgsite.org", + "domain": "nc.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "11.1.0+30.0.6-fpm", + "version": "11.1.0+30.0.6-fpm", + "upgrade": "12.0.1+31.0.6-fpm\n12.0.0+31.0.6-fpm\n11.4.0+30.0.10-fpm\n11.4.0+30.0.6-fpm\n11.3.0+30.0.6-fpm\n11.2.0+30.0.6-fpm" + }, + { + "server": "orgsite.org", + "recipe": "traefik", + "appName": "traefik.orgsite.org", + "domain": "traefik.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "3.4.2+v3.4.5", + "upgrade": "3.6.2+v3.4.5\n3.6.1+v3.4.5\n3.6.0+v3.4.5\n3.5.0+v3.4.5" + }, + { + "server": "orgsite.org", + "recipe": "vaultwarden", + "appName": "pass.orgsite.org", + "domain": "pass.orgsite.org", + "status": "deployed", + "chaos": "false", + "chaosVersion": "unknown", + "version": "2.0.0+1.33.2", + "upgrade": "2.1.0+1.34.1" + } + ], + "appCount": 9, + "versionCount": 9, + "unversionedCount": 0, + "latestCount": 3, + "upgradeCount": 6 + } +} \ No newline at end of file diff --git a/web/src/services/mock-catalogue.json b/web/src/services/mock-catalogue.json new file mode 100644 index 0000000..3d11e40 --- /dev/null +++ b/web/src/services/mock-catalogue.json @@ -0,0 +1,1726 @@ +[ + { + "category": "Apps", + "default_branch": "main", + "description": "An awesome, Online Office suite image suitable for home use.", + "features": { + "backups": "No", + "email": "3", + "healthcheck": "No", + "image": { + "image": "collabora", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/collabora/code" + }, + "status": 3, + "tests": "2", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/249-0937028d43099df58d85bc41a7e69fbd", + "name": "collabora", + "repository": "https://git.coopcloud.tech/coop-cloud/collabora.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/collabora.git", + "versions": [ + { + "1.0.0+6.4.9.3": { + "app": { + "image": "collabora/code", + "tag": "6.4.9.3" + }, + "web": { + "image": "nginx", + "tag": "1.21.4" + } + } + }, + { + "2.0.0+21.11.0.5.1": { + "app": { + "image": "collabora/code", + "tag": "21.11.0.5.1" + }, + "web": { + "image": "nginx", + "tag": "1.21.4" + } + } + }, + { + "2.1.0+21.11.1.4.1": { + "app": { + "image": "collabora/code", + "tag": "21.11.1.4.1" + }, + "web": { + "image": "nginx", + "tag": "1.21.4" + } + } + }, + { + "2.1.2+22.05.8.2.1": { + "app": { + "image": "collabora/code", + "tag": "22.05.8.2.1" + }, + "web": { + "image": "nginx", + "tag": "1.22.1" + } + } + }, + { + "2.2.0+22.05.10.1.1": { + "app": { + "image": "collabora/code", + "tag": "22.05.10.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.23.3" + } + } + }, + { + "2.3.0+22.05.14.3.1": { + "app": { + "image": "collabora/code", + "tag": "22.05.14.3.1" + }, + "web": { + "image": "nginx", + "tag": "1.24.0" + } + } + }, + { + "2.4.0+23.05.2.2.1": { + "app": { + "image": "collabora/code", + "tag": "23.05.2.2.1" + }, + "web": { + "image": "nginx", + "tag": "1.25.2" + } + } + }, + { + "2.5.0+23.05.3.1.1": { + "app": { + "image": "collabora/code", + "tag": "23.05.3.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.25.2" + } + } + }, + { + "2.6.0+23.05.5.4.1": { + "app": { + "image": "collabora/code", + "tag": "23.05.5.4.1" + }, + "web": { + "image": "nginx", + "tag": "1.25.3" + } + } + }, + { + "2.7.0+23.05.8.2.1": { + "app": { + "image": "collabora/code", + "tag": "23.05.8.2.1" + }, + "web": { + "image": "nginx", + "tag": "1.25.4" + } + } + }, + { + "2.7.1+23.05.10.1.1": { + "app": { + "image": "collabora/code", + "tag": "23.05.10.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.25.4" + } + } + }, + { + "3.0.0+24.04.6.1.1": { + "app": { + "image": "collabora/code", + "tag": "24.04.6.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.27.0" + } + } + }, + { + "3.1.0+24.04.10.2.1": { + "app": { + "image": "collabora/code", + "tag": "24.04.10.2.1" + }, + "web": { + "image": "nginx", + "tag": "1.27.3" + } + } + }, + { + "3.2.0+24.04.12.3.1": { + "app": { + "image": "collabora/code", + "tag": "24.04.12.3.1" + }, + "web": { + "image": "nginx", + "tag": "1.27.4" + } + } + }, + { + "3.3.0+24.04.13.3.1": { + "app": { + "image": "collabora/code", + "tag": "24.04.13.3.1" + }, + "web": { + "image": "nginx", + "tag": "1.28.0" + } + } + }, + { + "4.0.0+25.04.4.1.1": { + "app": { + "image": "collabora/code", + "tag": "25.04.4.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.29.0" + } + } + }, + { + "4.0.1+25.04.9.1.1": { + "app": { + "image": "collabora/code", + "tag": "25.04.9.1.1" + }, + "web": { + "image": "nginx", + "tag": "1.29.4" + } + } + }, + { + "4.0.2+25.04.9.4.1": { + "app": { + "image": "collabora/code", + "tag": "25.04.9.4.1" + }, + "web": { + "image": "nginx", + "tag": "1.29.7" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "HTTP/HTTPS compression proxy ", + "features": { + "backups": "No", + "email": "N/A", + "healthcheck": "No", + "image": { + "image": "compy", + "rating": "0", + "source": "own", + "url": "https://hub.docker.com/r/thecoopcloud/compy" + }, + "status": 1, + "tests": "No", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/435-127e3f583ac79a3b71f8fbd9c1cf5ba1", + "name": "compy", + "repository": "https://git.coopcloud.tech/coop-cloud/compy.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/compy.git", + "versions": [], + "website": "" + }, + { + "category": "Development", + "default_branch": "main", + "description": "A painless self-hosted Git service", + "features": { + "backups": "Yes", + "email": "Yes", + "healthcheck": "Yes", + "image": { + "image": "forgejo/forgejo", + "rating": "4", + "source": "upstream", + "url": "https://codeberg.org/forgejo/-/packages/container/forgejo/13-rootless" + }, + "status": 5, + "tests": "2", + "sso": "3 (OAuth)" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/eb3fb3ee823159765f746a75128143f3cd9b11c3f074ff6043ceeac8cdc434a9", + "name": "forgejo", + "repository": "https://git.coopcloud.tech/coop-cloud/forgejo.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/forgejo.git", + "versions": [ + { + "1.0.0+1.14.5-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.14.5-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.1.0+1.15.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.15.0-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.1.1+1.15.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.15.3-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.1.2+1.15.6-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.15.6-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.1.3+1.15.10-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.15.10-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.2.0+1.16.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.16.3-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.2.1+1.16.8-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.16.8-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.3.0+1.17.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.17.2-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.3.1+1.17.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.17.3-rootless" + }, + "db": { + "image": "mariadb", + "tag": "10.9" + } + } + }, + { + "2.0.0+1.18.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.18.0-rootless" + }, + "db": { + "image": "postgres", + "tag": "9.6" + } + } + }, + { + "2.0.1+1.18.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.18.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "9.6" + } + } + }, + { + "2.1.0+1.18.5-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.18.5-rootless" + }, + "db": { + "image": "postgres", + "tag": "9.6" + } + } + }, + { + "2.1.2+1.19.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.19.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.2" + } + } + }, + { + "2.2.0+1.19.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.19.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.3" + } + } + }, + { + "2.3.0+1.20.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.20.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.3" + } + } + }, + { + "2.3.1+1.20.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.20.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.3" + } + } + }, + { + "2.3.2+1.20.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.20.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.4" + } + } + }, + { + "2.3.3+1.20.5-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.20.5-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.4" + } + } + }, + { + "2.4.0+1.21.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.0-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.4" + } + } + }, + { + "2.5.0+1.21.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.5.1+1.21.4-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.4-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.5.2+1.21.5-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.5-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.6.0+1.21.5-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.5-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.6.1+1.21.10-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.10-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.6.2+1.21.10-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.10-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.5" + } + } + }, + { + "2.7.0+1.21.11-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.11-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.6" + } + } + }, + { + "2.8.0+1.21.11-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.21.11-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.7" + } + } + }, + { + "2.9.0+1.22.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.0-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.7" + } + } + }, + { + "2.9.1+1.22.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.0-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.7" + } + } + }, + { + "2.10.0+1.22.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.7" + } + } + }, + { + "2.10.1+1.22.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.8" + } + } + }, + { + "2.11.0+1.22.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.8" + } + } + }, + { + "3.0.0+1.22.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.8" + } + } + }, + { + "3.0.1+1.22.3-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.8" + } + } + }, + { + "3.0.2+1.22.6-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.6-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.0.3+1.22.6-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.22.6-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.1.0+1.23.0-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.23.0-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.1.1+1.23.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.23.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.2.0+1.23.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.23.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.3.0+1.23.1-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.23.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.3.1+1.23.8-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.23.8-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.10" + } + } + }, + { + "3.4.0+1.24.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.24.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "3.5.0+1.24.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.24.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "3.5.1+1.24.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.24.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "3.5.2+1.24.2-rootless": { + "app": { + "image": "gitea/gitea", + "tag": "1.24.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "4.0.0+12.0.2-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "12.0.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "4.0.1+12.0.2-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "12.0.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "4.0.2+12.0.2-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "12.0.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.0.0+13.0.2-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "13.0.2-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.0.1+13.0.3-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "13.0.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.0.2+13.0.4-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "13.0.4-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.0.3+13.0.4-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "13.0.4-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.1.0+14.0.1-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "14.0.1-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + }, + { + "5.1.1+14.0.3-rootless": { + "app": { + "image": "forgejo/forgejo", + "tag": "14.0.3-rootless" + }, + "db": { + "image": "postgres", + "tag": "15.13" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "A shared agenda for local communities", + "features": { + "backups": "No", + "email": "No", + "healthcheck": "No", + "image": { + "image": "gancio", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/cisti/gancio" + }, + "status": 0, + "tests": "No", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/485-f4a447197439adc13c2440728f1ba8b6", + "name": "gancio", + "repository": "https://git.coopcloud.tech/coop-cloud/gancio.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/gancio.git", + "versions": [ + { + "0.1.0+1.25.1": { + "app": { + "image": "cisti/gancio", + "tag": "1.25.1" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "A federated link aggregator in Rust", + "features": { + "backups": "No", + "email": "No", + "healthcheck": "Yes", + "image": { + "image": "lemmy", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/dessalines/lemmy" + }, + "status": 1, + "tests": "2", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/432-5751cb07a13ead1bea8249a8f3b5ae68", + "name": "lemmy", + "repository": "https://git.coopcloud.tech/coop-cloud/lemmy.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/lemmy.git", + "versions": [ + { + "0.1.0+0.16.6": { + "app": { + "image": "dessalines/lemmy", + "tag": "0.16.6" + }, + "db": { + "image": "postgres", + "tag": "12-alpine" + }, + "pictrs": { + "image": "asonix/pictrs", + "tag": "0.3.0-beta.12-r1" + }, + "ui": { + "image": "dessalines/lemmy-ui", + "tag": "0.16.6" + }, + "web": { + "image": "nginx", + "tag": "1.20.0" + } + } + }, + { + "0.1.0+0.16.1": { + "app": { + "image": "dessalines/lemmy", + "tag": "0.16.1" + }, + "db": { + "image": "postgres", + "tag": "12-alpine" + }, + "pictrs": { + "image": "asonix/pictrs", + "tag": "0.3.0-beta.12-r1" + }, + "ui": { + "image": "dessalines/lemmy-ui", + "tag": "0.16.1" + }, + "web": { + "image": "nginx", + "tag": "1.20.0" + } + } + }, + { + "0.2.0+0.18.3": { + "app": { + "image": "dessalines/lemmy", + "tag": "0.18.3" + }, + "db": { + "image": "postgres", + "tag": "15-alpine" + }, + "pictrs": { + "image": "asonix/pictrs", + "tag": "0.4.0-beta.19" + }, + "ui": { + "image": "dessalines/lemmy-ui", + "tag": "0.18.3" + }, + "web": { + "image": "nginx", + "tag": "1.20.0" + } + } + } + ], + "website": "" + }, + { + "category": "Utilities", + "default_branch": "main", + "description": "Spin up an onion service for your Co-op Cloud applications", + "features": { + "backups": "Yes", + "email": "N/A", + "healthcheck": "Yes", + "image": { + "image": "onimages/tor:alpine", + "rating": "4", + "source": "upstream", + "url": "https://onionservices.torproject.org/apps/base/containers" + }, + "status": 0, + "tests": "No", + "sso": "N/A" + }, + "icon": "", + "name": "onion", + "repository": "https://git.coopcloud.tech/coop-cloud/onion.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/onion.git", + "versions": [ + { + "1.0.0+alpine": { + "app": { + "image": "tpo/onion-services/onimages/tor", + "tag": "alpine" + } + } + }, + { + "1.0.1+alpine": { + "app": { + "image": "tpo/onion-services/onimages/tor", + "tag": "alpine" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "", + "features": { + "backups": "Yes", + "email": "Yes", + "healthcheck": "3", + "image": { + "image": "vaultwarden/server", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/vaultwarden/server" + }, + "status": 2, + "tests": "No", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/379-697fbe9f0f9481f250fde86e811c1e34", + "name": "vaultwarden", + "repository": "https://git.coopcloud.tech/coop-cloud/vaultwarden.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/vaultwarden.git", + "versions": [ + { + "0.1.0+1.25.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.25.0" + } + } + }, + { + "0.1.1+1.26.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.26.0" + } + } + }, + { + "0.2.0+1.26.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.26.0" + } + } + }, + { + "0.3.0+1.26.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.26.0" + } + } + }, + { + "0.4.0+1.28.1": { + "app": { + "image": "vaultwarden/server", + "tag": "1.28.1" + } + } + }, + { + "0.5.0+1.29.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.29.0" + } + } + }, + { + "0.5.1+1.29.1": { + "app": { + "image": "vaultwarden/server", + "tag": "1.29.1" + } + } + }, + { + "0.6.0+1.29.2": { + "app": { + "image": "vaultwarden/server", + "tag": "1.29.2" + } + } + }, + { + "0.7.0+1.30.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.30.0" + } + } + }, + { + "0.7.1+1.30.1": { + "app": { + "image": "vaultwarden/server", + "tag": "1.30.1" + } + } + }, + { + "0.7.2+1.30.3": { + "app": { + "image": "vaultwarden/server", + "tag": "1.30.3" + } + } + }, + { + "0.8.0+1.31.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.31.0" + } + } + }, + { + "0.9.0+1.32.0": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.0" + } + } + }, + { + "0.9.1+1.32.3": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.3" + } + } + }, + { + "1.0.0+1.32.3": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.3" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.0.1+1.32.5": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.5" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.0.2+1.32.5": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.5" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.0.3+1.32.5": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.5" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.0.4+1.32.7": { + "app": { + "image": "vaultwarden/server", + "tag": "1.32.7" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "1.1.0+1.33.2": { + "app": { + "image": "vaultwarden/server", + "tag": "1.33.2" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "2.0.0+1.33.2": { + "app": { + "image": "vaultwarden/server", + "tag": "1.33.2" + }, + "db": { + "image": "mariadb", + "tag": "10.6" + } + } + }, + { + "2.1.0+1.34.1": { + "app": { + "image": "vaultwarden/server", + "tag": "1.34.1" + }, + "db": { + "image": "mariadb", + "tag": "10.11" + } + } + }, + { + "2.1.1+1.34.3": { + "app": { + "image": "vaultwarden/server", + "tag": "1.34.3" + }, + "db": { + "image": "mariadb", + "tag": "10.11" + } + } + }, + { + "2.1.2+1.35.3": { + "app": { + "image": "vaultwarden/server", + "tag": "1.35.3" + }, + "db": { + "image": "mariadb", + "tag": "10.11" + } + } + }, + { + "2.1.3+1.35.4": { + "app": { + "image": "vaultwarden/server", + "tag": "1.35.4" + }, + "db": { + "image": "mariadb", + "tag": "10.11" + } + } + } + ], + "website": "" + }, + { + "category": "Utilities", + "default_branch": "main", + "description": "Easily and securely send things from one computer to another", + "features": { + "backups": "No", + "email": "No", + "healthcheck": "No", + "image": { + "image": "croc", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/schollz/croc" + }, + "status": 3, + "tests": "No", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/365-d44b58c8c77808fb82c2d9fdfee17800", + "name": "croc", + "repository": "https://git.coopcloud.tech/coop-cloud/croc.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/croc.git", + "versions": [], + "website": "https://github.com/schollz/croc" + }, + { + "category": "Utilities", + "default_branch": "main", + "description": "Community Keycloak SSO user management.", + "features": { + "backups": "No", + "email": "N/A", + "healthcheck": "Yes", + "image": { + "image": "", + "rating": "", + "source": "", + "url": "" + }, + "status": 1, + "tests": "Yes", + "sso": "N/A" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/281-ae6d28ba5409a428cbb3a49d5ef6d739", + "name": "keycloak-collective-portal", + "repository": "https://git.coopcloud.tech/coop-cloud/keycloak-collective-portal.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/keycloak-collective-portal.git", + "versions": [], + "website": "" + }, + { + "category": "Utilities", + "default_branch": "main", + "description": "Manage a node in a local resilient mesh, see https://lores.tech/", + "features": { + "backups": "No", + "email": "N/A", + "healthcheck": "No", + "image": { + "image": "lores-node", + "rating": "4", + "source": "upstream", + "url": "https://ghcr.io/local-resilience-tech/lores-node:latest" + }, + "status": 1, + "tests": "0", + "sso": "N/A" + }, + "icon": "", + "name": "lores-node", + "repository": "https://git.coopcloud.tech/coop-cloud/lores-node.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/lores-node.git", + "versions": [ + { + "0.1.0+0.15.3": { + "app": { + "image": "local-resilience-tech/lores-node", + "tag": "v0.15.3" + } + } + }, + { + "0.2.0+v0.16.0": { + "app": { + "image": "local-resilience-tech/lores-node", + "tag": "v0.16.0" + } + } + }, + { + "0.2.1+v0.16.1": { + "app": { + "image": "local-resilience-tech/lores-node", + "tag": "v0.16.1" + } + } + }, + { + "0.2.2+v0.16.2": { + "app": { + "image": "local-resilience-tech/lores-node", + "tag": "v0.16.2" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "", + "features": { + "backups": "Yes", + "email": "No", + "healthcheck": "", + "image": { + "image": "plausible/analytics", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/plausible/analytics" + }, + "status": 1, + "tests": "", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/ba858ce9af5a5e69074b125fd4f99ac7b43fd1ad4fb82d926541175826037921", + "name": "plausible", + "repository": "https://git.coopcloud.tech/coop-cloud/plausible.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/plausible.git", + "versions": [ + { + "1.1.0+v1.5.1": { + "app": { + "image": "plausible/analytics", + "tag": "v1.5.1" + }, + "db": { + "image": "postgres", + "tag": "13.11-alpine" + }, + "plausible_events_db": { + "image": "clickhouse/clickhouse-server", + "tag": "23.4.2.11-alpine" + } + } + }, + { + "2.0.0+v1.5.1": { + "app": { + "image": "plausible/analytics", + "tag": "v1.5.1" + }, + "db": { + "image": "postgres", + "tag": "13.11" + }, + "plausible_events_db": { + "image": "clickhouse/clickhouse-server", + "tag": "23.4.2.11-alpine" + } + } + }, + { + "3.0.0+v2.0.0": { + "app": { + "image": "plausible/analytics", + "tag": "v2.0.0" + }, + "db": { + "image": "postgres", + "tag": "13.12" + }, + "plausible_events_db": { + "image": "clickhouse/clickhouse-server", + "tag": "23.4.2.11-alpine" + } + } + }, + { + "3.0.1+v2.0.0": { + "app": { + "image": "plausible/analytics", + "tag": "v2.0.0" + }, + "db": { + "image": "postgres", + "tag": "13.12" + }, + "plausible_events_db": { + "image": "clickhouse/clickhouse-server", + "tag": "23.4.2.11-alpine" + } + } + } + ], + "website": "https://github.com/plausible/analytics/" + }, + { + "category": "Utilities", + "default_branch": "main", + "description": "RSS feed for Docker Hub images", + "features": { + "backups": "?", + "email": "?", + "healthcheck": "?", + "image": { + "image": "docker-hub-rss", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/theconnman/docker-hub-rss/" + }, + "status": 0, + "tests": "?", + "sso": "?" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/210-09cd28347983de7287a1af0792371087", + "name": "docker-hub-rss", + "repository": "https://git.coopcloud.tech/coop-cloud/docker-hub-rss.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/docker-hub-rss.git", + "versions": [ + { + "0.1.0+0.4.2": { + "app": { + "image": "theconnman/docker-hub-rss", + "tag": "0.4.2" + } + } + } + ], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "A web-based farm record keeping application. ", + "features": { + "backups": "No", + "email": "No", + "healthcheck": "No", + "image": { + "image": "farmos", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/farmos" + }, + "status": 0, + "tests": "No", + "sso": "No" + }, + "icon": "", + "name": "farmos", + "repository": "https://git.coopcloud.tech/coop-cloud/farmos.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/farmos.git", + "versions": [], + "website": "" + }, + { + "category": "Apps", + "default_branch": "main", + "description": "Funkwhale is a self-hosted, modern free and open-source music server, heavily inspired by Grooveshark.", + "features": { + "backups": "No", + "email": "No", + "healthcheck": "No", + "image": { + "image": "funkwhale", + "rating": "4", + "source": "upstream", + "url": "https://hub.docker.com/r/funkwhale" + }, + "status": 0, + "tests": "No", + "sso": "No" + }, + "icon": "https://git.coopcloud.tech/repo-avatars/455-5c7ee2a7d5ca18dbd1fba168994f9196", + "name": "funkwhale", + "repository": "https://git.coopcloud.tech/coop-cloud/funkwhale.git", + "ssh_url": "ssh://git@git.coopcloud.tech:2222/coop-cloud/funkwhale.git", + "versions": [ + { + "0.1.0+1.25.3": { + "api": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "app": { + "image": "nginx", + "tag": "1.25.3" + }, + "cache": { + "image": "redis", + "tag": "7-alpine" + }, + "celerybeat": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "celeryworker": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "db": { + "image": "postgres", + "tag": "10-alpine" + } + } + }, + { + "0.1.1+1.27.1": { + "api": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "app": { + "image": "nginx", + "tag": "1.27.1" + }, + "cache": { + "image": "redis", + "tag": "7-alpine" + }, + "celerybeat": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "celeryworker": { + "image": "funkwhale/funkwhale", + "tag": "1.2" + }, + "db": { + "image": "postgres", + "tag": "10-alpine" + } + } + } + ], + "website": "" + } +] \ No newline at end of file diff --git a/web/src/services/mock-logs.json b/web/src/services/mock-logs.json new file mode 100644 index 0000000..5c19923 --- /dev/null +++ b/web/src/services/mock-logs.json @@ -0,0 +1,326 @@ +{ + "deploy": { + "logs": [ + { + "type": "info", + "text": "📦 Deploying {appName}..." + }, + { + "type": "command", + "text": "$ abra app deploy {appName}" + }, + { + "type": "info", + "text": "Checking server connection..." + }, + { + "type": "success", + "text": "✓ Connected to server" + }, + { + "type": "info", + "text": "Pulling latest images..." + }, + { + "type": "output", + "text": "latest: Pulling from library/traefik" + }, + { + "type": "output", + "text": "a1b2c3d4: Pull complete" + }, + { + "type": "output", + "text": "e5f6g7h8: Pull complete" + }, + { + "type": "success", + "text": "✓ Images pulled successfully" + }, + { + "type": "info", + "text": "Creating containers..." + }, + { + "type": "output", + "text": "Creating {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Creating {appName}_db_1 ... done" + }, + { + "type": "info", + "text": "Starting services..." + }, + { + "type": "output", + "text": "Starting {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Starting {appName}_db_1 ... done" + }, + { + "type": "success", + "text": "✓ {appName} deployed successfully!" + } + ] + }, + "stop": { + "logs": [ + { + "type": "info", + "text": "🛑 Stopping {appName}..." + }, + { + "type": "command", + "text": "$ abra app stop {appName}" + }, + { + "type": "output", + "text": "Stopping {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Stopping {appName}_db_1 ... done" + }, + { + "type": "success", + "text": "✓ {appName} stopped" + } + ] + }, + "start": { + "logs": [ + { + "type": "info", + "text": "▶️ Starting {appName}..." + }, + { + "type": "command", + "text": "$ abra app start {appName}" + }, + { + "type": "output", + "text": "Starting {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Starting {appName}_db_1 ... done" + }, + { + "type": "success", + "text": "✓ {appName} started" + } + ] + }, + "upgrade": { + "logs": [ + { + "type": "info", + "text": "Upgrading {appName} to {version}..." + }, + { + "type": "command", + "text": "$ abra app upgrade {appName}" + }, + { + "type": "info", + "text": "Pulling new version..." + }, + { + "type": "output", + "text": "{version}: Pulling from library/app" + }, + { + "type": "output", + "text": "x9y8z7w6: Pull complete" + }, + { + "type": "success", + "text": "✓ New version pulled" + }, + { + "type": "info", + "text": "Stopping old containers..." + }, + { + "type": "output", + "text": "Stopping {appName}_app_1 ... done" + }, + { + "type": "info", + "text": "Starting new containers..." + }, + { + "type": "output", + "text": "Starting {appName}_app_1 ... done" + }, + { + "type": "success", + "text": "✓ {appName} upgraded to {version}" + } + ] + }, + "remove": { + "logs": [ + { + "type": "warning", + "text": "⚠️ Removing {appName}..." + }, + { + "type": "command", + "text": "$ abra app remove {appName}" + }, + { + "type": "output", + "text": "Stopping {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Stopping {appName}_db_1 ... done" + }, + { + "type": "output", + "text": "Removing {appName}_app_1 ... done" + }, + { + "type": "output", + "text": "Removing {appName}_db_1 ... done" + }, + { + "type": "output", + "text": "Removing network {appName}_default" + }, + { + "type": "success", + "text": "✓ {appName} removed" + } + ] + }, + "serverRefresh": { + "logs": [ + { + "type": "info", + "text": "🔄 Refreshing server {serverName}..." + }, + { + "type": "command", + "text": "$ abra server ls" + }, + { + "type": "info", + "text": "Fetching server status..." + }, + { + "type": "output", + "text": "Checking connection..." + }, + { + "type": "success", + "text": "✓ Connected" + }, + { + "type": "output", + "text": "Fetching app list..." + }, + { + "type": "success", + "text": "✓ Found apps on {serverName}" + }, + { + "type": "success", + "text": "✓ Server refreshed" + } + ] + }, + "deployAll": { + "logs": [ + { + "type": "info", + "text": "📦 Deploying all apps on {serverName}..." + }, + { + "type": "command", + "text": "$ abra app deploy --all --server {serverName}" + }, + { + "type": "info", + "text": "Found {appCount} apps to deploy" + }, + { + "type": "output", + "text": "Deploying apps in parallel..." + }, + { + "type": "info", + "text": "[1/{appCount}] Deploying app_1..." + }, + { + "type": "success", + "text": "✓ app_1 deployed" + }, + { + "type": "info", + "text": "[2/{appCount}] Deploying app_2..." + }, + { + "type": "success", + "text": "✓ app_2 deployed" + }, + { + "type": "info", + "text": "[3/{appCount}] Deploying app_3..." + }, + { + "type": "success", + "text": "✓ app_3 deployed" + }, + { + "type": "success", + "text": "✓ All apps deployed on {serverName}" + } + ] + }, + "upgradeAll": { + "logs": [ + { + "type": "info", + "text": "Upgrading all apps on {serverName}..." + }, + { + "type": "command", + "text": "$ abra app upgrade --all --server {serverName}" + }, + { + "type": "info", + "text": "Found {upgradeCount} apps with available upgrades" + }, + { + "type": "output", + "text": "Upgrading apps sequentially..." + }, + { + "type": "info", + "text": "[1/{upgradeCount}] Upgrading app_1..." + }, + { + "type": "success", + "text": "✓ app_1 upgraded" + }, + { + "type": "info", + "text": "[2/{upgradeCount}] Upgrading app_2..." + }, + { + "type": "success", + "text": "✓ app_2 upgraded" + }, + { + "type": "success", + "text": "✓ All apps upgraded on {serverName}" + } + ] + } +} \ No newline at end of file diff --git a/web/src/services/mock-servers.json b/web/src/services/mock-servers.json new file mode 100644 index 0000000..8aec675 --- /dev/null +++ b/web/src/services/mock-servers.json @@ -0,0 +1,30 @@ +[ + { + "host": "mydomain.com", + "name": "mydomain.com" + }, + { + "host": "coop.test.org", + "name": "coop.test.org" + }, + { + "host": "internal.website.com", + "name": "internal.website.com" + }, + { + "host": "internal.server.net", + "name": "internal.server.net" + }, + { + "host": "internal.intranet.site.com", + "name": "internal.intranet.site.com" + }, + { + "host": "internal.test.org", + "name": "internal.test.org" + }, + { + "host": "orgsite.org", + "name": "orgsite.org" + } +] \ No newline at end of file diff --git a/web/src/services/mockApi.ts b/web/src/services/mockApi.ts new file mode 100644 index 0000000..09a5cfb --- /dev/null +++ b/web/src/services/mockApi.ts @@ -0,0 +1,145 @@ +import type { AbraRecipe, AbraServer, ServerAppsResponse } from '../types'; +import appsData from './mock-apps.json'; +import serversData from './mock-servers.json'; +import logsData from './mock-logs.json'; +import catalogue from './mock-catalogue.json'; + + +// Log entry type +export type LogEntry = { + type: 'info' | 'success' | 'error' | 'warning' | 'command' | 'output'; + text: string; + timestamp: Date; +}; + +// Type for the imported JSON structure +type LogTemplate = { + type: 'info' | 'success' | 'error' | 'warning' | 'command' | 'output'; + text: string; +}; + +type LogsDataStructure = { + [key: string]: { + logs: LogTemplate[]; + }; +}; + +// Simulate API delay +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +// Helper to replace template variables like {appName}, {version} +const replaceVars = (text: string, vars: Record): string => { + let result = text; + Object.entries(vars).forEach(([key, value]) => { + result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value)); + }); + return result; +}; + +// Helper to process log templates into LogEntry objects +const processLogs = ( + action: string, + vars: Record +): LogEntry[] => { + console.log('Processing logs for action:', action); + console.log('Loaded logsData:', logsData); + + const typedLogsData = logsData as LogsDataStructure; + const actionData = typedLogsData[action]; + + if (!actionData || !actionData.logs) { + console.error(`No logs found for action: ${action}`); + return []; + } + + console.log('Found logs:', actionData.logs); + + const templates = actionData.logs; + return templates + .filter(template => template && template.type && template.text) // Filter out any undefined/malformed entries + .map(template => ({ + type: template.type, + text: replaceVars(template.text, vars), + timestamp: new Date(), + })); +}; + +export const mockApiService = { + async getAppsGrouped(): Promise { + await delay(500); + return appsData as ServerAppsResponse; + }, + + async getServers(): Promise { + await delay(300); + return serversData as AbraServer[]; + }, + + async deployApp(appName: string, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('deploy', { appName }); + + for (const log of logs) { + await delay(200); + onLog?.(log); + } + }, + + async stopApp(appName: string, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('stop', { appName }); + + for (const log of logs) { + await delay(150); + onLog?.(log); + } + }, + + async upgradeApp(appName: string, version: string, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('upgrade', { appName, version }); + + for (const log of logs) { + await delay(200); + onLog?.(log); + } + }, + + async removeApp(appName: string, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('remove', { appName }); + + for (const log of logs) { + await delay(180); + onLog?.(log); + } + }, + + async refreshServer(serverName: string, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('serverRefresh', { serverName }); + + for (const log of logs) { + await delay(200); + onLog?.(log); + } + }, + + async deployAllApps(serverName: string, appCount: number, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('deployAll', { serverName, appCount }); + + for (const log of logs) { + await delay(250); + onLog?.(log); + } + }, + + async upgradeAllApps(serverName: string, upgradeCount: number, onLog?: (log: LogEntry) => void): Promise { + const logs = processLogs('upgradeAll', { serverName, upgradeCount }); + + for (const log of logs) { + await delay(250); + onLog?.(log); + } + }, + // recipe catalog imports + async getRecipes(): Promise { + await delay(300); + return catalogue as AbraRecipe[]; + } +}; \ No newline at end of file diff --git a/web/src/types/index.ts b/web/src/types/index.ts new file mode 100644 index 0000000..b20bce4 --- /dev/null +++ b/web/src/types/index.ts @@ -0,0 +1,126 @@ +export interface ApiError { + message: string; + status: number; +} + +export interface AbraApp { + server: string; + recipe: string; + appName: string; + domain: string; + status: 'deployed' | 'stopped' | 'unknown'; + chaos: string; + chaosVersion: string; + version: string; + upgrade: string; +} + +export interface AbraServer { + name: string; + host: string; +} + +export interface ServerAppsResponse { + [serverName: string]: { + apps: AbraApp[]; + appCount: number; + versionCount: number; + unversionedCount: number; + latestCount: number; + upgradeCount: number; + backupEnabled: boolean; + }; +} + +export interface AppWithServer extends AbraApp { + serverStats: { + appCount: number; + upgradeCount: number; + }; +} + +export interface AbraAppService { + service: string; + chaos: boolean; + created: string; + image: string; + ports: string; + state: string; + status: string; + version: string; +} +export interface AbraServiceState { + name: string; + err: string; + id: string; + status: string; + retries: number; + health: string; + rollback: boolean; + failed: boolean; +} +export interface DeployState { + count: number; + total: number; + failed: boolean; + quit: boolean; +} + +export type DeployEvent = + | { + type: "service"; + data: AbraServiceState; + } + | { + type: "done"; + data: DeployState; + } +export interface Image { + image: string; + rating: string + source: string + url: string +} +type RecipeVersions = Record>[]; +export interface ServiceMeta { + image: string; + tag: string +} +export interface Features { + backups: string; + email: string; + healthcheck: string; + image: Image; + status: number; + tests: string; + sso: string; +} +export interface AbraRecipe { + category: string; + default_branch: string; + description: string; + features: Features; + icon: string; + name: string; + repository: string; + ssh_url: string; + versions: RecipeVersions; + website: string; +} + +export interface AbraAppSecret { + name: string; + version: string; + value: string; + created: boolean; +} + +export interface File { + content: string; +} + +export interface AppSnapshot { + createdAt: string; + snapshot: string; + version: string; +} \ No newline at end of file diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..3e34260 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) \ No newline at end of file