commit 09ba4114c1ff5868897a56bc0f9e3d3afa329b9e Author: Alex Mickelson Date: Mon Dec 30 11:42:12 2024 -0700 Initial commit diff --git a/.github/workflows/backup-zfs.yml b/.github/workflows/backup-zfs.yml new file mode 100644 index 0000000..50e6933 --- /dev/null +++ b/.github/workflows/backup-zfs.yml @@ -0,0 +1,46 @@ +name: ZFS Backup +on: + schedule: + - cron: 0 1 * * * + workflow_dispatch: +jobs: + update-infrastructure: + runs-on: [self-hosted, home-server] + steps: + - name: run syncoid + run: | + zpool status + echo "" + zfs list + echo "" + syncoid \ + --recursive \ + --no-privilege-elevation \ + data-ssd/data \ + backup/data + + syncoid \ + --recursive \ + --no-privilege-elevation \ + data-ssd/media \ + backup/media + # steps: + # - name: run syncoid + # run: | + # zpool status + # echo "" + # zfs list + # echo "" + # syncoid \ + # --recursive \ + # --no-privilege-elevation \ + # --no-rollback \ + # data-ssd/data \ + # backup/data + + # syncoid \ + # --recursive \ + # --no-privilege-elevation \ + # --no-rollback \ + # data-ssd/media \ + # backup/media \ No newline at end of file diff --git a/.github/workflows/deploy-bot.yml b/.github/workflows/deploy-bot.yml new file mode 100644 index 0000000..9110077 --- /dev/null +++ b/.github/workflows/deploy-bot.yml @@ -0,0 +1,15 @@ +name: Deploy Discord Bot +on: + workflow_dispatch: +jobs: + run-python: + runs-on: [self-hosted, home-server] + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: deploy bot + env: + DISCORD_SECRET: ${{ secrets.DISCORD_SECRET }} + run: | + cd discord-bot + ./run.sh diff --git a/.github/workflows/update-home-server.yml b/.github/workflows/update-home-server.yml new file mode 100644 index 0000000..1d19806 --- /dev/null +++ b/.github/workflows/update-home-server.yml @@ -0,0 +1,51 @@ +name: Update home server containers +on: [push, workflow_dispatch] +jobs: + update-repo: + runs-on: [home-server] + steps: + - name: checkout repo + working-directory: /home/github/infrastructure + run: | + if [ -d "infrastructure" ]; then + cd infrastructure + echo "Infrastructure folder exists. Resetting to the most recent commit." + git reset --hard HEAD + git pull https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} $(git rev-parse --abbrev-ref HEAD) + else + git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + fi + update-infrastructure: + runs-on: [home-server] + needs: update-repo + steps: + - name: update home server containers + env: + MY_GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} + HOMEASSISTANT_TOKEN: ${{ secrets.HOMEASSISTANT_TOKEN }} + GRAFANA_PASSWORD: ${{ secrets.GRAFANA_PASSWORD }} + CLOUDFLARE_CONFIG: ${{ secrets.CLOUDFLARE_CONFIG }} + working-directory: /home/github/infrastructure/infrastructure + run: | + # git secret reveal -f + pwd + # echo "$CLOUDFLARE_CONFIG" > /data/cloudflare/cloudflare.ini + cd home-server + docker pull nextcloud:production + docker compose pull + docker compose build + docker compose up -d + # docker restart reverse-proxy + # docker exec reverse-proxy nginx -t + # docker exec reverse-proxy nginx -s reload + + update-pihole: + runs-on: [home-server] + needs: update-repo + steps: + - working-directory: /home/github/infrastructure/infrastructure + run: | + cd dns + docker compose pull + docker compose up -d + diff --git a/.github/workflows/update-playlist.yml b/.github/workflows/update-playlist.yml new file mode 100644 index 0000000..a35a172 --- /dev/null +++ b/.github/workflows/update-playlist.yml @@ -0,0 +1,37 @@ +name: Manage Jellyfin Playlists +on: + workflow_dispatch: + schedule: + - cron: '0 * * * *' +jobs: + run-python: + runs-on: [self-hosted, home-server] + steps: + - name: checkout repo + working-directory: /home/github/infrastructure + run: | + if [ -d "infrastructure" ]; then + cd infrastructure + echo "Infrastructure folder exists. Resetting to the most recent commit." + git reset --hard HEAD + git pull https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} $(git rev-parse --abbrev-ref HEAD) + else + git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git + fi + - name: Run Python script + env: + JELLYFIN_USER: ${{ secrets.JELLYFIN_USER }} + JELLYFIN_PASSWORD: ${{ secrets.JELLYFIN_PASSWORD }} + working-directory: /home/github/infrastructure/infrastructure + run: | + docker build -t jellyfin_management -f jellyfin/Dockerfile . + docker run --rm \ + -e JELLYFIN_USER=$JELLYFIN_USER \ + -e JELLYFIN_PASSWORD=$JELLYFIN_PASSWORD \ + jellyfin_management \ + -m jellyfin.update_all_songs_playlist + docker run --rm \ + -e JELLYFIN_USER=$JELLYFIN_USER \ + -e JELLYFIN_PASSWORD=$JELLYFIN_PASSWORD \ + jellyfin_management \ + -m jellyfin.update_unindexed diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..587480d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.gitsecret/keys/random_seed +!*.secret +home-pi/dns/cloudflare.env +linode/wireguard/wg-easy.env +home-pi/plex.env +*.env +**/*.env +__pycache__/ +.mypy_cache/ +.venv/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c63e326 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.mypyEnabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..84da982 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +![home server update](https://github.com/alexmickelson/infrastructure/actions/workflows/update-home-server.yml/badge.svg) + +[![ZFS Backup](https://github.com/alexmickelson/infrastructure/actions/workflows/backup-zfs.yml/badge.svg)](https://github.com/alexmickelson/infrastructure/actions/workflows/backup-zfs.yml) + +[![Manage Jellyfin Playlists](https://github.com/alexmickelson/infrastructure/actions/workflows/update-playlist.yml/badge.svg)](https://github.com/alexmickelson/infrastructure/actions/workflows/update-playlist.yml) diff --git a/discord-bot/.dockerignore b/discord-bot/.dockerignore new file mode 100644 index 0000000..f090c06 --- /dev/null +++ b/discord-bot/.dockerignore @@ -0,0 +1,7 @@ +.vscode/ +virtualenv/ +songs/ +.mypy_cache/ +Dockerfile +node_modules/ +venv/ \ No newline at end of file diff --git a/discord-bot/.gitignore b/discord-bot/.gitignore new file mode 100644 index 0000000..c081c0d --- /dev/null +++ b/discord-bot/.gitignore @@ -0,0 +1,2 @@ +songs/ +venv/ diff --git a/discord-bot/.vscode/settings.json b/discord-bot/.vscode/settings.json new file mode 100644 index 0000000..3874b78 --- /dev/null +++ b/discord-bot/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.linting.mypyEnabled": false, + "python.linting.enabled": true, + "python.linting.flake8Enabled": false, + "python.linting.pylintEnabled": false, + "python.linting.banditEnabled": true +} \ No newline at end of file diff --git a/discord-bot/Dockerfile b/discord-bot/Dockerfile new file mode 100644 index 0000000..b7e3328 --- /dev/null +++ b/discord-bot/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20 as build-stage + +WORKDIR /app + +COPY client/package.json client/package-lock.json ./ +RUN npm install + +COPY client/ ./ +RUN npm run build + +FROM python:3.10 +RUN apt-get update && apt-get install -y ffmpeg +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY src src +COPY main.py main.py +RUN mkdir songs + +RUN mkdir client +COPY --from=build-stage /app/dist /client + +ENTRYPOINT [ "fastapi", "run", "main.py", "--port", "5677" ] + diff --git a/discord-bot/client/.eslintrc.cjs b/discord-bot/client/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/discord-bot/client/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/discord-bot/client/.gitignore b/discord-bot/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/discord-bot/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/discord-bot/client/README.md b/discord-bot/client/README.md new file mode 100644 index 0000000..0d6babe --- /dev/null +++ b/discord-bot/client/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/discord-bot/client/index.html b/discord-bot/client/index.html new file mode 100644 index 0000000..3fd548f --- /dev/null +++ b/discord-bot/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/discord-bot/client/package-lock.json b/discord-bot/client/package-lock.json new file mode 100644 index 0000000..39867ee --- /dev/null +++ b/discord-bot/client/package-lock.json @@ -0,0 +1,3673 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sass": "^1.77.6" + }, + "devDependencies": { + "@types/bootstrap": "^5.2.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "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/core/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/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "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.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "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.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/bootstrap": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", + "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz", + "integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/type-utils": "7.15.0", + "@typescript-eslint/utils": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz", + "integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/typescript-estree": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz", + "integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz", + "integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.15.0", + "@typescript-eslint/utils": "7.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz", + "integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz", + "integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/visitor-keys": "7.15.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz", + "integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.15.0", + "@typescript-eslint/types": "7.15.0", + "@typescript-eslint/typescript-estree": "7.15.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz", + "integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.15.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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/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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "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": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "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/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "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/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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.818", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", + "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "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" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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/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/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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/eslint/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/eslint/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/eslint/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/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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.4" + }, + "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/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.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==", + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "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/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "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.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.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.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.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/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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.1.2", + "picocolors": "^1.0.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/vite": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "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/discord-bot/client/package.json b/discord-bot/client/package.json new file mode 100644 index 0000000..eff31df --- /dev/null +++ b/discord-bot/client/package.json @@ -0,0 +1,32 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sass": "^1.77.6" + }, + "devDependencies": { + "@types/bootstrap": "^5.2.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } +} diff --git a/discord-bot/client/public/vite.svg b/discord-bot/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/discord-bot/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/discord-bot/client/src/App.tsx b/discord-bot/client/src/App.tsx new file mode 100644 index 0000000..49e320a --- /dev/null +++ b/discord-bot/client/src/App.tsx @@ -0,0 +1,14 @@ +import { CurrentSong } from "./components/CurrentSong"; +import { PlaybackInfo } from "./components/PlaybackInfo"; +import { SongQueue } from "./components/SongQueue"; + +export const App = () => { + return ( +
+

Discord Music

+ + + +
+ ); +}; diff --git a/discord-bot/client/src/assets/react.svg b/discord-bot/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/discord-bot/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/discord-bot/client/src/components/CurrentSong.tsx b/discord-bot/client/src/components/CurrentSong.tsx new file mode 100644 index 0000000..d6966af --- /dev/null +++ b/discord-bot/client/src/components/CurrentSong.tsx @@ -0,0 +1,26 @@ +import { useWebSocket } from "../contexts/useWebSocket"; +import { Slider } from "./Slider"; + +export const CurrentSong = () => { + const { ws, playbackInfo, sendMessage } = useWebSocket(); + return ( + <> + {playbackInfo && ( +
+

Playing Song

+
{playbackInfo.file_name}
+ {ws && ( + { + sendMessage({ action: "set_playback", position: v }); + }} + /> + )} +
+ )} + + ); +}; diff --git a/discord-bot/client/src/components/PlaybackInfo.tsx b/discord-bot/client/src/components/PlaybackInfo.tsx new file mode 100644 index 0000000..f9ad002 --- /dev/null +++ b/discord-bot/client/src/components/PlaybackInfo.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { useInfoTask } from "./useInfoTask"; +import { useWebSocket } from "../contexts/useWebSocket"; + +export const PlaybackInfo: React.FC = () => { + const { ws, error, message, botStatus } = useWebSocket(); + + useInfoTask(ws); + + return ( +
+
+
+
Status Messages
+ {botStatus &&
status: {botStatus}
} + {error &&
error: {error}
} + {message &&
message: {message}
} +
+
+
+ ); +}; diff --git a/discord-bot/client/src/components/Slider.scss b/discord-bot/client/src/components/Slider.scss new file mode 100644 index 0000000..fd2cb48 --- /dev/null +++ b/discord-bot/client/src/components/Slider.scss @@ -0,0 +1,38 @@ +@import "../../node_modules/bootstrap/scss/bootstrap.scss"; + +:root { + --slider-color: var(--bs-primary); + --slider-background-color: var(--bs-primary-bg-subtle); +} + + +.slider { + height: 15px; + border-radius: 5px; + background: var(--slider-background-color); + outline: none; + transition: opacity 0.2s; + opacity: .5; + + &:hover { + opacity: 1; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: var(--slider-color); + cursor: pointer; + } + + &::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: var(--slider-color); + cursor: pointer; + } +} \ No newline at end of file diff --git a/discord-bot/client/src/components/Slider.tsx b/discord-bot/client/src/components/Slider.tsx new file mode 100644 index 0000000..2f9cc58 --- /dev/null +++ b/discord-bot/client/src/components/Slider.tsx @@ -0,0 +1,44 @@ +import { ChangeEvent, FC, useEffect, useState } from "react"; +import "./Slider.scss"; + +interface SliderProps { + min: number; + max: number; + current: number; + onChange: (value: number) => void; +} +export const Slider: FC = ({ min, max, current, onChange }) => { + const [localValue, setLocalValue] = useState(current); + const [isDragging, setIsDragging] = useState(false); + + const handleChange = (e: ChangeEvent) => { + setLocalValue(Number(e.target.value)); + }; + + const handleMouseDown = () => { + setIsDragging(true); + }; + + const handleMouseUp = () => { + setIsDragging(false); + onChange(localValue); + }; + useEffect(() => { + if (!isDragging) setLocalValue(current); + }, [current, isDragging]); + + return ( +
+ +
+ ); +}; diff --git a/discord-bot/client/src/components/SongQueue.module.scss b/discord-bot/client/src/components/SongQueue.module.scss new file mode 100644 index 0000000..a2c7593 --- /dev/null +++ b/discord-bot/client/src/components/SongQueue.module.scss @@ -0,0 +1,8 @@ +@import "../../node_modules/bootstrap/scss/bootstrap.scss"; + +.songListItem { + height: 3em; + &:hover { + @extend .bg-dark-subtle; + } +} \ No newline at end of file diff --git a/discord-bot/client/src/components/SongQueue.tsx b/discord-bot/client/src/components/SongQueue.tsx new file mode 100644 index 0000000..e8fd193 --- /dev/null +++ b/discord-bot/client/src/components/SongQueue.tsx @@ -0,0 +1,63 @@ +import { useWebSocket } from "../contexts/useWebSocket"; +import classes from "./SongQueue.module.scss"; + +export const SongQueue = () => { + const { songQueue, sendMessage } = useWebSocket(); + + return ( +
+ {songQueue && ( +
+
    + {songQueue.song_file_list.map((s, i) => { + const isCurrent = i === songQueue.position; + return ( +
  • +
    +
    + {!isCurrent && ( + { + sendMessage({ + action: "set_position", + position: i, + }); + }} + > + )} + {isCurrent && ( + { + // send pause message + // sendMessage({ + // action: "set_position", + // position: i, + // }); + }} + > + )} +
    +
    + {s.filename + .substring(s.filename.lastIndexOf("/") + 1) + .replace(".mp3", "")} +
    +
    +
  • + ); + })} +
+
+ )} +
+ ); +}; diff --git a/discord-bot/client/src/components/useInfoTask.ts b/discord-bot/client/src/components/useInfoTask.ts new file mode 100644 index 0000000..50cdbde --- /dev/null +++ b/discord-bot/client/src/components/useInfoTask.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; + +const updateInterval = 100; + +const getPlaybackInfo = (ws: WebSocket) => { + ws.send(JSON.stringify({ action: "get_playback_info" })); +}; +export const useInfoTask = (websocket?: WebSocket) => { + useEffect(() => { + const interval = setInterval(() => { + if(websocket) + getPlaybackInfo(websocket); + }, updateInterval); + + return () => clearInterval(interval); + }, [websocket]); +}; diff --git a/discord-bot/client/src/contexts/WebSocketContext.tsx b/discord-bot/client/src/contexts/WebSocketContext.tsx new file mode 100644 index 0000000..8f904e0 --- /dev/null +++ b/discord-bot/client/src/contexts/WebSocketContext.tsx @@ -0,0 +1,82 @@ +import { + FC, + ReactNode, + useEffect, + useState, +} from "react"; +import { BotResponse, PlaybackInfoData, SongQueue } from "../models"; +import { WebSocketContext } from "./useWebSocket"; + +export const WebSocketProvider: FC<{ children: ReactNode }> = ({ + children, +}) => { + const [ws, setWs] = useState(); + const [playbackInfo, setPlaybackInfo] = useState< + PlaybackInfoData | undefined + >(); + const [songQueue, setSongQueue] = useState(); + const [error, setError] = useState(""); + const [message, setMessage] = useState(""); + const [botStatus, setBotStatus] = useState(); + + useEffect(() => { + const websocket = new WebSocket(`ws://server.alexmickelson.guru:5678/`); + // const websocket = new WebSocket(`ws://${window.location.hostname}:5678/`); + + setWs(websocket); + + websocket.onopen = () => { + console.log("websocket connected"); + websocket.send(JSON.stringify({ action: "get_playback_info" })); + }; + + websocket.onmessage = (event) => { + const response: BotResponse = JSON.parse(event.data); + setBotStatus(response.status); + if (response.message_type === "ERROR") { + setError(response.error ?? ""); + } else if (response.message_type === "MESSAGE") { + setMessage(response.message ?? ""); + } else if (response.message_type === "PLAYBACK_INFORMATION") { + setPlaybackInfo(response.playback_information); + setSongQueue(response.song_queue); + } + }; + + websocket.onerror = (event: Event) => { + console.log(event); + setError("WebSocket error occurred."); + }; + + websocket.onclose = () => { + console.log("WebSocket connection closed"); + }; + + return () => { + setWs(undefined); + websocket.close(); + }; + }, []); + + const sendMessage = (message: unknown) => { + if (ws) { + ws.send(JSON.stringify(message)); + } + }; + + return ( + + {children} + + ); +}; diff --git a/discord-bot/client/src/contexts/useWebSocket.ts b/discord-bot/client/src/contexts/useWebSocket.ts new file mode 100644 index 0000000..94163bb --- /dev/null +++ b/discord-bot/client/src/contexts/useWebSocket.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from "react"; +import { PlaybackInfoData, SongQueue } from "../models"; + +interface WebSocketContextType { + ws: WebSocket | undefined; + error: string; + message: string; + botStatus: string | undefined; + playbackInfo: PlaybackInfoData | undefined; + songQueue: SongQueue | undefined; + sendMessage: (message: unknown) => void; +} + +export const WebSocketContext = createContext( + undefined +); + +export const useWebSocket = () => { + const context = useContext(WebSocketContext); + if (!context) { + throw new Error("useWebSocket must be used within a WebSocketProvider"); + } + return context; +}; diff --git a/discord-bot/client/src/main.tsx b/discord-bot/client/src/main.tsx new file mode 100644 index 0000000..1dd35d3 --- /dev/null +++ b/discord-bot/client/src/main.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "bootstrap"; +import "bootstrap/scss/bootstrap.scss"; +import "bootstrap-icons/font/bootstrap-icons.css"; +import { App } from "./App"; +import { WebSocketProvider } from "./contexts/WebSocketContext"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/discord-bot/client/src/models.ts b/discord-bot/client/src/models.ts new file mode 100644 index 0000000..6546ac3 --- /dev/null +++ b/discord-bot/client/src/models.ts @@ -0,0 +1,27 @@ +export enum BotStatus { + PLAYING = "Playing", + Idle = "Idle", +} + +export interface PlaybackInfoData { + file_name: string; + current_position: number; + duration: number; +} + +export interface SongQueue { + song_file_list: { + filename: string; + duration: number; + }[]; + position: number; +} + +export interface BotResponse { + message_type: "PLAYBACK_INFORMATION" | "ERROR" | "MESSAGE"; + status: BotStatus; + error?: string; + message?: string; + playback_information?: PlaybackInfoData; + song_queue?: SongQueue; +} \ No newline at end of file diff --git a/discord-bot/client/src/vite-env.d.ts b/discord-bot/client/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/discord-bot/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/discord-bot/client/tsconfig.app.json b/discord-bot/client/tsconfig.app.json new file mode 100644 index 0000000..d739292 --- /dev/null +++ b/discord-bot/client/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/discord-bot/client/tsconfig.json b/discord-bot/client/tsconfig.json new file mode 100644 index 0000000..ea9d0cd --- /dev/null +++ b/discord-bot/client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/discord-bot/client/tsconfig.node.json b/discord-bot/client/tsconfig.node.json new file mode 100644 index 0000000..3afdd6e --- /dev/null +++ b/discord-bot/client/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/discord-bot/client/vite.config.ts b/discord-bot/client/vite.config.ts new file mode 100644 index 0000000..5a33944 --- /dev/null +++ b/discord-bot/client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/discord-bot/main.py b/discord-bot/main.py new file mode 100644 index 0000000..11ab30d --- /dev/null +++ b/discord-bot/main.py @@ -0,0 +1,114 @@ +from enum import Enum +import os +from pprint import pprint +from typing import Optional, Set +import discord +from discord.ext import commands +from threading import Thread +from dotenv import load_dotenv +from fastapi.concurrency import asynccontextmanager +from pydantic import BaseModel +import asyncio +import websockets +import json +import time +from src.models import BotResponse, BotStatus, MessageType, PlaybackInformation +from src.my_voice_client import get_voice_client, set_voice_client +from src.playback_service import ( + get_filename_and_starttime, + get_status, + handle_message, + handle_new_song_on_queue, + pause_song, + play_current_song, + start_time_now, +) +from src.song_queue import add_to_queue, get_current_metadata, handle_song_end, has_current_song, move_to_last_song_in_queue + +load_dotenv() + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + + +bot = commands.Bot(command_prefix="!", intents=discord.Intents.all()) +connected_clients: Set[websockets.WebSocketServerProtocol] = set() + +async def broadcast_bot_response(response: BotResponse): + if connected_clients: + await asyncio.wait( + [ + asyncio.create_task(client.send(response.model_dump_json())) + for client in connected_clients + ] + ) + else: + raise TypeError("Passing coroutines is forbidden, use tasks explicitly.") + +async def send_response_message( + websocket: websockets.WebSocketServerProtocol, response: BotResponse +): + await websocket.send(response.model_dump_json()) + +async def websocket_handler(websocket: websockets.WebSocketServerProtocol, path: str): + connected_clients.add(websocket) + try: + async for message in websocket: + data = json.loads(message) + response = handle_message(data) + await send_response_message(websocket, response) + + except websockets.ConnectionClosedError as e: + print(f"Connection closed with error: {e}") + except Exception as e: + print(f"Unexpected error: {e}") + raise e + finally: + connected_clients.remove(websocket) + print("WebSocket connection closed") + +@bot.event +async def on_ready(): + print("Bot is ready") + +@bot.command(name="play", pass_context=True) +async def play(ctx: commands.Context, url: str): + print("playing", url) + channel = ctx.message.author.voice.channel + + if ctx.voice_client is None: + set_voice_client(await channel.connect()) + add_to_queue(url) + handle_new_song_on_queue() + +@bot.command(pass_context=True) +async def stop(ctx: commands.Context): + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + voice_client.stop() + await voice_client.disconnect() + await ctx.send("Stopped playing") + +@bot.command(pass_context=True) +async def pause(ctx: commands.Context): + pause_song() + +def run_websocket(): + print("started websocket") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + start_server = websockets.serve(websocket_handler, "0.0.0.0", 5678) + loop.run_until_complete(start_server) + loop.run_forever() + + + +@asynccontextmanager +async def lifespan(app: FastAPI): + Thread(target=run_websocket).start() + Thread(target=lambda: bot.run(os.getenv("DISCORD_SECRET"))).start() + yield + +app = FastAPI(lifespan=lifespan) + +app.mount("/", StaticFiles(directory="./client", html=True), name="static") \ No newline at end of file diff --git a/discord-bot/requirements.txt b/discord-bot/requirements.txt new file mode 100644 index 0000000..10a6d86 --- /dev/null +++ b/discord-bot/requirements.txt @@ -0,0 +1,9 @@ +discord +yt_dlp +PyNaCl +python-dotenv +websockets +ffmpeg +pydantic +mutagen +fastapi \ No newline at end of file diff --git a/discord-bot/run.sh b/discord-bot/run.sh new file mode 100755 index 0000000..891224c --- /dev/null +++ b/discord-bot/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +docker pull node:20 +docker pull python:3.10 +docker build -t discord-bot . +# docker run -it --rm discord-bot + + +echo $DISCORD_SECRET +docker rm -f discord-bot || true +docker run -d \ + --name discord-bot \ + -e DISCORD_SECRET=$DISCORD_SECRET \ + --restart always\ + -p 0.0.0.0:5677:5677 \ + -p 0.0.0.0:5678:5678 \ + discord-bot diff --git a/discord-bot/src/__init__.py b/discord-bot/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/discord-bot/src/models.py b/discord-bot/src/models.py new file mode 100644 index 0000000..7602e8c --- /dev/null +++ b/discord-bot/src/models.py @@ -0,0 +1,39 @@ +from enum import Enum +from typing import List, Optional +from pydantic import BaseModel + + +class BotStatus(str, Enum): + PLAYING = "Playing" + IDLE = "Idle" + + +class MessageType(str, Enum): + PLAYBACK_INFORMATION = "PLAYBACK_INFORMATION" + ERROR = "ERROR" + MESSAGE = "MESSAGE" + + +class SongItem(BaseModel): + filename: str + duration: int + + +class SongQueueStatus(BaseModel): + song_file_list: list[SongItem] + position: int + + +class PlaybackInformation(BaseModel): + file_name: str + current_position: float + duration: float + + +class BotResponse(BaseModel): + message_type: MessageType + status: BotStatus + error: Optional[str] = None + message: Optional[str] = None + playback_information: Optional[PlaybackInformation] = None + song_queue: Optional[SongQueueStatus] = None diff --git a/discord-bot/src/my_voice_client.py b/discord-bot/src/my_voice_client.py new file mode 100644 index 0000000..c719c5d --- /dev/null +++ b/discord-bot/src/my_voice_client.py @@ -0,0 +1,14 @@ +from discord import VoiceClient + + +__voice_client: VoiceClient | None = None + + +def get_voice_client(): + global __voice_client + return __voice_client + + +def set_voice_client(client: VoiceClient | None): + global __voice_client + __voice_client = client diff --git a/discord-bot/src/playback_service.py b/discord-bot/src/playback_service.py new file mode 100644 index 0000000..7f89a85 --- /dev/null +++ b/discord-bot/src/playback_service.py @@ -0,0 +1,192 @@ +import time +import discord +from src.models import BotResponse, BotStatus, MessageType, PlaybackInformation +from src.my_voice_client import get_voice_client +from src.song_queue import ( + get_current_metadata, + get_queue_status, + handle_song_end, + has_current_song, + move_to_last_song_in_queue, + set_current_song_start_time, + set_queue_position, +) + +pause_offset = -1 + + +def after_playing(error): + if error: + print(f"Error during playback: {error}") + fileName, duration, start_time = get_current_metadata() + print(f"Finished playing {fileName}") + + fileName, duration, start_time = get_current_metadata() + current_playing_time = time.time() - start_time + + if current_playing_time > (duration - 1): + # song ended + handle_song_end() + if has_current_song(): + print("start next song") + play_current_song() + else: + print("end of queue") + else: + print("not changing song because it is still playing") + + +def change_playback_position(position: int): + fileName, duration, start_time = get_current_metadata() + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + voice_client.pause() + audio = discord.FFmpegPCMAudio( + source=fileName, before_options=f"-ss {position}" + ) + voice_client.play(audio, after=after_playing) + set_current_song_start_time(time.time() - position) + return {"status": "Playback position changed"} + else: + print("cannot change position, no song playing") + return None + + +def play_current_song(): + if has_current_song(): + fileName, duration, start_time = get_current_metadata() + start_time_now() + get_voice_client().play( + discord.FFmpegPCMAudio(source=fileName), after=after_playing + ) + + +def get_status(): + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + return BotStatus.PLAYING + return BotStatus.IDLE + + +def get_playback_info(): + fileName, duration, start_time = get_current_metadata() + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + elapsed_time = time.time() - start_time + + return PlaybackInformation( + file_name=fileName, + current_position=elapsed_time, + duration=duration, + ) + else: + return None + + +def handle_message(data) -> BotResponse: + if "action" not in data: + response = BotResponse( + message_type=MessageType.ERROR, + status=get_status(), + error="Invalid request, action is required", + ) + return response + + if data["action"] == "set_playback": + if "position" not in data: + response = BotResponse( + message_type=MessageType.ERROR, + status=get_status(), + error="Invalid request, position is required", + ) + return response + + result = change_playback_position(data["position"]) + if result: + response = BotResponse( + message_type=MessageType.MESSAGE, + status=get_status(), + message="position changed", + ) + return response + else: + response = BotResponse( + message_type=MessageType.ERROR, + status=get_status(), + error="unable to change position", + ) + return response + elif data["action"] == "set_position": + if "position" not in data: + response = BotResponse( + message_type=MessageType.ERROR, + status=get_status(), + error="Invalid request, position is required", + ) + return response + set_queue_position(data["position"]) + get_voice_client().stop() + play_current_song() + info = get_playback_info() + response = BotResponse( + message_type=MessageType.PLAYBACK_INFORMATION, + status=BotStatus.PLAYING if info else BotStatus.IDLE, + playback_information=info, + song_queue=get_queue_status(), + ) + return response + + elif data["action"] == "get_playback_info": + if not has_current_song(): + return BotResponse( + message_type=MessageType.PLAYBACK_INFORMATION, + status=BotStatus.IDLE, + playback_information=None, + song_queue=get_queue_status(), + ) + info = get_playback_info() + response = BotResponse( + message_type=MessageType.PLAYBACK_INFORMATION, + status=BotStatus.PLAYING if info else BotStatus.IDLE, + playback_information=info, + song_queue=get_queue_status(), + ) + return response + + +def get_filename_and_starttime(): + fileName, duration, start_time = get_current_metadata() + return fileName, start_time + + +def start_time_now(): + set_current_song_start_time(time.time()) + + +def handle_new_song_on_queue(): + if not has_current_song(): + move_to_last_song_in_queue() + if has_current_song(): + play_current_song() + else: + print("moving to the last song did not put us on a current song") + else: + print("not moving to new song because there is current song") + + +def pause_song(): + global pause_offset + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + fileName, duration, start_time = get_current_metadata() + pause_offset = time.time() - start_time + voice_client.pause() + + +def unpause_song(): + global pause_offset + voice_client = get_voice_client() + if voice_client and voice_client.is_playing(): + voice_client.resume() + set_current_song_start_time(time.time() - pause_offset) + pause_offset = -1 diff --git a/discord-bot/src/song_queue.py b/discord-bot/src/song_queue.py new file mode 100644 index 0000000..bb7a190 --- /dev/null +++ b/discord-bot/src/song_queue.py @@ -0,0 +1,95 @@ +from typing import List + +from pydantic import BaseModel +import yt_dlp + +from src.models import SongItem, SongQueueStatus + + +song_file_list: List[SongItem] = [] +current_position = -1 +current_song_start_time = 0 + + +def __download_url(url: str): + fileName = "" + + def yt_dlp_monitor(d): + nonlocal fileName + final_filename = d.get("info_dict").get("_filename") + fileName = final_filename + + ydl_opts = { + "extract_audio": True, + "format": "bestaudio/best", + "outtmpl": "./songs/%(title)s.mp3", + "progress_hooks": [yt_dlp_monitor], + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + res = ydl.extract_info(url) + song_duration = res["duration"] + return fileName, song_duration + + +def add_to_queue(url: str): + global current_song_start_time, song_file_list, current_position + filename, duration = __download_url(url) + song = SongItem(filename=filename, duration=duration) + song_file_list.append(song) + + +def has_current_song(): + global current_song_start_time, song_file_list, current_position + if not song_file_list: + return False + if len(song_file_list) == current_position: + return False + if current_position == -1: + return False + return True + + +def get_current_metadata(): + global current_song_start_time, song_file_list, current_position + if not has_current_song(): + print("cannot request metadata when no current song") + return None + + return ( + song_file_list[current_position].filename, + song_file_list[current_position].duration, + current_song_start_time, + ) + + +def set_current_song_start_time(start_time: float): + global current_song_start_time, song_file_list, current_position + current_song_start_time = start_time + + +def handle_song_end(): + global current_song_start_time, song_file_list, current_position + print("handling song end ", current_position, len(song_file_list)) + if current_position == -1: + return + if current_position == (len(song_file_list) - 1): + print("last song ended, reseting position") + current_position = -1 + return + print("song ended, moving to next song") + current_position += 1 + + +def move_to_last_song_in_queue(): + global current_song_start_time, song_file_list, current_position + current_position = len(song_file_list) - 1 + + +def get_queue_status(): + global current_song_start_time, song_file_list, current_position + return SongQueueStatus(song_file_list=song_file_list, position=current_position) + + +def set_queue_position(position: int): + global current_song_start_time, song_file_list, current_position + current_position = position diff --git a/dns/docker-compose.yml b/dns/docker-compose.yml new file mode 100644 index 0000000..6f63671 --- /dev/null +++ b/dns/docker-compose.yml @@ -0,0 +1,57 @@ +services: + ts-ingress: + image: tailscale/tailscale:latest + container_name: dns-tailscale + hostname: home-dns + restart: unless-stopped + environment: + - TS_STATE_DIR=/var/lib/tailscale + - TS_SERVE_CONFIG=/config/config.json + # - TS_AUTHKEY= + volumes: + - tailscale-data:/var/lib/tailscale + - ./ts-serve-config.json:/config/config.json + - /dev/net/tun:/dev/net/tun + cap_add: + - net_admin + - sys_module + + # pihole: + # container_name: pihole + # image: pihole/pihole:latest + # # For DHCP it is recommended to remove these ports and instead add: network_mode: "host" + # # ports: + # # - "0.0.0.0:53:53/tcp" + # # - "0.0.0.0:53:53/udp" + # # - "127.0.0.1:53:53/tcp" + # # - "127.0.0.1:53:53/udp" + # # - "100.122.128.107:53:53/tcp" + # # - "100.122.128.107:53:53/udp" + # # # - "67:67/udp" # Only required if you are using Pi-hole as your DHCP server + # # - "8580:80" + # environment: + # TZ: 'America/Denver' + # # WEBPASSWORD: 'set a secure password here or it will be random' + # volumes: + # - '/data/pihole/etc-pihole:/etc/pihole' + # - '/data/pihole/etc-dnsmasq.d:/etc/dnsmasq.d' + # # https://github.com/pi-hole/docker-pi-hole#note-on-capabilities + # # cap_add: + # # - NET_ADMIN # Required if you are using Pi-hole as your DHCP server, else not needed + # restart: unless-stopped + # network_mode: service:ts-ingress + + + adguardhome: + image: adguard/adguardhome + container_name: dns-adguardhome + network_mode: service:ts-ingress + restart: unless-stopped + volumes: + - /data/adguard/conf:/opt/adguardhome/conf + - /data/adguard/work:/opt/adguardhome/work + depends_on: + - ts-ingress + +volumes: + tailscale-data: \ No newline at end of file diff --git a/dns/ts-serve-config.json b/dns/ts-serve-config.json new file mode 100644 index 0000000..4c6c2a0 --- /dev/null +++ b/dns/ts-serve-config.json @@ -0,0 +1,16 @@ +{ + "TCP": { + "443": { + "HTTPS": true + } + }, + "Web": { + "${TS_CERT_DOMAIN}:443": { + "Handlers": { + "/": { + "Proxy": "http://127.0.0.1:80" + } + } + } + } +} \ No newline at end of file diff --git a/esphome/docker-compose.yml b/esphome/docker-compose.yml new file mode 100644 index 0000000..b0f43da --- /dev/null +++ b/esphome/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3' +services: + esphome: + container_name: esphome + image: ghcr.io/esphome/esphome + volumes: + - esphome-data:/config + - /etc/localtime:/etc/localtime:ro + restart: always + privileged: true + network_mode: host + # network_mode: service:ts-ingress + environment: + - USERNAME=alex + - PASSWORD=alex + + # ts-ingress: + # image: tailscale/tailscale:latest + # container_name: ts-ingress + # hostname: esphomehttps://tailscale.com/blog/docker-tailscale-guide + # env_file: + # - .env + # environment: + # - TS_STATE_DIR=/var/lib/tailscale + # - TS_SERVE_CONFIG=/config/esphome.json + # volumes: + # - tailscale-data:/var/lib/tailscale + # - ./ts-serve-config.json:/config/esphome.json + # - /dev/net/tun:/dev/net/tun + # cap_add: + # - net_admin + # - sys_module +volumes: + tailscale-data: + esphome-data: \ No newline at end of file diff --git a/esphome/ts-serve-config.json b/esphome/ts-serve-config.json new file mode 100644 index 0000000..6897442 --- /dev/null +++ b/esphome/ts-serve-config.json @@ -0,0 +1,19 @@ +{ + "TCP": { + "443": { + "HTTPS": true + } + }, + "Web": { + "${TS_CERT_DOMAIN}:443": { + "Handlers": { + "/": { + "Proxy": "http://127.0.0.1:6052" + } + } + } + }, + "AllowFunnel": { + "${TS_CERT_DOMAIN}:443": false + } +} \ No newline at end of file diff --git a/gitea/.gitignore b/gitea/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/gitea/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/gitea/config.yaml b/gitea/config.yaml new file mode 100644 index 0000000..7af2e97 --- /dev/null +++ b/gitea/config.yaml @@ -0,0 +1,98 @@ +# Example configuration file, it's safe to copy this as the default config file without any modification. + +# You don't have to copy this file to your instance, +# just run `./act_runner generate-config > config.yaml` to generate a config file. + +log: + # The level of logging, can be trace, debug, info, warn, error, fatal + level: info + +runner: + # Where to store the registration result. + file: .runner + # Execute how many tasks concurrently at the same time. + capacity: 1 + # Extra environment variables to run jobs. + envs: + A_TEST_ENV_NAME_1: a_test_env_value_1 + A_TEST_ENV_NAME_2: a_test_env_value_2 + # Extra environment variables to run jobs from a file. + # It will be ignored if it's empty or the file doesn't exist. + env_file: .env + # The timeout for a job to be finished. + # Please note that the Gitea instance also has a timeout (3h by default) for the job. + # So the job could be stopped by the Gitea instance if it's timeout is shorter than this. + timeout: 3h + # Whether skip verifying the TLS certificate of the Gitea instance. + insecure: false + # The timeout for fetching the job from the Gitea instance. + fetch_timeout: 5s + # The interval for fetching the job from the Gitea instance. + fetch_interval: 2s + # The labels of a runner are used to determine which jobs the runner can run, and how to run them. + # Like: "macos-arm64:host" or "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest" + # Find more images provided by Gitea at https://gitea.com/gitea/runner-images . + # If it's empty when registering, it will ask for inputting labels. + # If it's empty when execute `daemon`, will use labels in `.runner` file. + labels: + - "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest" + - "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04" + - "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04" + +cache: + # Enable cache server to use actions/cache. + enabled: true + # The directory to store the cache data. + # If it's empty, the cache data will be stored in $HOME/.cache/actcache. + dir: "" + # The host of the cache server. + # It's not for the address to listen, but the address to connect from job containers. + # So 0.0.0.0 is a bad choice, leave it empty to detect automatically. + host: "" + # The port of the cache server. + # 0 means to use a random available port. + port: 0 + # The external cache server URL. Valid only when enable is true. + # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself. + # The URL should generally end with "/". + external_server: "" + +container: + # Specifies the network to which the container will connect. + # Could be host, bridge or the name of a custom network. + # If it's empty, act_runner will create a network automatically. + network: host + # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). + privileged: false + # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway). + options: + # The parent directory of a job's working directory. + # NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically. + # If the path starts with '/', the '/' will be trimmed. + # For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir + # If it's empty, /workspace will be used. + workdir_parent: + # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob + # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted. + # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to: + # valid_volumes: + # - data + # - /src/*.json + # If you want to allow any volume, please use the following configuration: + # valid_volumes: + # - '**' + valid_volumes: [] + # overrides the docker client host with the specified one. + # If it's empty, act_runner will find an available docker host automatically. + # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers. + # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work. + docker_host: "" + # Pull docker image(s) even if already present + force_pull: true + # Rebuild docker image(s) even if already present + force_rebuild: false + +host: + # The parent directory of a job's working directory. + # If it's empty, $HOME/.cache/act/ will be used. + workdir_parent: diff --git a/gitea/docker-compose.yml b/gitea/docker-compose.yml new file mode 100644 index 0000000..e961d3c --- /dev/null +++ b/gitea/docker-compose.yml @@ -0,0 +1,47 @@ +services: + server: + image: gitea/gitea:1.22.2 + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=gitea + restart: always + volumes: + - ./data/gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - 0.0.0.0:3000:3000 + - 0.0.0.0:222:22 + depends_on: + - db + db: + image: postgres:14 + restart: always + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea + - POSTGRES_DB=gitea + volumes: + - ./data/postgres:/var/lib/postgresql/data + + runner: + image: gitea/act_runner:nightly + environment: + CONFIG_FILE: /config.yaml + GITEA_INSTANCE_URL: http://0.0.0.0:3000/ + GITEA_RUNNER_REGISTRATION_TOKEN: SMANpMfJk5G4fTFmuEZ9zleTBcdrj4M3k3eDCW6e + GITEA_RUNNER_NAME: test-runner + GITEA_RUNNER_LABELS: label1 + network_mode: host + volumes: + - ./config.yaml:/config.yaml + - ./data/runner:/data + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - server \ No newline at end of file diff --git a/home-kubernetes/README.md b/home-kubernetes/README.md new file mode 100644 index 0000000..d6b8c2f --- /dev/null +++ b/home-kubernetes/README.md @@ -0,0 +1,18 @@ + + +## Home Kubernetes With K3S + + + + + + + + + +## Other Kubernetes Distros + +I have tried k0s a few times and consistently got an "agent not available on node" error that stopped me from reading logs from pods. + +I have used kubeadm to deploy clusters, while it works, it it pretty manual. + diff --git a/home-manager/home.nix b/home-manager/home.nix new file mode 100644 index 0000000..6282b9b --- /dev/null +++ b/home-manager/home.nix @@ -0,0 +1,146 @@ +{ config, pkgs, ... }: + +{ + # Home Manager needs a bit of information about you and the paths it should + # manage. + home.username = "alexm"; + home.homeDirectory = "/home/alexm"; + + # You should not change this value, even if you update Home Manager. If you do + # want to update the value, then make sure to first check the Home Manager + # release notes. + home.stateVersion = "24.05"; # Please read the comment before changing. + + home.packages = [ + pkgs.openldap + pkgs.k9s + pkgs.jwt-cli + pkgs.thefuck + pkgs.fish + pkgs.kubectl + pkgs.lazydocker + ]; + programs.fish = { + enable = true; + shellAliases = { + dang="fuck"; + }; + shellInit = '' + +function commit + git add --all + git commit -m "$argv" + git push +end + +# have ctrl+backspace delete previous word +bind \e\[3\;5~ kill-word +# have ctrl+delete delete following word +bind \b backward-kill-word + +set -U fish_user_paths ~/.local/bin $fish_user_paths +#set -U fish_user_paths ~/.dotnet $fish_user_paths +#set -U fish_user_paths ~/.dotnet/tools $fish_user_paths + +export VISUAL=vim +export EDITOR="$VISUAL" +export DOTNET_WATCH_RESTART_ON_RUDE_EDIT=1 +export DOTNET_CLI_TELEMETRY_OPTOUT=1 + +set -x LIBVIRT_DEFAULT_URI qemu:///system + +thefuck --alias | source + ''; + }; + # Home Manager is pretty good at managing dotfiles. The primary way to manage + # plain files is through 'home.file'. + home.file = { + # # Building this configuration will create a copy of 'dotfiles/screenrc' in + # # the Nix store. Activating the configuration will then make '~/.screenrc' a + # # symlink to the Nix store copy. + # ".screenrc".source = dotfiles/screenrc; + + # # You can also set the file content immediately. + # ".gradle/gradle.properties".text = '' + # org.gradle.console=verbose + # org.gradle.daemon.idletimeout=3600000 + # ''; + ".config/lazydocker/config.yml".text = '' +gui: + returnImmediately: true + ''; + ".config/k9s/config.yaml".text = '' +k9s: + liveViewAutoRefresh: true + screenDumpDir: /home/alexm/.local/state/k9s/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + reactive: false + noIcons: false + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 1000 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 + namespace: + lockFavorites: false + ''; + }; + + # Home Manager can also manage your environment variables through + # 'home.sessionVariables'. These will be explicitly sourced when using a + # shell provided by Home Manager. If you don't want to manage your shell + # through Home Manager then you have to manually source 'hm-session-vars.sh' + # located at either + # + # ~/.nix-profile/etc/profile.d/hm-session-vars.sh + # + # or + # + # ~/.local/state/nix/profiles/profile/etc/profile.d/hm-session-vars.sh + # + # or + # + # /etc/profiles/per-user/alexm/etc/profile.d/hm-session-vars.sh + # + home.sessionVariables = { + EDITOR = "vim"; + }; + dconf.enable = true; + dconf.settings = { + "org/gnome/desktop/wm/keybindings" = { + toggle-maximized=["m"]; + }; + }; + # Let Home Manager install and manage itself. + programs.home-manager.enable = true; +} diff --git a/home-server/dns/update-dns.sh b/home-server/dns/update-dns.sh new file mode 100755 index 0000000..3109adb --- /dev/null +++ b/home-server/dns/update-dns.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# curl -X GET https://api.cloudflare.com/client/v4/zones/bf7a05315be9bf7a39d50dd4001e7a97/dns_records -H "X-Auth-Email: alexmickelson96@gmail.com" -H "X-Auth-Key: jo7GntHEEBtANFsuteAM8EJ-stLUqyNbOk2x4Czr" | python -m json.tool + +source /home/alex/actions-runner/_work/infrastructure/infrastructure/home-pi/dns/cloudflare.env + +NETWORK_INTERFACE=wlan0 +IP=$(ip a s $NETWORK_INTERFACE | awk '/inet / {print$2}' | cut -d/ -f1) +EMAIL="alexmickelson96@gmail.com"; +ZONE_ID="bf7a05315be9bf7a39d50dd4001e7a97"; + + +update_record() { + LOCAL_NAME=$1 + LOCAL_RECORD_ID=$2 + + echo "UPDATING RECORD FOR $LOCAL_NAME TO $IP" + + curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$LOCAL_RECORD_ID" \ + -H "X-Auth-Email: alexmickelson96@gmail.com" \ + -H "X-Auth-Key: $CLOUDFLARE_TOKEN" \ + -H "Content-Type: application/json" \ + --data '{"type":"A","name":"'"$LOCAL_NAME"'","content":"'"$IP"'","ttl":1}' \ + | python3 -m json.tool; + + echo + echo "------------------------------------" + echo +} + +NAME="ha.alexmickelson.guru"; +RECORD_ID="09eac5a17fa4302091532dabdbe73a68" +update_record $NAME $RECORD_ID + +NAME="jellyfin.alexmickelson.guru"; +RECORD_ID="577293ab0488913308fda78010a7483b" +update_record $NAME $RECORD_ID + +NAME="next.alexmickelson.guru"; +RECORD_ID="cc686333d2421a4e558a04589b375ded" +update_record $NAME $RECORD_ID + + diff --git a/home-server/docker-compose.yml b/home-server/docker-compose.yml new file mode 100644 index 0000000..9b2f2c1 --- /dev/null +++ b/home-server/docker-compose.yml @@ -0,0 +1,256 @@ +services: + jellyfin: + image: jellyfin/jellyfin + container_name: jellyfin + user: 1000:1000 + network_mode: "host" + volumes: + - /data/jellyfin/config:/config + - /data/jellyfin/cache:/cache + - /data/media/music/tagged:/music + - /data/media/movies:/movies + - /data/media/tvshows:/tvshows + restart: "unless-stopped" + environment: + - JELLYFIN_PublishedServerUrl=https://jellyfin.alexmickelson.guru + + nextcloud: + build: + context: nextcloud + container_name: nextcloud + environment: + - TZ=America/Denver + - OVERWRITEPROTOCOL=https + - MYSQL_PASSWORD=slkdnflksnelkfnsdweoinv + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud + - MYSQL_HOST=nextcloud-db + volumes: + - /data/nextcloud/html:/var/www/html + - /data/media/music:/music + - /data/media/movies:/movies + - /data/media/tvshows:/tvshows + - /data/media/shared:/shared + - /data/media/audiobooks:/audiobooks + restart: unless-stopped + networks: + - proxy + + nextcloud-cron: + build: + context: nextcloud + container_name: nextcloud-cron + environment: + - TZ=America/Denver + - OVERWRITEPROTOCOL=https + - MYSQL_PASSWORD=slkdnflksnelkfnsdweoinv + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud + - MYSQL_HOST=nextcloud-db + volumes: + - /data/nextcloud/html:/var/www/html + - /data/media/music:/music + - /data/media/movies:/movies + - /data/media/tvshows:/tvshows + - /data/media/shared:/shared + - /data/media/audiobooks:/audiobooks + restart: unless-stopped + entrypoint: /cron.sh + depends_on: + - nextcloud + networks: + - proxy + + nextcloud-db: + image: mariadb:10.6 + container_name: nextcloud_db + # mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE + restart: always + command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW + volumes: + - /data/nextcloud-db/:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=klsdnofinsodkflksen34tesrg + - MYSQL_PASSWORD=slkdnflksnelkfnsdweoinv + - MYSQL_DATABASE=nextcloud + - MYSQL_USER=nextcloud + networks: + - proxy + + homeassistant: + container_name: homeassistant + image: homeassistant/home-assistant:stable + volumes: + - /data/homeAssistant/config:/config + - /etc/localtime:/etc/localtime:ro + - /dev/serial/by-id:/dev/serial/by-id + devices: + - /dev/ttyUSB0:/dev/ttyUSB0 + - /dev/ttyUSB1:/dev/ttyUSB1 + environment: + - TZ=America/Denver + restart: always + network_mode: host + + # octoprint: + # image: octoprint/octoprint + # container_name: octoprint + # restart: unless-stopped + # # ports: + # # - 80:80 + # # devices: + # # # use `python -m serial.tools.miniterm` to see what the name is of the printer, this requires pyserial + # # - /dev/ttyACM0:/dev/ttyACM0 + # # - /dev/video0:/dev/video0 + # volumes: + # - /data/octoprint:/octoprint + # # uncomment the lines below to ensure camera streaming is enabled when + # # you add a video device + # environment: + # - ENABLE_MJPG_STREAMER=true + # - MJPG_SREAMER_INPUT=-n -r 1280x720 -f 30 + + prometheus: + image: bitnami/prometheus:2 + container_name: prometheus + restart: unless-stopped + environment: + - HOMEASSISTANT_TOKEN=${HOMEASSISTANT_TOKEN} + volumes: + - ./prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml + - /data/prometheus:/opt/bitnami/prometheus/data + # command: + # - '--config.file=/etc/prometheus/prometheus.yml' + # - '--storage.tsdb.path=/prometheus' + # - '--web.console.libraries=/etc/prometheus/console_libraries' + # - '--web.console.templates=/etc/prometheus/consoles' + # - '--web.enable-lifecycle' + # expose: + # - 9090 + networks: + - proxy + + grafana: + image: grafana/grafana:main + container_name: grafana + restart: always + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} + volumes: + - /data/grafana:/var/lib/grafana + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/robots.txt"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 3s + networks: + - proxy + + # acpupsd_exporter: + # image: sfudeus/apcupsd_exporter:master_1.19 + # container_name: apcupsd_exporter + # restart: always + # extra_hosts: + # - host.docker.internal:host-gateway + # command: -apcupsd.addr host.docker.internal:3551 + # ports: + # - 0.0.0.0:9162:9162 + # docker run -it --rm -p 9162:9162 --net=host sfudeus/apcupsd_exporter:master_1.19 + + reverse-proxy: + image: ghcr.io/linuxserver/swag + container_name: reverse-proxy + restart: unless-stopped + cap_add: + - NET_ADMIN + environment: + - PUID=1000 + - PGID=1000 + - TZ=America/Denver + - URL=alexmickelson.guru + - SUBDOMAINS=wildcard + - VALIDATION=dns + - DNSPLUGIN=cloudflare + volumes: + - ./nginx.conf:/config/nginx/site-confs/default.conf + - /data/swag:/config + - /data/cloudflare/cloudflare.ini:/config/dns-conf/cloudflare.ini + ports: + - 0.0.0.0:80:80 + - 0.0.0.0:443:443 + extra_hosts: + - host.docker.internal:host-gateway + networks: + - proxy + + + audiobookshelf: + image: ghcr.io/advplyr/audiobookshelf:latest + restart: unless-stopped + ports: + - 13378:80 + volumes: + - /data/media/audiobooks:/audiobooks + # - :/podcasts + - /data/audiobookshelf/config:/config + - /data/audiobookshelf/metadata:/metadata + networks: + - proxy + + docker-registry: + image: registry:2 + container_name: docker-registry + restart: unless-stopped + ports: + - "5000:5000" + environment: + REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data + REGISTRY_HTTP_TLS_CERTIFICATE: /etc/docker/certs.d/server.alexmickelson.guru/cert.pem + REGISTRY_HTTP_TLS_KEY: /etc/docker/certs.d/server.alexmickelson.guru/key.pem + volumes: + - /data/docker-registry:/data + - /data/swag/keys/letsencrypt/fullchain.pem:/etc/docker/certs.d/server.alexmickelson.guru/cert.pem + - /data/swag/keys/letsencrypt/privkey.pem:/etc/docker/certs.d/server.alexmickelson.guru/key.pem + depends_on: + - reverse-proxy + networks: + - proxy + # github-actions-exporter: + # # ports: + # # - 9999:9999 + # image: ghcr.io/labbs/github-actions-exporter + # environment: + # - GITHUB_REPOS=alexmickelson/infrastructure + # - GITHUB_TOKEN=${MY_GITHUB_TOKEN} + + + # pihole: + # container_name: pihole + # image: pihole/pihole:latest + # # For DHCP it is recommended to remove these ports and instead add: network_mode: "host" + # ports: + # # - "0.0.0.0:53:53/tcp" + # # - "0.0.0.0:53:53/udp" + # # - "127.0.0.1:53:53/tcp" + # # - "127.0.0.1:53:53/udp" + # - "100.122.128.107:53:53/tcp" + # - "100.122.128.107:53:53/udp" + # # # - "67:67/udp" # Only required if you are using Pi-hole as your DHCP server + # - "8580:80" + # environment: + # TZ: 'America/Denver' + # # WEBPASSWORD: 'set a secure password here or it will be random' + # volumes: + # - '/data/pihole/etc-pihole:/etc/pihole' + # - '/data/pihole/etc-dnsmasq.d:/etc/dnsmasq.d' + # # https://github.com/pi-hole/docker-pi-hole#note-on-capabilities + # cap_add: + # - NET_ADMIN # Required if you are using Pi-hole as your DHCP server, else not needed + # restart: unless-stopped + +networks: + proxy: + external: + name: proxy \ No newline at end of file diff --git a/home-server/nextcloud/dockerfile b/home-server/nextcloud/dockerfile new file mode 100644 index 0000000..b482c89 --- /dev/null +++ b/home-server/nextcloud/dockerfile @@ -0,0 +1,4 @@ +FROM nextcloud:production + +RUN usermod -u 1000 www-data +RUN groupmod -g 1000 www-data \ No newline at end of file diff --git a/home-server/nginx.conf b/home-server/nginx.conf new file mode 100644 index 0000000..32532e9 --- /dev/null +++ b/home-server/nginx.conf @@ -0,0 +1,202 @@ + +# include mime.types; +# default_type application/octet-stream; + +# log_format main '$remote_addr - $remote_user [$time_local] "$request" ' +# '$status $body_bytes_sent "$http_referer" ' +# '"$http_user_agent" "$http_x_forwarded_for"'; + +# access_log /var/log/nginx/access.log main; + +# sendfile on; +# keepalive_timeout 65; + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name ha.alexmickelson.guru; + include /config/nginx/ssl.conf; + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + + location / { + proxy_pass http://host.docker.internal:8123; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name next.alexmickelson.guru; + include /config/nginx/ssl.conf; + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + + + location /.well-known/carddav { + return 301 $scheme://$host/remote.php/dav; + } + + location /.well-known/caldav { + return 301 $scheme://$host/remote.php/dav; + } + + location / { + proxy_pass http://nextcloud:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Port $server_port; + client_max_body_size 1G; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name plex.alexmickelson.guru; + + location / { + proxy_pass http://host.docker.internal:32400; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name jellyfin.alexmickelson.guru; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "0"; # Do NOT enable. This is obsolete/dangerous + add_header X-Content-Type-Options "nosniff"; + client_max_body_size 20M; + + + location / { + # Proxy main Jellyfin traffic + proxy_pass http://host.docker.internal:8096; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + proxy_buffering off; + } + + location /socket { + # Proxy Jellyfin Websockets traffic + proxy_pass http://host.docker.internal:8096; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name audiobook.alexmickelson.guru; + + location / { + proxy_pass http://audiobookshelf:80; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_http_version 1.1; + } +} + +# server { +# listen 443 ssl; +# listen [::]:443 ssl; +# server_name octoprint.alexmickelson.guru; + +# location / { +# proxy_pass http://octoprint:80; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# } +# } + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name prometheus.alexmickelson.guru; + + location / { + proxy_pass http://prometheus:9090; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name grafana.alexmickelson.guru; + + location / { + proxy_pass http://grafana:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name photos.alexmickelson.guru; + + # allow large file uploads + client_max_body_size 50000M; + + # Set headers + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # enable websockets: http://nginx.org/en/docs/http/websocket.html + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + + # set timeout + proxy_read_timeout 600s; + proxy_send_timeout 600s; + send_timeout 600s; + + location / { + proxy_pass http://immich_server:2283; + } +} \ No newline at end of file diff --git a/home-server/nix/home-server.nix b/home-server/nix/home-server.nix new file mode 100644 index 0000000..0d096d3 --- /dev/null +++ b/home-server/nix/home-server.nix @@ -0,0 +1,340 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page +# and in the NixOS manual (accessible by running ‘nixos-help’). + +{ config, pkgs, ... }: + +{ + imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + + ]; + + # Bootloader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + networking.hostName = "home-server"; # Define your hostname. + # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Enable networking + networking.networkmanager.enable = true; + + networking.nat.enable = true; + + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; + # Set your time zone. + time.timeZone = "America/Denver"; + + # Select internationalisation properties. + i18n.defaultLocale = "en_US.UTF-8"; + + i18n.extraLocaleSettings = { + LC_ADDRESS = "en_US.UTF-8"; + LC_IDENTIFICATION = "en_US.UTF-8"; + LC_MEASUREMENT = "en_US.UTF-8"; + LC_MONETARY = "en_US.UTF-8"; + LC_NAME = "en_US.UTF-8"; + LC_NUMERIC = "en_US.UTF-8"; + LC_PAPER = "en_US.UTF-8"; + LC_TELEPHONE = "en_US.UTF-8"; + LC_TIME = "en_US.UTF-8"; + }; + + # Configure keymap in X11 + services.xserver.xkb = { + layout = "us"; + variant = ""; + }; + + users.users.github = { + isNormalUser = true; + description = "github"; + extraGroups = [ "docker" ]; + shell = pkgs.fish; + }; + users.users.alex = { + isNormalUser = true; + description = "alex"; + extraGroups = [ "networkmanager" "wheel" "docker" "users" "libvirtd" "cdrom" ]; + shell = pkgs.fish; + }; + home-manager.users.alex = { pgks, ...}: { + home.stateVersion = "24.05"; + home.packages = with pkgs; [ + openldap + k9s + jwt-cli + thefuck + fish + kubectl + lazydocker + btop + nix-index + usbutils + makemkv + mbuffer + lzop + lsof + code-server + ]; + programs.fish = { + enable = true; + shellAliases = { + dang="fuck"; + }; + shellInit = '' +function commit + git add --all + git commit -m "$argv" + git push +end + +# have ctrl+backspace delete previous word +bind \e\[3\;5~ kill-word +# have ctrl+delete delete following word +bind \b backward-kill-word + +set -U fish_user_paths ~/.local/bin $fish_user_paths +#set -U fish_user_paths ~/.dotnet $fish_user_paths +#set -U fish_user_paths ~/.dotnet/tools $fish_user_paths + +export VISUAL=vim +export EDITOR="$VISUAL" +export DOTNET_WATCH_RESTART_ON_RUDE_EDIT=1 +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +set -x LIBVIRT_DEFAULT_URI qemu:///system + +thefuck --alias | source + ''; + }; + home.file = { + ".config/lazydocker/config.yml".text = '' +gui: + returnImmediately: true + ''; + ".config/k9s/config.yaml".text = '' +k9s: + liveViewAutoRefresh: true + screenDumpDir: /home/alexm/.local/state/k9s/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + reactive: false + noIcons: false + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 1000 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 + namespace: + lockFavorites: false + ''; + }; + home.sessionVariables = { + EDITOR = "vim"; + }; + }; + home-manager.useGlobalPkgs = true; + + + # Allow unfree packages + nixpkgs.config.allowUnfree = true; + + # List packages installed in system profile. To search, run: + # $ nix search wget + environment.systemPackages = with pkgs; [ + vim + wget + curl + docker + fish + git + zfs + gcc-unwrapped + github-runner + sanoid + virtiofsd + tmux + ]; + + services.openssh.enable = true; + programs.fish.enable = true; + virtualisation.docker.enable = true; + #virtualisation.docker.extraOptions = "--dns 1.1.1.1 --dns 8.8.8.8 --dns 100.100.100.100"; + services.tailscale.enable = true; + services.tailscale.extraSetFlags = [ + "--stateful-filtering=false" + ]; + services.envfs.enable = true; + + # printing + services.printing = { + enable = true; + drivers = [ pkgs.brgenml1lpr pkgs.brgenml1cupswrapper pkgs.brlaser]; + listenAddresses = [ "*:631" ]; + + extraConf = '' + ServerAlias server.alexmickelson.guru + ''; + allowFrom = [ "all" ]; + browsing = true; + defaultShared = true; + openFirewall = true; + }; + services.avahi = { + enable = true; + nssmdns4 = true; + openFirewall = true; + publish = { + enable = true; + userServices = true; + }; + }; + + systemd.services.printing-server = { + description = "Web Printing Server Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = "${pkgs.nix}/bin/nix run .#fastapi-server"; + Restart = "always"; + WorkingDirectory = "/home/alex/infrastructure/home-server/printing/server"; + User = "alex"; + }; + }; + + # virtualization stuff + virtualisation.libvirtd.enable = true; + + # zfs stuff + boot.supportedFilesystems = [ "zfs" ]; + boot.zfs.forceImportRoot = false; + networking.hostId = "eafe9551"; + boot.zfs.extraPools = [ "data-ssd" "backup" ]; + services.sanoid = { + enable = true; + templates.production = { + hourly = 24; + daily = 14; + monthly = 5; + autoprune = true; + autosnap = true; + }; + + datasets."data-ssd/data" = { + useTemplate = [ "production" ]; + }; + datasets."data-ssd/media" = { + useTemplate = [ "production" ]; + }; + + + templates.backup = { + hourly = 24; + daily = 14; + monthly = 5; + autoprune = true; + autosnap = false; + }; + datasets."backup/data" = { + useTemplate = [ "backup" ]; + }; + datasets."backup/media" = { + useTemplate = [ "backup" ]; + }; + }; + + + + services.github-runners = { + infrastructure = { + enable = true; + name = "infrastructure-runner"; + user = "github"; + tokenFile = "/data/runner/github-infrastructure-token.txt"; + url = "https://github.com/alexmickelson/infrastructure"; + extraLabels = [ "home-server" ]; + #workDir = "/data/runner/infrastructure/"; + replace = true; + serviceOverrides = { + ReadWritePaths = [ + "/data/cloudflare/" + "/data/runner/infrastructure" + "/data/runner" + "/home/github/infrastructure" + ]; + PrivateDevices = false; + DeviceAllow = "/dev/zfs rw"; + ProtectProc = false; + ProtectSystem = false; + PrivateMounts = false; + PrivateUsers = false; + #DynamicUser = true; + #NoNewPrivileges = false; + ProtectHome = false; + #RuntimeDirectoryPreserve = "yes"; + }; + extraPackages = with pkgs; [ + docker + git-secret + zfs + sanoid + mbuffer + lzop + ]; + }; + }; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + networking.firewall.enable = false; + # networking.firewall.trustedInterfaces = [ "docker0" ]; + + # This value determines the NixOS release from which the default + # settings for stateful data, like file locations and database versions + # on your system were taken. It‘s perfectly fine and recommended to leave + # this value at the release version of the first install of this system. + # Before changing this value read the documentation for this option + # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). + system.stateVersion = "24.05"; # Did you read the comment? + +} diff --git a/home-server/printing/cupsd.conf b/home-server/printing/cupsd.conf new file mode 100644 index 0000000..c3193f6 --- /dev/null +++ b/home-server/printing/cupsd.conf @@ -0,0 +1,198 @@ +# +# Configuration file for the CUPS scheduler. See "man cupsd.conf" for a +# complete description of this file. +# + +# Log general information in error_log - change "warn" to "debug" +# for troubleshooting... +LogLevel warn +PageLogFormat + +# Specifies the maximum size of the log files before they are rotated. The value "0" disables log rotation. +MaxLogSize 0 + +# Default error policy for printers +ErrorPolicy retry-job + +# Allow remote access +Listen *:631 # important +ServerAlias * # important + +# Show shared printers on the local network. +Browsing Yes +BrowseLocalProtocols dnssd + +# Default authentication type, when authentication is required... +DefaultAuthType Basic +DefaultEncryption IfRequested + +# Web interface setting... +WebInterface Yes + +# Timeout after cupsd exits if idle (applied only if cupsd runs on-demand - with -l) +IdleExitTimeout 60 + +# Restrict access to the server... + + Order allow,deny + Allow all + + +# Restrict access to the admin pages... + + Order allow,deny + Allow all + + +# Restrict access to configuration files... + + AuthType Default + Require user @SYSTEM + Order allow,deny + Allow all + + +# Restrict access to log files... + + AuthType Default + Require user @SYSTEM + Order allow,deny + Allow all + + +# Set the default printer/job policies... + + # Job/subscription privacy... + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + # Job-related operations must be done by the owner or an administrator... + + Order deny,allow + # Allow all # mine... + + + + Require user @OWNER @SYSTEM + Order deny,allow + + + # All administration operations require an administrator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # All printer operations require a printer operator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # Only the owner or an administrator can cancel or authenticate a job... + + Require user @OWNER @SYSTEM + Order deny,allow + + + + Order deny,allow + # Allow all # mine + + + +# Set the authenticated printer/job policies... + + # Job/subscription privacy... + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + # Job-related operations must be done by the owner or an administrator... + + AuthType Default + Order deny,allow + + + + AuthType Default + Require user @OWNER @SYSTEM + Order deny,allow + + + # All administration operations require an administrator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # All printer operations require a printer operator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # Only the owner or an administrator can cancel or authenticate a job... + + AuthType Default + Require user @OWNER @SYSTEM + Order deny,allow + + + + Order deny,allow + + + +# Set the kerberized printer/job policies... + + # Job/subscription privacy... + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + # Job-related operations must be done by the owner or an administrator... + + AuthType Negotiate + Order deny,allow + + + + AuthType Negotiate + Require user @OWNER @SYSTEM + Order deny,allow + + + # All administration operations require an administrator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # All printer operations require a printer operator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # Only the owner or an administrator can cancel or authenticate a job... + + AuthType Negotiate + Require user @OWNER @SYSTEM + Order deny,allow + + + + Order deny,allow + + diff --git a/home-server/printing/docker-compose.yml b/home-server/printing/docker-compose.yml new file mode 100644 index 0000000..c0142a1 --- /dev/null +++ b/home-server/printing/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.8" +services: + cups: + image: olbat/cupsd:stable-2024-01-19 # admin user/password: print/print + container_name: cups + privileged: true + volumes: + - "/dev/bus/usb:/dev/bus/usb" # keep this under volumes, not devices + - "/run/dbus:/run/dbus" + - "./cupsd.conf:/etc/cups/cupsd.conf:ro" + #- "./data/printers.conf:/etc/cups/printers.conf:ro" + ports: + - "631:631/tcp" # CUPS + restart: "always" + + cups-webpage: + buid: server + ports: + - 6311:6311 \ No newline at end of file diff --git a/home-server/printing/printer.conf b/home-server/printing/printer.conf new file mode 100644 index 0000000..314bf8b --- /dev/null +++ b/home-server/printing/printer.conf @@ -0,0 +1,24 @@ +# Printer configuration file for CUPS v2.4.2 +# Written by cupsd +# DO NOT EDIT THIS FILE WHEN CUPSD IS RUNNING +NextPrinterId 2 + +PrinterId 1 +UUID urn:uuid:8ac038d7-8659-3de9-57d0-0a7f97b956cc +Info Brother HL-L2300D series +Location +MakeModel Brother HL-L2300D series, using brlaser v6 +DeviceURI usb://Brother/HL-L2300D%20series?serial=U63878J0N375067 +State Idle +StateTime 1714021523 +ConfigTime 1714021523 +Type 4180 +Accepting Yes +Shared Yes +JobSheets none none +QuotaPeriod 0 +PageLimit 0 +KLimit 0 +OpPolicy default +ErrorPolicy retry-job + diff --git a/home-server/printing/readme.md b/home-server/printing/readme.md new file mode 100644 index 0000000..103fb9d --- /dev/null +++ b/home-server/printing/readme.md @@ -0,0 +1,13 @@ + + + +## what I am running on office server + +```bash +sudo apt install python3-pip cups python3-cups hplip +pip install pycups fastapi "uvicorn[standard]" python-multipart +sudo hp-setup -i # manually configure printer... +python -m uvicorn print_api:app --reload --host 0.0.0.0 +``` + +url: http://100.103.75.97:8000/ \ No newline at end of file diff --git a/home-server/printing/server/Dockerfile b/home-server/printing/server/Dockerfile new file mode 100644 index 0000000..6c34bbe --- /dev/null +++ b/home-server/printing/server/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3 + +RUN apt-get update \ + && apt-get install -y libcups2-dev python3-pip cups python3-cups gcc \ + && pip install pycups fastapi "uvicorn[standard]" python-multipart + + +WORKDIR /app +COPY ./src . + +CMD python -m uvicorn print_api:app --reload --host 0.0.0.0 --port 6311 \ No newline at end of file diff --git a/home-server/printing/server/flake.lock b/home-server/printing/server/flake.lock new file mode 100644 index 0000000..66f72d1 --- /dev/null +++ b/home-server/printing/server/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1725634671, + "narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/home-server/printing/server/flake.nix b/home-server/printing/server/flake.nix new file mode 100644 index 0000000..534880d --- /dev/null +++ b/home-server/printing/server/flake.nix @@ -0,0 +1,38 @@ +{ + description = "Printer Server Flake"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + outputs = { self, nixpkgs, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + myPython = pkgs.python3.withPackages (python-pkgs: with pkgs; [ + python312Packages.fastapi + python312Packages.fastapi-cli + python312Packages.pycups + python312Packages.python-multipart + python312Packages.uvicorn + ]); + in + { + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + python312Packages.fastapi + python312Packages.fastapi-cli + python312Packages.pycups + python312Packages.python-multipart + python312Packages.uvicorn + ]; + }; + + packages.${system} = rec { + fastapi-server = pkgs.writeShellScriptBin "start-server" '' + ${myPython}/bin/fastapi run ${self}/src/print_api.py + ''; + + default = fastapi-server; + }; + + }; +} \ No newline at end of file diff --git a/home-server/printing/server/src/index.html b/home-server/printing/server/src/index.html new file mode 100644 index 0000000..57a03ad --- /dev/null +++ b/home-server/printing/server/src/index.html @@ -0,0 +1,149 @@ + + + + Document Upload + + + +

Upload Document

+
+
+
+
Drop file to upload or click to select
+ +
+ +
+
+ + + diff --git a/home-server/printing/server/src/print_api.py b/home-server/printing/server/src/print_api.py new file mode 100644 index 0000000..7197af3 --- /dev/null +++ b/home-server/printing/server/src/print_api.py @@ -0,0 +1,69 @@ +import os +from pprint import pprint +import tempfile +from fastapi import FastAPI, File, UploadFile, Request +import cups +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +# @app.post("/print/") +# async def print_document(file: UploadFile = File(...)): +# temp_file = tempfile.NamedTemporaryFile(delete=False) +# temp_file.write(await file.read()) +# temp_file.close() + +# conn = cups.Connection(host="server.alexmickelson.guru") + +# printers = conn.getPrinters() +# print(file.filename) +# print(temp_file.name) +# pprint(printers) +# for printer in printers: +# print(printer, printers[printer]["device-uri"]) + +# default_printer = list(printers.keys())[0] + + +# job_id = conn.printFile(default_printer, temp_file.name, f"FastAPI Print Job for {temp_file.name}", {}) +# os.unlink(temp_file.name) + +# return {"job_id": job_id, "file_name": file.filename} + + +@app.post("/print/") +async def print_document(file: UploadFile = File(...)): + # Save the uploaded file to a temporary file + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.write(await file.read()) + temp_file.close() + + # Connect to the CUPS server on the host (use default CUPS connection) + conn = cups.Connection() # This will connect to localhost CUPS + + # Get the list of available printers + printers = conn.getPrinters() + print(file.filename) + print(temp_file.name) + pprint(printers) + for printer in printers: + print(printer, printers[printer]["device-uri"]) + + # Use the default printer (first one in the list) + default_printer = list(printers.keys())[0] + + # Print the file + job_id = conn.printFile(default_printer, temp_file.name, f"FastAPI Print Job for {temp_file.name}", {}) + + # Clean up the temporary file + os.unlink(temp_file.name) + + return {"job_id": job_id, "file_name": file.filename} + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + with open('src/index.html', 'r') as f: + html_content = f.read() + return HTMLResponse(content=html_content, status_code=200) + diff --git a/home-server/prometheus.yml b/home-server/prometheus.yml new file mode 100644 index 0000000..fb1b26e --- /dev/null +++ b/home-server/prometheus.yml @@ -0,0 +1,52 @@ +# my global config +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +# Alertmanager configuration +alerting: + alertmanagers: + - static_configs: + - targets: + # - alertmanager:9093 + +# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "node" + static_configs: + - targets: + - 100.119.183.105:9100 # desktop + - 100.122.128.107:9100 # home server + - 100.64.229.40:9100 # linode + + - job_name: "docker" + static_configs: + - targets: + # - 100.119.183.105:9323 # desktop + - 100.122.128.107:9323 # home server + - 100.64.229.40:9323 # linode + + - job_name: ups + static_configs: + - targets: + - 100.122.128.107:9162 # home server + + - job_name: homeassistant + scrape_interval: 60s + metrics_path: /api/prometheus + authorization: + credentials: '%{HOMEASSITANT_TOKEN}' + scheme: https + static_configs: + - targets: ['ha.alexmickelson.guru'] \ No newline at end of file diff --git a/immich/docker-compose.yml b/immich/docker-compose.yml new file mode 100644 index 0000000..7a4d12a --- /dev/null +++ b/immich/docker-compose.yml @@ -0,0 +1,80 @@ +name: immich +services: + immich-server: + container_name: immich_server + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.transcoding.yml + # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + volumes: + # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file + - ${UPLOAD_LOCATION}:/usr/src/app/upload + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ports: + - 0.0.0.0:2283:2283 + depends_on: + - redis + - database + restart: always + healthcheck: + disable: false + networks: + - proxy + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable + volumes: + - model-cache:/cache + env_file: + - .env + restart: always + healthcheck: + disable: false + networks: + - proxy + + redis: + container_name: immich_redis + image: docker.io/redis:6.2-alpine@sha256:2d1463258f2764328496376f5d965f20c6a67f66ea2b06dc42af351f75248792 + healthcheck: + test: redis-cli ping || exit 1 + restart: always + networks: + - proxy + + database: + container_name: immich_postgres + image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file + - ${DB_DATA_LOCATION}:/var/lib/postgresql/data + healthcheck: + test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 + interval: 5m + start_interval: 30s + start_period: 5m + command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] + restart: always + networks: + - proxy + +volumes: + model-cache: + +networks: + proxy: + external: + name: proxy \ No newline at end of file diff --git a/immich/immich-env b/immich/immich-env new file mode 100644 index 0000000..bccb4e4 --- /dev/null +++ b/immich/immich-env @@ -0,0 +1,21 @@ +# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables + +# The location where your uploaded files are stored +UPLOAD_LOCATION=/data/immich/library +# The location where your database files are stored +DB_DATA_LOCATION=/data/immich/postgres + +# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List +# TZ=Etc/UTC + +# The Immich version to use. You can pin this to a specific version like "v1.71.0" +IMMICH_VERSION=release + +# Connection secret for postgres. You should change it to a random password +# Please use only the characters `A-Za-z0-9`, without special characters or spaces +DB_PASSWORD=postgres + +# The values below this line do not need to be changed +################################################################################### +DB_USERNAME=postgres +DB_DATABASE_NAME=immich \ No newline at end of file diff --git a/jellyfin/.dockerignore b/jellyfin/.dockerignore new file mode 100644 index 0000000..d1b8bec --- /dev/null +++ b/jellyfin/.dockerignore @@ -0,0 +1,3 @@ +__pycache__/ +Dockerifile +*.http \ No newline at end of file diff --git a/jellyfin/Dockerfile b/jellyfin/Dockerfile new file mode 100644 index 0000000..9d1fff8 --- /dev/null +++ b/jellyfin/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.10 + +RUN pip install pydantic requests python-dotenv + +COPY jellyfin /app/jellyfin + +WORKDIR /app + +ENTRYPOINT [ "python" ] \ No newline at end of file diff --git a/jellyfin/create_all_songs_playlist.py b/jellyfin/create_all_songs_playlist.py new file mode 100644 index 0000000..9ae0db9 --- /dev/null +++ b/jellyfin/create_all_songs_playlist.py @@ -0,0 +1,85 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +# Set your Jellyfin server address and API key here +server_address = "https://jellyfin.alexmickelson.guru" +api_key = os.environ["JELLYFIN_TOKEN"] + +# Set the API endpoints to get all songs and create a playlist +songs_endpoint = ( + "/Users/b30951b36b37400498dbfd182d49a42e/Items" + + "?SortBy=DateCreated,SortName" + + "&SortOrder=Descending" + + "&IncludeItemTypes=Audio" + + "&Recursive=true" + + "&Fields=AudioInfo,ParentId" + + "&StartIndex=0" + + "&ImageTypeLimit=1" + + "&EnableImageTypes=Primary" + + "&Limit=100" + + "&ParentId=7e64e319657a9516ec78490da03edccb" +) +songs_endpoint = "/Users/b30951b36b37400498dbfd182d49a42e/Items" + +# Set the parameters for the API request to get all songs +params = { + "api_key": api_key, + "SortBy": "SortName", + "ParentId": "7e64e319657a9516ec78490da03edccb", +} + +# Make the API request to get all songs +response = requests.get(server_address + songs_endpoint, params=params) +# Parse the JSON response +data = json.loads(response.text) +# # Loop through the songs and print their names +for song in data["Items"]: + print(song["Name"], song["Id"]) + + +# Create a list of all song IDs +song_ids = [song["Id"] for song in data["Items"]] +ids = ",".join(song_ids) +# print(ids) +playlist_data = { + "Name": "All Songs", + "UserId": "b30951b36b37400498dbfd182d49a42e", + "Ids": ids, + "MediaType": "Audio", +} +headers = {"Content-type": "application/json"} +params = {"api_key": api_key} +playlist_endpoint = "/Playlists" +# https://jellyfin.alexmickelson.guru/Playlists?Name=test playlist&Ids=f78ddd409c5ebb2405f5477d15e8e23c&userId=b30951b36b37400498dbfd182d49a42e +response = requests.post( + server_address + playlist_endpoint, + headers=headers, + params=params, + data=json.dumps(playlist_data), +) +# print(response.text) +playlist_id = response.json()["Id"] + + + + +# add_song_url = f"/Playlists/{playlist_id}/Items" +# params = {"api_key": api_key} +# body = { +# "Ids": ids, +# "UserId": "b30951b36b37400498dbfd182d49a42e", +# "MediaType": "Audio", +# } + +# response = requests.post( +# server_address + add_song_url, headers=headers, params=params, json=body +# ) +# print(response.text) +# print(response.status_code) +# print(response.headers) + +# jellyfin_service.logout() \ No newline at end of file diff --git a/jellyfin/jellyfin.http b/jellyfin/jellyfin.http new file mode 100644 index 0000000..3f9609a --- /dev/null +++ b/jellyfin/jellyfin.http @@ -0,0 +1,50 @@ +# https://jellyfin.alexmickelson.guru/api-docs/swagger/index.html +# https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f +GET https://jellyfin.alexmickelson.guru/Users/b30951b36b37400498dbfd182d49a42e/Items + ?SortBy=SortName&SortOrder=Ascending + &IncludeItemTypes=Playlist + &Recursive=true + &Fields=PrimaryImageAspectRatio,SortName,CanDelete + &StartIndex=0 + &api_key={{$dotenv JELLYFIN_TOKEN}} + + +### +GET https://jellyfin.alexmickelson.guru/Users/b30951b36b37400498dbfd182d49a42e/Items + ?IncludeItemTypes=Playlist + &Recursive=true + &ParentId=7e64e319657a9516ec78490da03edccb + &api_key={{$dotenv JELLYFIN_TOKEN}} + +### +# get items from unindexed playlist +GET https://jellyfin.alexmickelson.guru/Playlists/2f191b23f0a49e70d6f90e9d82e408c6/Items + ?Fields=PrimaryImageAspectRatio + &EnableImageTypes=Primary,Backdrop,Banner,Thumb + &UserId=b30951b36b37400498dbfd182d49a42e + &api_key={{$dotenv JELLYFIN_TOKEN}} + +### remove item from unindexed +DELETE https://jellyfin.alexmickelson.guru/Playlists/2f191b23f0a49e70d6f90e9d82e408c6/Items + ?EntryIds=186f4d63492b405b97865ff9a99ef3ab + &userId=b30951b36b37400498dbfd182d49a42e +Authorization: MediaBrowser Client="scriptclient", Device="script", DeviceId="asdfasdfasdfasdfasdf", Version="1.0.0", Token="f313e2045fc34ce3ac510ce9ba2be1fc" + +### get all playlists +GET https://jellyfin.alexmickelson.guru/Users/b30951b36b37400498dbfd182d49a42e/Items + ?api_key={{$dotenv JELLYFIN_TOKEN}} + &ParentId=29772619d609592f4cdb3fc34a6ec97d + +### get token by user/pass +POST https://jellyfin.alexmickelson.guru/Users/AuthenticateByName +Content-Type: application/json +Authorization: MediaBrowser Client="scriptclient", Device="script", DeviceId="asdfasdfasdfasdfasdf", Version="1.0.0", Token="" + +{ + "Username": "alex", + "Pw": "{{$dotenv JELLYFIN_PASSWORD}}" +} + +### +POST https://jellyfin.alexmickelson.guru/Sessions/Logout +Authorization: MediaBrowser Client="scriptclient", Device="script", DeviceId="asdfasdfasdfasdfasdf", Version="1.0.0", Token="c704c71900cc41d2a454a4f3b5132778" diff --git a/jellyfin/jellyfin_service.py b/jellyfin/jellyfin_service.py new file mode 100644 index 0000000..6a1feb5 --- /dev/null +++ b/jellyfin/jellyfin_service.py @@ -0,0 +1,163 @@ +from functools import lru_cache +import os +from pprint import pprint +from typing import List, Optional +from pydantic import BaseModel, Field + +import requests +from dotenv import load_dotenv + +load_dotenv() + + +server_address = "https://jellyfin.alexmickelson.guru" +# api_key = os.environ["JELLYFIN_TOKEN"] +username = os.environ["JELLYFIN_USER"] +password = os.environ["JELLYFIN_PASSWORD"] +alex_user_id = "b30951b36b37400498dbfd182d49a42e" +all_songs_playlist_id = "2e176c02e7cc7f460c40bb1510723510" +unindexed_playlist_id = "2f191b23f0a49e70d6f90e9d82e408c6" + + +class Song(BaseModel): + Id: str + Name: str + Album: Optional[str] = Field(default=None) + Artists: Optional[List[str]] = Field(default=None) + + +class PlaylistSong(BaseModel): + Id: str + PlaylistItemId: str + Name: str + Album: Optional[str] = Field(default=None) + Artists: Optional[List[str]] = Field(default=None) + + +class Playlist(BaseModel): + Id: str + Name: str + Songs: List[PlaylistSong] + + +@lru_cache(maxsize=10) +def get_token(): + auth_endpoint = f"{server_address}/Users/AuthenticateByName" + body = {"Username": username, "Pw": password} + response = requests.post( + auth_endpoint, + json=body, + headers={ + "Content-Type": "application/json", + "Authorization": 'MediaBrowser Client="scriptclient", Device="script", DeviceId="testscriptasdfasdfasdf", Version="1.0.0", Token=""', + }, + ) + return response.json()["AccessToken"] + + +def get_auth_headers(): + token = get_token() + return { + "Authorization": f'MediaBrowser Client="scriptclient", Device="script", DeviceId="asdfasdfasdfasdfasdf", Version="1.0.0", Token="{token}"' + } + + +def get_all_songs(): + songs_endpoint = ( + f"{server_address}/Users/{alex_user_id}/Items" + # + "?SortBy=DateCreated,SortName" + # + "&SortOrder=Descending" + + "?IncludeItemTypes=Audio" + + "&Recursive=true" + # + "&Fields=AudioInfo,ParentId" + # + "&StartIndex=0" + # + "&ImageTypeLimit=1" + # + "&EnableImageTypes=Primary" + # + "&Limit=100" + + "&ParentId=7e64e319657a9516ec78490da03edccb" + ) + params = { + "SortBy": "SortName", + } + response = requests.get(songs_endpoint, params=params, headers=get_auth_headers()) + if not response.ok: + print(response.status_code) + print(response.text) + data = response.json() + + songs = [Song(**song) for song in data["Items"]] + return songs + + +def add_song_to_playlist(song_id: str, playlist_id: str): + add_song_endpoint = f"{server_address}/Playlists/{playlist_id}/Items" + params = {"ids": song_id, "userId": alex_user_id} + response = requests.post( + add_song_endpoint, params=params, headers=get_auth_headers() + ) + if not response.ok: + print(response.status_code) + print(response.text) + + +def remove_song_from_playlist(song_playlist_id: str, playlist_id: str): + url = f"{server_address}/Playlists/{playlist_id}/Items" + params = { + "EntryIds": song_playlist_id, + "userId": alex_user_id, + } # , "apiKey": api_key} + response = requests.delete(url, params=params, headers=get_auth_headers()) + if not response.ok: + print(response.status_code) + print(response.text) + print(response.url) + print(song_playlist_id) + print(playlist_id) + print(response.content) + pprint(response.request.headers) + + +def get_songs_in_playlist(playlist_id: str): + url = f"{server_address}/Playlists/{playlist_id}/Items" + params = {"userId": alex_user_id} + response = requests.get(url, params=params, headers=get_auth_headers()) + if not response.ok: + print(response.status_code) + print(response.text) + raise Exception(f"Error getting songs in playlist: {playlist_id}") + data = response.json() + + songs = [PlaylistSong.parse_obj(song) for song in data["Items"]] + return songs + + +def get_all_playlists(): + url = f"{server_address}/Users/{alex_user_id}/Items" + params = { + "IncludeItemTypes": "Playlist", + "Recursive": True, + "ParentId": "29772619d609592f4cdb3fc34a6ec97d", + } + response = requests.get(url, params=params, headers=get_auth_headers()) + if not response.ok: + print(response.status_code) + print(response.text) + raise Exception("Error getting all playlists") + + data = response.json() + print("got all playlists", len(data["Items"])) + playlists: List[Playlist] = [] + for playlist in data["Items"]: + songs = get_songs_in_playlist(playlist["Id"]) + playlist_object = Playlist( + Id=playlist["Id"], Name=playlist["Name"], Songs=songs + ) + playlists.append(playlist_object) + + return playlists + + +def logout(): + url = f"{server_address}/Sessions/Logout" + response = requests.post(url, headers=get_auth_headers()) + print("ending session: " + str(response.status_code)) diff --git a/jellyfin/update_all_songs_playlist.py b/jellyfin/update_all_songs_playlist.py new file mode 100644 index 0000000..483d191 --- /dev/null +++ b/jellyfin/update_all_songs_playlist.py @@ -0,0 +1,20 @@ +from jellyfin import jellyfin_service + + +if __name__ == "__main__": + all_songs = jellyfin_service.get_all_songs() + print("total songs", len(all_songs)) + playlist_songs = jellyfin_service.get_songs_in_playlist( + jellyfin_service.all_songs_playlist_id + ) + print("songs already in playlist", len(playlist_songs)) + playlist_ids = [s.Id for s in playlist_songs] + + for song in all_songs: + if song.Id not in playlist_ids: + print(f"adding song {song.Name} to playlist") + jellyfin_service.add_song_to_playlist( + song.Id, jellyfin_service.all_songs_playlist_id + ) + + jellyfin_service.logout() \ No newline at end of file diff --git a/jellyfin/update_unindexed.py b/jellyfin/update_unindexed.py new file mode 100644 index 0000000..b0a559b --- /dev/null +++ b/jellyfin/update_unindexed.py @@ -0,0 +1,34 @@ +from pprint import pprint +from jellyfin import jellyfin_service + +if __name__ == "__main__": + all_songs = jellyfin_service.get_all_songs() + playlists = jellyfin_service.get_all_playlists() + song_ids_in_playlist = list( + set( + song.Id + for playlist in playlists + for song in playlist.Songs + if playlist.Id != jellyfin_service.unindexed_playlist_id + and playlist.Id != jellyfin_service.all_songs_playlist_id + ) + ) + unindexed_playlist = next( + p for p in playlists if p.Id == jellyfin_service.unindexed_playlist_id + ) + unindexed_songs_ids = [song.Id for song in unindexed_playlist.Songs] + for song in all_songs: + if song.Id not in song_ids_in_playlist: + if song.Id not in unindexed_songs_ids: + print(f"adding {song.Name} to unindexed playlist") + jellyfin_service.add_song_to_playlist( + song.Id, jellyfin_service.unindexed_playlist_id + ) + for song in unindexed_playlist.Songs: + if song.Id in song_ids_in_playlist: + print(f"removing {song.Name} from unindexed playlist") + # pprint(song) + jellyfin_service.remove_song_from_playlist( + song.PlaylistItemId, jellyfin_service.unindexed_playlist_id + ) + jellyfin_service.logout() diff --git a/kubernetes/rancher-install.md b/kubernetes/rancher-install.md new file mode 100644 index 0000000..bb094ab --- /dev/null +++ b/kubernetes/rancher-install.md @@ -0,0 +1,6 @@ +# sources + +https://ranchermanager.docs.rancher.com/pages-for-subheaders/install-upgrade-on-a-kubernetes-cluster + +install r3k for manager cluster: https://docs.k3s.io/quick-start + diff --git a/linode/web/docker-compose.yml b/linode/web/docker-compose.yml new file mode 100644 index 0000000..0cf12db --- /dev/null +++ b/linode/web/docker-compose.yml @@ -0,0 +1,52 @@ +version: "3.8" +services: + swag: + image: ghcr.io/linuxserver/swag + container_name: swag-proxy + cap_add: + - NET_ADMIN + environment: + - PUID=1000 + - PGID=1000 + - TZ=America/Denver + - URL=alexmickelson.guru + - SUBDOMAINS=wildcard + - VALIDATION=dns + - DNSPLUGIN=cloudflare + volumes: + - ./nginx/default.conf:/config/nginx/site-confs/default + - /data/swag:/config + - /var/www/html:/var/www/html:ro + ports: + - 443:443 + - 80:80 #optional + restart: unless-stopped + networks: + linode-web: + proxy: + + pihole: + container_name: pihole + image: pihole/pihole + ports: + - 0.0.0.0:53:53/tcp + - 0.0.0.0:53:53/udp + # - 67:67/udp # dhcp + #- "80:80/tcp" + environment: + TZ: 'America/Denver' + VIRTUAL_HOST: alexmickelson.guru + WEBPASSWORD: chaos-concise-nickname + volumes: + - /data/pihole/etc-pihole/:/etc/pihole/ + - /data/pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/ + cap_add: + - NET_ADMIN + restart: unless-stopped + networks: + linode-web: + +networks: + linode-web: + proxy: + external: true \ No newline at end of file diff --git a/linode/web/nginx/default.conf b/linode/web/nginx/default.conf new file mode 100644 index 0000000..c46876c --- /dev/null +++ b/linode/web/nginx/default.conf @@ -0,0 +1,58 @@ +error_page 502 /502.html; + + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + include /config/nginx/ssl.conf; + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + + root /var/www/html; + index index.html index.htm index.php; + + server_name alexmickelson.guru; + + location /admin/ { + rewrite /(.*) /$1 break; + proxy_pass http://pihole; + proxy_set_header Host $http_host; + + # allow 172.18.0.0/24; + # deny all; + } + location / { + try_files $uri $uri/ /index.html; + allow all; + } + # allow 172.18.0.0/24; + # deny all; +} + +# server { +# listen 443 ssl http2; +# listen [::]:443 ssl http2; +# include /config/nginx/ssl.conf; +# include /config/nginx/proxy.conf; +# include /config/nginx/resolver.conf; + +# root /config/www; +# index index.html index.htm index.php; + +# server_name wg.alexmickelson.guru; + +# location / { +# proxy_pass http://wireguard-web:51821/; +# } +# allow 172.18.0.0/24; +# deny all; +# } + +proxy_cache_path cache/ keys_zone=auth_cache:10m; \ No newline at end of file diff --git a/linode/wireguard/docker-compose.yml b/linode/wireguard/docker-compose.yml new file mode 100644 index 0000000..a150247 --- /dev/null +++ b/linode/wireguard/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.8" +services: + wg-easy: + environment: + - WG_HOST=45.79.102.212 + - WG_DEFAULT_ADDRESS=10.11.0.x + - WG_ALLOWED_IPS=0.0.0.0/0, ::/0 + - WG_PERSISTENT_KEEPALIVE=25 + - WG_DEFAULT_DNS=45.79.102.212 + # - WG_PORT=51820 + env_file: + - ./wg-easy.env + image: weejewel/wg-easy + container_name: wireguard-web + volumes: + - /data/wireguard:/etc/wireguard + ports: + - 51820:51820/udp + - 51821:51821/tcp + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + networks: + default: + proxy: + +networks: + default: + + proxy: + external: true \ No newline at end of file diff --git a/notes/gpu-passthrough.md b/notes/gpu-passthrough.md new file mode 100644 index 0000000..e69de29 diff --git a/notes/ufw.md b/notes/ufw.md new file mode 100644 index 0000000..6ecf990 --- /dev/null +++ b/notes/ufw.md @@ -0,0 +1,41 @@ +# ufw + +### read logs +``` +sudo dmesg | grep '\\[UFW' +``` + +### interactions + +```bash +ufw allow from 172.19.0.2/32 to any port 443 +``` + + +### docker config in /etc/ufw/after.rules + +https://stackoverflow.com/questions/30383845/what-is-the-best-practice-of-docker-ufw-under-ubuntu + + +```bash +# BEGIN UFW AND DOCKER +*filter +:ufw-user-forward - [0:0] +:DOCKER-USER - [0:0] +-A DOCKER-USER -j RETURN -s 10.0.0.0/8 +-A DOCKER-USER -j RETURN -s 172.16.0.0/12 +-A DOCKER-USER -j RETURN -s 192.168.0.0/16 + +-A DOCKER-USER -j ufw-user-forward + +-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 +-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 +-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 +-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 +-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 +-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 + +-A DOCKER-USER -j RETURN +COMMIT +# END UFW AND DOCKER +``` \ No newline at end of file diff --git a/outbound-proxy/Dockerfile b/outbound-proxy/Dockerfile new file mode 100644 index 0000000..9762768 --- /dev/null +++ b/outbound-proxy/Dockerfile @@ -0,0 +1,11 @@ +FROM tailscale/tailscale:latest + +RUN apk add --no-cache \ + openssh-client \ + bash \ + fish \ + shadow + +RUN echo "/usr/bin/fish" >> /etc/shells && sed -i 's|/root:/bin/ash|/root:/usr/bin/fish|' /etc/passwd +COPY ./ssh-config.sh /ssh-config.sh +RUN chmod +x /ssh-config.sh diff --git a/outbound-proxy/docker-compose.yml b/outbound-proxy/docker-compose.yml new file mode 100644 index 0000000..e52315e --- /dev/null +++ b/outbound-proxy/docker-compose.yml @@ -0,0 +1,31 @@ +services: + tailscale-outbound: + build: . + hostname: tailscale-outbound + env_file: + - .env # TS_AUTHKEY + environment: + # - TS_EXTRA_ARGS=--advertise-tags=tag:container + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=false + - TS_OUTBOUND_HTTP_PROXY_LISTEN=:1055 + - TS_SOCKS5_SERVER=:1055 + volumes: + - tailscale-data:/var/lib/tailscale + # - ./ts-serve-config.json:/config/config.json + - /dev/net/tun:/dev/net/tun + # - $HOME/.ssh:/root/.ssh:ro + restart: unless-stopped + ports: + - 1055:1055 + privileged: true + # cap_add: + # - NET_ADMIN + # - sys_module + # nginx: + # image: nginx + # depends_on: + # - tailscale-outbound + # network_mode: service:tailscale-outbound +volumes: + tailscale-data: \ No newline at end of file diff --git a/outbound-proxy/ssh-config.sh b/outbound-proxy/ssh-config.sh new file mode 100755 index 0000000..f87c917 --- /dev/null +++ b/outbound-proxy/ssh-config.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +tailscale_status=$(tailscale status) + +# Extract the lines containing IP addresses and hostnames +# Example lines we are interested in: +# 100.101.102.103 server1 linux active; direct 192.168.1.101:41641, tx 3867808 rx 7391200 +# 100.101.102.104 server2 windows active; direct 192.168.1.102:41641, tx 3867808 rx 7391200 + +ssh_entries=$(echo "$tailscale_status" | awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print "Host " $2 "\n HostName " $1 "\n User alex\n"}') + +ssh_config_content="# SSH config - generated by tailscale script\n\n" +ssh_config_content+="$ssh_entries" + +output_file="$HOME/.ssh/config" + +mkdir -p /root/.ssh +echo -e "$ssh_config_content" > "$output_file" + +chmod 600 "$output_file" + +echo "SSH config file has been updated with Tailscale hosts at $output_file."