diff --git a/.githooks/pre-commit/build_docs.sh b/.githooks/pre-commit/build_docs.sh new file mode 100755 index 000000000..93d0c707b --- /dev/null +++ b/.githooks/pre-commit/build_docs.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +# Get the list of staged files +staged_files=$(git diff --cached --name-status | grep -E '^(A|M)' | awk '{print $2}') + +# Exit early if no files are staged for addition or modification +if [[ -z "$staged_files" ]]; then + # echo "No files staged for addition or modification. Exiting without linting." + exit 0 +fi + +# check if any of the staged files are in the docs directory +# ignore this if any of the files from docs/.vitepress/dist is staged +if [[ -n $(echo "$staged_files" | grep -E '^docs/') ]] && [[ -z $(echo "$staged_files" | grep -E '^docs/.vitepress/dist/') ]]; then + echo "Docs have changed but not built yet. Building docs..." + docker compose run -T --rm --entrypoint "" docs npm run docs:build + echo "Docs built successfully. Please stage the changes in docs/.vitepress/dist/ and commit again." + exit 1 +fi \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 539e8ac7d..cfdecf586 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -35,7 +35,7 @@ swagger_output.json # generated reports reports -test.js +src/test*.js src/test.js coverage diff --git a/api/Dockerfile b/api/Dockerfile index 5b14fcc59..f258f3227 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:19 +FROM node:21 ARG APP_GID ARG APP_UID @@ -23,9 +23,9 @@ RUN mkdir -p /home/appuser/.local/bin ENV PATH="/home/appuser/.local/bin/:${PATH}" # install pnpm and install packages from pnpm-lock.yaml -RUN npm install pm2 -g +# RUN npm install pm2 -g # RUN npm install -g pnpm -ENTRYPOINT npm ci && npx prisma generate client && exec pm2-runtime ecosystem.config.js +ENTRYPOINT npm ci && npx prisma generate client && exec node src/cluster.js # ENTRYPOINT pnpm fetch && npx prisma db push && exec /opt/sca/app/node_modules/.bin/nodemon --signal SIGTERM src/index.js # Entrypoint will be run in the shell mode - https://docs.docker.com/engine/reference/builder/#shell-form-entrypoint-example diff --git a/api/config/default.json b/api/config/default.json index ebc41c8d2..8b3e2e720 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -5,8 +5,20 @@ "port": 3030, "host": "localhost" }, + "metrics": { + "cluster": { + "port": 9999 + } + }, + "cluster": { + "enabled": true, + "max_workers": 2, + "max_restarts": 3, + "max_restarts_interval": 10000, + "grace": 5000 + }, "logger": { - "level": "debug" + "level": "info" }, "workflow_server": { "base_url": "http://127.0.0.1:5001", diff --git a/api/config/localhost.json b/api/config/localhost.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api/config/localhost.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api/ecosystem.config.js b/api/ecosystem.config.js deleted file mode 100644 index 0aa9135bb..000000000 --- a/api/ecosystem.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = [{ - script: 'src/index.js', - name: 'api', - exec_mode: 'cluster', - instances: 2, - exp_backoff_restart_delay: 100, - max_restarts: 3, - watch: false, -}]; diff --git a/api/package-lock.json b/api/package-lock.json index 9987de985..b1799aa7d 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -22,6 +22,7 @@ "dompurify": "^3.0.9", "dotenv-safe": "^8.2.0", "express": "^4.18.2", + "express-prom-bundle": "^8.0.0", "express-validator": "^7.0.1", "he": "^1.2.0", "http-errors": "^2.0.0", @@ -30,8 +31,13 @@ "lodash": "^4.17.21", "morgan": "^1.10.0", "multer": "1.4.5-lts.1", + "node-cron": "^3.0.3", "picomatch": "^4.0.2", - "spark-md5": "^3.0.2", + "pino": "^9.6.0", + "pino-caller": "^3.4.0", + "pino-pretty": "^13.0.0", + "prom-client": "^15.1.3", + "rate-limiter-flexible": "^6.2.1", "swagger-autogen": "^2.23.5", "swagger-ui-express": "^5.0.0", "uuid": "^9.0.1", @@ -381,6 +387,14 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -455,6 +469,49 @@ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", "hasInstallScript": true }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -467,12 +524,54 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -952,6 +1051,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1021,6 +1128,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1333,6 +1445,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -1621,6 +1738,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -1880,6 +2005,14 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhance-visitors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/enhance-visitors/-/enhance-visitors-1.0.0.tgz", @@ -2463,6 +2596,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-prom-bundle": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-8.0.0.tgz", + "integrity": "sha512-UHdpaMks6Z/tvxQsNzhsE7nkdXb4/zEh/jwN0tfZSZOEF+aD0dlfl085EU4jveOq09v01c5sIUfjV4kJODZ2eQ==", + "dependencies": { + "@types/express": "^5.0.0", + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "prom-client": ">=15.0.0" + } + }, "node_modules/express-validator": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", @@ -2491,6 +2640,11 @@ "node >=0.6.0" ] }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2537,6 +2691,19 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -3015,6 +3182,11 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -3628,6 +3800,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4204,6 +4384,25 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -4431,6 +4630,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4641,6 +4848,77 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-caller": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pino-caller/-/pino-caller-3.4.0.tgz", + "integrity": "sha512-2aEjlmhLA7J3lGBXKDSxtlfDY+cBzGh5PnLFP6ZUhvyqCnqKfv28ulpSch6uymGIdo7fzxXHK2hvR5FrdzbhTg==", + "dependencies": { + "source-map-support": "^0.5.13" + }, + "engines": { + "node": ">6.0.0" + }, + "peerDependencies": { + "pino": "*" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4691,6 +4969,33 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4725,6 +5030,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4772,6 +5086,11 @@ } ] }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/random": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/random/-/random-4.1.0.tgz", @@ -4798,6 +5117,11 @@ "node": ">= 0.6" } }, + "node_modules/rate-limiter-flexible": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-6.2.1.tgz", + "integrity": "sha512-d9AN+d/wwKW3/yHAL0G3zKpWZQFe55VjRGIFK9VG1w3CSOkcRqRqh0NhCiIXvgKhihNZPjGfISuN3it07NjPbw==" + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -4866,6 +5190,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5126,6 +5458,11 @@ "node": ">=v12.22.7" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -5372,10 +5709,38 @@ "node": ">=8" } }, - "node_modules/spark-md5": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", - "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==" + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } }, "node_modules/stack-trace": { "version": "0.0.10", @@ -5507,7 +5872,6 @@ "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, "engines": { "node": ">=8" }, @@ -5588,6 +5952,14 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -5599,6 +5971,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5858,6 +6238,11 @@ "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -5892,6 +6277,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/url-value-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.2.0.tgz", + "integrity": "sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/api/package.json b/api/package.json index 4dcc7729d..11964c031 100644 --- a/api/package.json +++ b/api/package.json @@ -7,7 +7,7 @@ "node": ">=18.12.0" }, "scripts": { - "start": "nodemon src/index.js", + "start": "nodemon src/cluster.js", "swagger": "node src/scripts/swagger.js", "lint": "eslint . --ext .js --fix --ignore-path .gitignore" }, @@ -40,6 +40,7 @@ "dompurify": "^3.0.9", "dotenv-safe": "^8.2.0", "express": "^4.18.2", + "express-prom-bundle": "^8.0.0", "express-validator": "^7.0.1", "he": "^1.2.0", "http-errors": "^2.0.0", @@ -48,8 +49,13 @@ "lodash": "^4.17.21", "morgan": "^1.10.0", "multer": "1.4.5-lts.1", + "node-cron": "^3.0.3", "picomatch": "^4.0.2", - "spark-md5": "^3.0.2", + "pino": "^9.6.0", + "pino-caller": "^3.4.0", + "pino-pretty": "^13.0.0", + "prom-client": "^15.1.3", + "rate-limiter-flexible": "^6.2.1", "swagger-autogen": "^2.23.5", "swagger-ui-express": "^5.0.0", "uuid": "^9.0.1", diff --git a/api/src/app.js b/api/src/app.js index c83daa0fe..9899f8c34 100644 --- a/api/src/app.js +++ b/api/src/app.js @@ -16,6 +16,8 @@ const { axiosErrorHandler, prismaConstraintFailedHandler, } = require('./middleware/error'); +const { metricsMiddleware } = require('./core/metrics'); +const swagger = require('./scripts/swagger'); // Register application const app = express(); @@ -46,13 +48,16 @@ app.use(compression()); if (!['production', 'test'].includes(config.get('mode'))) { // mount swagger ui try { - const swaggerFile = JSON.parse(fs.readFileSync('./swagger_output.json')); + const swaggerFile = JSON.parse(fs.readFileSync(swagger.outputFile, 'utf8')); app.use('/doc', swaggerUi.serve, swaggerUi.setup(swaggerFile)); } catch (e) { - console.error('Unable to load "./swagger_output.json"', e); + // console.error('Unable to load "./swagger_output.json"', e); } } +// prometheus metrics +app.use(metricsMiddleware); + // mount router app.use('/', indexRouter); diff --git a/api/src/cluster.js b/api/src/cluster.js new file mode 100644 index 000000000..c5167c311 --- /dev/null +++ b/api/src/cluster.js @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +const path = require('path'); +// __basedir is the path of root directory +// has same value when used in any js file in this project +global.__basedir = path.join(__dirname, '..'); +require('dotenv-safe').config(); +const config = require('config'); + +const express = require('express'); +const promBundle = require('express-prom-bundle'); +const promClient = require('prom-client'); +const manage_cluster = require('./core/cluster-manager'); +const { beforeApplicationFork } = require('./core/lifecycle'); + +function master() { + const metricsApp = express(); + metricsApp.use('/metrics', promBundle.clusterMetrics()); + + const port = config.get('metrics.cluster.port'); + // const metricsServer = + metricsApp.listen(port, () => { + console.log(`Aggregated metrics listening on : http://localhost:${port}/metrics`); + }); + + // function shutdown() { + // return new Promise((resolve) => { + // metricsServer.close(() => { + // console.log('Metrics server closed'); + // resolve(); + // }); + // }); + // } + // // Attach shutdown handler to the master process + // return shutdown; +} + +function worker() { + // eslint-disable-next-line no-new + new promClient.AggregatorRegistry(); // Required for metrics + // eslint-disable-next-line global-require + require('./index'); // Business logic +} + +manage_cluster({ + master, + worker, + beforeApplicationFork, + count: config.get('cluster.max_workers'), + max_restarts: config.get('cluster.max_restarts'), + max_restarts_interval: config.get('cluster.max_restarts_interval'), + grace: config.get('cluster.grace'), + signals: ['SIGTERM', 'SIGINT'], +}); diff --git a/api/src/core/cluster-manager.js b/api/src/core/cluster-manager.js new file mode 100644 index 000000000..5eebf75e9 --- /dev/null +++ b/api/src/core/cluster-manager.js @@ -0,0 +1,122 @@ +/* eslint-disable no-console */ +const cluster = require('node:cluster'); +const os = require('node:os'); +const process = require('node:process'); +const { RateLimiterMemory } = require('rate-limiter-flexible'); + +/** + * Manages a cluster of worker processes with configurable options for scaling, + * restart limits, and graceful shutdown. + * + * @param {Object} options - Configuration options for the cluster manager. + * @param {Function} [options.master=null] - Optional callback to execute in the master process. + * @param {Function} options.worker - Callback to execute in each worker process. + * @param {Function} [options.beforeApplicationFork=null] - Optional callback to execute before forking workers. + * @param {number} [options.count=2] - Desired number of worker processes (default is 2). + * @param {number} [options.max_restarts=3] - Maximum number of worker restarts allowed within the interval. + * @param {number} [options.max_restarts_interval=10000] - Time interval (in milliseconds) for the restart limit. + * @param {number} [options.grace=5000] - Grace period (in milliseconds) for workers to shut down gracefully. + * @param {string[]} [options.signals=['SIGINT', 'SIGTERM']] - List of signals to listen for to trigger shutdown. + * + * @returns {void} + * + * @example + * manage_cluster({ + * master: () => console.log('Master process running'), + * worker: () => setInterval(() => console.log('Worker process running'), 1000), + * count: 4, + * max_restarts: 5, + * max_restarts_interval: 15000, + * grace: 3000, + * signals: ['SIGINT', 'SIGTERM', 'SIGHUP'], + * }); + */ +async function manage_cluster({ + master = null, + worker, + beforeApplicationFork = null, + count = 2, + max_restarts = 3, + max_restarts_interval = 10000, + grace = 5000, + signals = ['SIGINT', 'SIGTERM'], +}) { + const numCPUs = Math.min(count, os.availableParallelism()); + + if (cluster.isPrimary) { + console.log(`Primary ${process.pid} is running`); + + // Execute the optional callback before forking workers + if (beforeApplicationFork) { + await beforeApplicationFork(); + } + + let activeWorkers = numCPUs; + let exiting = false; + const restartLimiter = new RateLimiterMemory({ + points: max_restarts, + duration: max_restarts_interval / 1000, // seconds + }); + + for (let i = 0; i < numCPUs; i += 1) { + cluster.fork(); + } + + cluster.on('exit', async (_worker, code, signal) => { + activeWorkers -= 1; + console.log(`Worker ${_worker.process.pid} died (${signal || code})`); + + if (!exiting) { + try { + await restartLimiter.consume('restart'); + console.log('Restarting worker...'); + activeWorkers += 1; + cluster.fork(); + } catch { + console.log('Restart limit reached, not spawning new workers.'); + } + } + + if (activeWorkers === 0) { + console.log('All workers have exited. Shutting down master...'); + if (exiting) { + process.exit(0); + } + process.exit(1); + } + }); + + // let shutdownMaster; + const shutdown = async () => { + // If the shutdown function is already called, do nothing to prevent multiple calls + if (exiting) return; + + console.log('Shutting down cluster...'); + exiting = true; + + Object.values(cluster.workers).forEach((_worker) => _worker.process.kill('SIGTERM')); + // unref- If there is no other activity keeping the event loop running, + // the process may exit before the Timeout object's callback is invoked. + setTimeout(() => process.exit(1), grace).unref(); + + // try { + // await shutdownMaster?.(); + // } catch (error) { + // console.error('Error during master shutdown:', error); + // } finally { + // Object.values(cluster.workers).forEach((_worker) => _worker.process.kill('SIGTERM')); + // // unref- If there is no other activity keeping the event loop running, + // // the process may exit before the Timeout object's callback is invoked. + // setTimeout(() => process.exit(1), grace).unref(); + // } + }; + + signals.forEach((signal) => process.on(signal, shutdown)); + master?.(); + } else { + console.log(`Worker ${process.pid} started`); + worker?.(); + } +} + +module.exports = manage_cluster; diff --git a/api/src/core/cron.js b/api/src/core/cron.js new file mode 100644 index 000000000..83eda599a --- /dev/null +++ b/api/src/core/cron.js @@ -0,0 +1,15 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable global-require */ +const cron = require('node-cron'); +const { createTaskLogger } = require('./logger'); + +function registerCronJobs() { + // Example: Schedule a task to run every minute + // cron.schedule('* * * * *', () => { + // const task = require('../cron/exampleTask.cron'); + // const taskLogger = createTaskLogger('exampleTask'); + // task.run(taskLogger); + // }); +} + +module.exports = registerCronJobs; diff --git a/api/src/core/lifecycle.js b/api/src/core/lifecycle.js new file mode 100644 index 000000000..e731b13a0 --- /dev/null +++ b/api/src/core/lifecycle.js @@ -0,0 +1,52 @@ +const fs = require('node:fs'); +const config = require('config'); +const { logger } = require('./logger'); +const swagger = require('../scripts/swagger'); +const registerCronJobs = require('./cron'); + +// run only in master process before forking workers +async function beforeApplicationFork() { + try { + logger.debug('Before Application Fork: Running master process tasks (one-time setup)'); + + if (!fs.existsSync(swagger.outputFile)) { + await swagger.generate(); + } + + registerCronJobs(); + } catch (error) { + logger.error('Error in beforeApplicationFork:', error); + process.exit(1); // Exit if critical setup fails + } +} + +async function onApplicationBootstrap() { + try { + if (config.get('auth.auto_sign_up.enabled')) { + logger.warn(`⚠️ Auto sign up (default role: "${config.get('auth.auto_sign_up.default_role')}") is enabled. \ +If this is not intended, please disable it in the configuration. \ +This feature can be exploited to create multiple user accounts without rate limiting.‼️\n`); + } + logger.debug('On Application Bootstrap'); + } catch (error) { + logger.error('Error in onApplicationBootstrap:', error); + process.exit(1); // Exit if critical setup fails + } +} + +// run in worker processes when shutting down before closing the server +async function beforeApplicationShutdown() { + logger.debug('Before Application Shutdown'); +} + +// run in worker processes after closing the server +async function onApplicationShutdown() { + logger.debug('On Application Shutdown'); +} + +module.exports = { + beforeApplicationFork, + onApplicationBootstrap, + beforeApplicationShutdown, + onApplicationShutdown, +}; diff --git a/api/src/core/logger.js b/api/src/core/logger.js new file mode 100644 index 000000000..f69e7c228 --- /dev/null +++ b/api/src/core/logger.js @@ -0,0 +1,61 @@ +const path = require('path'); + +const pino = require('pino'); +const pinoCaller = require('pino-caller'); +const config = require('config'); + +function createDevLogger() { + const logger = pino({ + level: config.get('logger.level'), + formatters: { + level: (label) => ({ level: label.toUpperCase() }), + }, + timestamp: pino.stdTimeFunctions.isoTime, + transport: { + target: 'pino-pretty', + options: { colorize: true }, // Add color for better visibility + }, + }); + + const loggerWithCaller = pinoCaller(logger, { relativeTo: global.__basedir }); + return loggerWithCaller; +} + +/** + * Creates a Pino logger instance for a given task + * @param {string} taskName - Name of the task + * @returns {pino.Logger} - Pino logger instance + */ +function createTaskLogger(taskName) { + return pino({ + level: config.get('logger.level'), + formatters: { + level: (label) => ({ level: label.toUpperCase() }), + }, + timestamp: pino.stdTimeFunctions.isoTime, + transport: { + target: 'pino/file', + options: { + destination: path.join(global.__basedir, `logs/${taskName}.log`), // Log to task-specific file + mkdir: true, // Ensure directory exists + }, + }, + }); +} + +// https://betterstack.com/community/guides/logging/how-to-install-setup-and-use-pino-to-log-node-js-applications/#adding-context-to-your-logs +// logger.error( +// { transaction_id: '12343_ff', user_id: 'johndoe' }, +// 'Transaction failed' +// ); + +// try { +// alwaysThrowError(); +// } catch (err) { +// logger.error(err, 'An unexpected error occurred while processing the request'); +// } + +module.exports = { + logger: config.get('mode') === 'production' ? createTaskLogger('app') : createDevLogger(), + createTaskLogger, +}; diff --git a/api/src/core/metrics.js b/api/src/core/metrics.js new file mode 100644 index 000000000..fac21e3f3 --- /dev/null +++ b/api/src/core/metrics.js @@ -0,0 +1,84 @@ +const promBundle = require('express-prom-bundle'); +const client = require('prom-client'); +const config = require('config'); + +const CLIENT_CLOSED_REQUEST_CODE = 499; + +function normalizeStatusCode(res) { + if (res.headersSent) { + const code = res.status_code || res.statusCode; + // collapse all codes starting with 2 to '2xx' + if (code >= 200 && code < 300) { + return '2xx'; + } + // collapse all codes starting with 3 to '3xx' + if (code >= 300 && code < 400) { + return '3xx'; + } + // collapse all codes starting with 4 to '4xx' + // if (code >= 400 && code < 500) { + // return '4xx'; + // } + // collapse all codes starting with 5 to '5xx' + // if (code >= 500 && code < 600) { + // return '5xx'; + // } + return `${code}`; + } + return CLIENT_CLOSED_REQUEST_CODE; +} + +/** + * Reasing for histogram buckets: + * 0 - 0.03 (0-30ms, mostly empty but captures edge cases) + * 0.03 - 0.1 (30-100ms, high-resolution for fast requests) + * 0.1 - 0.3 (100-300ms, common API latencies) + * 0.3 - 1.0 (300ms-1s, still in acceptable range) + * 1.0 - 3.0 (1s-3s, noticeable slowdowns) + * 3.0 - 10.0 (3s-10s, major slow responses) + * 10.0 - 30.0 (10s-30s, very slow but still relevant) + * 30.0 - Inf (Requests that are extremely slow) + */ +const metricsMiddleware = promBundle({ + autoregister: !config.get('cluster.enabled'), + includeMethod: true, + includePath: true, + formatStatusCode: normalizeStatusCode, + // Return the path of the express route (i.e. /projects/:username/datasets/:id + // or /datasets/:id) instead of the full URL (i.e. /projects/ben/datasets/1234)") + normalizePath: (req) => req.baseUrl + (req.route?.path || req.path), + promClient: { + collectDefaultMetrics: { + }, + }, + buckets: [0.03, 0.1, 0.3, 1.0, 3.0, 10.0, 30.0], +}); + +/** + * Counter metric to track the total number of failed authentication attempts. + * + * This metric is labeled with the following: + * - `auth_method`: The authentication method used (e.g., 'password', 'oauth'). + * - `reason`: The reason for the failure (e.g., 'invalid_credentials', 'account_locked'). + * - `client_id`: The identifier for the client making the request (e.g., 'web', 'cli'). + * + * Example usage: + * + * ```javascript + * authFailures.inc({ + * auth_method: 'password', + * reason: 'invalid_credentials', + * client_id: 'web', + * }); + * ``` + */ +const authFailures = new client.Counter({ + name: 'auth_failures_total', + help: 'Total number of failed authentication attempts', + labelNames: ['auth_method', 'reason', 'client_id'], +}); + +module.exports = { + metricsMiddleware, + authFailures, +}; diff --git a/api/src/cron/exampleTask.cron.js b/api/src/cron/exampleTask.cron.js new file mode 100644 index 000000000..a158f623c --- /dev/null +++ b/api/src/cron/exampleTask.cron.js @@ -0,0 +1,12 @@ +module.exports.run = async function run(logger) { + logger.info('Example Task is running'); + // Your task logic here + try { + // Simulate some work + await new Promise((resolve) => { setTimeout(resolve, 1000); }); + throw new Error('Simulated error for demonstration purposes'); + // logger.info('Example Task completed successfully'); + } catch (error) { + logger.error(error, 'Example Task failed'); + } +}; diff --git a/api/src/db.js b/api/src/db.js index 6f7282ed6..b4129ac27 100644 --- a/api/src/db.js +++ b/api/src/db.js @@ -18,7 +18,9 @@ require('dotenv-safe').config(); // // console.log(dbUrl); // process.env.DATABASE_URL = dbUrl; -// Postgres columns can be of BigInt type for which serialization is not defined +// Override BigInt serialization for JSON.stringify +// Postgres columns can be of BigInt type, which JSON.stringify does not handle by default. +// This ensures BigInt values are converted to strings during serialization. // eslint-disable-next-line no-extend-native, func-names BigInt.prototype.toJSON = function () { return this.toString(); diff --git a/api/src/index.js b/api/src/index.js index 0a277ff3b..077d395f8 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -1,46 +1,58 @@ const path = require('path'); -// __basedir is the path of root directory -// has same value when used in any js file in this project +// __basedir is the path of the root directory +// has the same value when used in any JS file in this project global.__basedir = path.join(__dirname, '..'); +// Load environment variables and validate against .env.example require('dotenv-safe').config(); + require('./db'); const config = require('config'); const app = require('./app'); -const logger = require('./services/logger'); +const { logger } = require('./core/logger'); +const { + onApplicationBootstrap, + beforeApplicationShutdown, + onApplicationShutdown, +} = require('./core/lifecycle'); +const { safeAwait } = require('./utils'); -// log a warning when auto sign up is enabled -if (config.get('auth.auto_sign_up.enabled')) { - logger.warn(`⚠️ Auto sign up (default role: "${config.get('auth.auto_sign_up.default_role')}") is enabled. \ -If this is not intended, please disable it in the configuration. \ -This feature can be exploited to create multiple user accounts without rate limiting.‼️\n`); -} +async function main() { + await onApplicationBootstrap(); -const port = config.get('express.port'); -const host = config.get('express.host'); -const server = app.listen(port, () => { - logger.info(`Listening: http://${host}:${port}`); -}); + const port = config.get('express.port'); + const host = config.get('express.host'); + const server = app.listen(port, () => { + logger.info(`Listening: http://${host}:${port}`); + }); -const shutdown = () => { - logger.warn('server: closing'); - server.close((err) => { - if (err) { - logger.error('server: closed with ERROR', err); - process.exit(1); - } - logger.warn('server: closed'); - process.exit(); + const shutdown = async () => { + await safeAwait(beforeApplicationShutdown()); + logger.debug('server: closing'); + server.close(async (err) => { + if (err) { + logger.error('server: closed with ERROR', err); + process.exit(1); + } + await safeAwait(onApplicationShutdown()); + logger.warn('server: closed'); + process.exit(); + }); + }; + + process.on('SIGINT', () => { + logger.debug('process received SIGINT'); + shutdown(); }); -}; -process.on('SIGINT', () => { - logger.warn('process received SIGINT'); - shutdown(); -}); + process.on('SIGTERM', () => { + logger.debug('process received SIGTERM'); + shutdown(); + }); +} -process.on('SIGTERM', () => { - logger.warn('process received SIGTERM'); - shutdown(); +main().catch((err) => { + logger.error('Error in main:', err); + process.exit(1); }); diff --git a/api/src/middleware/error.js b/api/src/middleware/error.js index bb516bcdc..62c5f6bf9 100644 --- a/api/src/middleware/error.js +++ b/api/src/middleware/error.js @@ -4,7 +4,7 @@ const { Prisma } = require('@prisma/client'); const axios = require('axios'); const _ = require('lodash/fp'); const { log_axios_error } = require('../utils'); -const logger = require('../services/logger'); +const { logger } = require('../core/logger'); // catch 404 and forward to error handler function notFound(req, res, next) { diff --git a/api/src/routes/datasetUploads.js b/api/src/routes/datasetUploads.js index 9be86721c..84b922406 100644 --- a/api/src/routes/datasetUploads.js +++ b/api/src/routes/datasetUploads.js @@ -6,7 +6,7 @@ const path = require('path'); const { PrismaClient } = require('@prisma/client'); const createError = require('http-errors'); -const logger = require('../services/logger'); +const { logger } = require('../core/logger'); const asyncHandler = require('../middleware/asyncHandler'); const { validate } = require('../middleware/validators'); const { accessControl } = require('../middleware/auth'); diff --git a/api/src/routes/datasets.js b/api/src/routes/datasets.js index 115231ae6..cde3fbba4 100644 --- a/api/src/routes/datasets.js +++ b/api/src/routes/datasets.js @@ -17,7 +17,7 @@ const { validate } = require('../middleware/validators'); const datasetService = require('../services/dataset'); const authService = require('../services/auth'); const CONSTANTS = require('../constants'); -const logger = require('../services/logger'); +const { logger } = require('../core/logger'); const isPermittedTo = accessControl('datasets'); diff --git a/api/src/routes/index.js b/api/src/routes/index.js index c2865bda9..a765b05de 100644 --- a/api/src/routes/index.js +++ b/api/src/routes/index.js @@ -13,11 +13,11 @@ router.use('/env', require('./env')); // From this point on, all routes require authentication. router.use(authenticate); -router.use('/datasets', require('./datasets') /* #swagger.security = [{"BearerAuth": []}] */); -router.use('/metrics', require('./metrics') /* #swagger.security = [{"BearerAuth": []}] */); -router.use('/users', require('./users') /* #swagger.security = [{"BearerAuth": []}] */); -router.use('/workflows', require('./workflows') /* #swagger.security = [{"BearerAuth": []}] */); -router.use('/projects', require('./projects') /* #swagger.security = [{"BearerAuth": []}] */); +router.use('/datasets', require('./datasets')); +router.use('/resource-metrics', require('./metrics')); +router.use('/users', require('./users')); +router.use('/workflows', require('./workflows')); +router.use('/projects', require('./projects')); router.use('/statistics', require('./statistics')); router.use('/notifications', require('./notifications')); router.use('/fs', require('./fs')); diff --git a/api/src/scripts/swagger.js b/api/src/scripts/swagger.js index 0e2c79810..281fa2830 100644 --- a/api/src/scripts/swagger.js +++ b/api/src/scripts/swagger.js @@ -1,5 +1,6 @@ const swaggerAutogen = require('swagger-autogen')(); const config = require('config'); +const path = require('node:path'); const doc = { info: { @@ -25,7 +26,16 @@ const doc = { ], }; -const outputFile = '../../swagger_output.json'; -const endpointsFiles = ['../routes/index.js']; +const outputFile = path.join(global.__basedir, 'swagger_output.json'); // '../../swagger_output.json'; +const endpointsFiles = [path.join(global.__basedir, 'src', 'routes', 'index.js')]; // ['../routes/index.js']; -swaggerAutogen(outputFile, endpointsFiles, doc); +// Generate swagger.json file +function generate() { + return swaggerAutogen(outputFile, endpointsFiles, doc); +} + +if (require.main === module) { + generate(); +} + +module.exports = { generate, outputFile }; diff --git a/api/src/services/auth.js b/api/src/services/auth.js index d62a689ae..f6e756e86 100644 --- a/api/src/services/auth.js +++ b/api/src/services/auth.js @@ -8,7 +8,7 @@ const config = require('config'); const { OAuth2Client } = require('@badgateway/oauth2-client'); const { PrismaClient } = require('@prisma/client'); -const logger = require('./logger'); +const { logger } = require('../core/logger'); const userService = require('./user'); const prisma = new PrismaClient(); @@ -105,7 +105,7 @@ const find_or_create_test_user = async ({ role }) => { }; function get_upload_token(file_path) { - // [^\w.-]+ matches one or more characters that are not word + // [^\w.-]+ matches one or more characters that are not word // characters (letters, digits, or underscore), dots, or hyphens const hyphen_delimited_file_path = file_path.replace(/[^\w.-]+/g, '-'); const scope = `${config.get('oauth.upload.scope_prefix')}${hyphen_delimited_file_path}`; diff --git a/api/src/services/logger.js b/api/src/services/logger.js deleted file mode 100644 index ba2342e11..000000000 --- a/api/src/services/logger.js +++ /dev/null @@ -1,20 +0,0 @@ -const winston = require('winston'); -const { format } = require('winston'); -const config = require('config'); - -const logger = winston.createLogger({ - format: format.combine( - format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss', - }), - // format.json(), - // eslint-disable-next-line max-len - format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}${info.splat !== undefined ? `${info.splat}` : ' '}`), - ), - transports: [ - new (winston.transports.Console)({ - level: config.get('logger.level'), - }), - ], -}); -module.exports = logger; diff --git a/api/src/utils/db.js b/api/src/utils/db.js new file mode 100644 index 000000000..e702bfea2 --- /dev/null +++ b/api/src/utils/db.js @@ -0,0 +1,76 @@ +const { Prisma } = require('@prisma/client'); + +class TransactionRetryError extends Error { + constructor(maxRetries, message = 'Transaction failed after maximum retries') { + const _mesage = maxRetries ? `${message}. Maximum retries: ${maxRetries}` : message; + super(_mesage); + this.name = 'TransactionRetryError'; + } +} + +/** + * Executes a transaction with retry logic in case of write conflicts or deadlocks. + * + * @param {object} prisma - The Prisma client instance. + * @param {function|array} statementsOrFn - A function representing an interactive transaction or an array of statements for a batch transaction. + * @param {object} [options] - Optional settings for the transaction. + * @param {number} [options.maxRetries=5] - The maximum number of retry attempts. + * @param {string} [options.isolationLevel=Prisma.TransactionIsolationLevel.RepeatableRead] - The isolation level for the transaction. + * @param {} [options.kwargs] - Additional options to pass to the transaction. + * + * @throws {TypeError} If the input is not a function or an array of statements. + * @throws {Error} If the Prisma client is not defined. + * @throws {Error} If maxRetries is less than 1. + * @throws {TransactionRetryError} If all retry attempts are exhausted. + * @returns {Promise<*>} The result of the transaction. + */ +async function transactionWithRetry( + prisma, + statementsOrFn, + { + maxRetries = 5, + isolationLevel = Prisma.TransactionIsolationLevel.RepeatableRead, + ...kwargs + } = {}, +) { + let retries = 0; + const isFunction = typeof statementsOrFn === 'function'; + const isArray = Array.isArray(statementsOrFn); + + if (!isFunction && !isArray) { + throw new TypeError('Invalid transaction input. Must be a function or an array of statements.'); + } + if (!prisma) { + throw new Error('Prisma client is not defined'); + } + if (maxRetries < 1) { + throw new Error('maxRetries must be greater than 0'); + } + while (retries < maxRetries) { + try { + if (isFunction) { + // Interactive transaction + // eslint-disable-next-line no-await-in-loop + return await prisma.$transaction(statementsOrFn, { isolationLevel, ...kwargs }); + } + // Batch transaction + // eslint-disable-next-line no-await-in-loop + return await prisma.$transaction(statementsOrFn, { isolationLevel, ...kwargs }); + } catch (error) { + if (error?.code === 'P2034') { + // P2034: "Transaction failed due to a write conflict or a deadlock. Please retry your transaction" + console.warn(`Transaction failed with error code P2034. Retrying (${retries + 1}/${maxRetries})...`); + retries += 1; + } else { + throw error; + } + } + } + // if we reach here, it means we have exhausted all retries + throw new TransactionRetryError(maxRetries); +} + +module.exports = { + TransactionRetryError, + transactionWithRetry, +}; diff --git a/api/src/utils/index.js b/api/src/utils/index.js index d534c95eb..f954e3e5e 100644 --- a/api/src/utils/index.js +++ b/api/src/utils/index.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); const _ = require('lodash/fp'); -const { Prisma } = require('@prisma/client'); function renameKey(oldKey, newKey) { return (obj) => { @@ -232,82 +231,32 @@ function readUsersFromJSON(fname) { } } -class TransactionRetryError extends Error { - constructor(maxRetries, message = 'Transaction failed after maximum retries') { - const _mesage = maxRetries ? `${message}. Maximum retries: ${maxRetries}` : message; - super(_mesage); - this.name = 'TransactionRetryError'; - } +function base64urlEncode(buf) { + return buf + .toString('base64') // Regular base64 encoder + .replace(/=/g, '') // Remove any trailing '='s + .replace(/\+/g, '-') // Replace '+' with '-' + .replace(/\//g, '_'); // Replace '/' with '_' } /** - * Executes a transaction with retry logic in case of write conflicts or deadlocks. - * - * @param {object} prisma - The Prisma client instance. - * @param {function|array} statementsOrFn - A function representing an interactive transaction or an array of statements for a batch transaction. - * @param {object} [options] - Optional settings for the transaction. - * @param {number} [options.maxRetries=5] - The maximum number of retry attempts. - * @param {string} [options.isolationLevel=Prisma.TransactionIsolationLevel.RepeatableRead] - The isolation level for the transaction. - * @param {} [options.kwargs] - Additional options to pass to the transaction. + * Safely handles a promise by returning an array with either the resolved data or the error. + * This function helps avoid using try-catch blocks when working with async/await. * - * @throws {TypeError} If the input is not a function or an array of statements. - * @throws {Error} If the Prisma client is not defined. - * @throws {Error} If maxRetries is less than 1. - * @throws {TransactionRetryError} If all retry attempts are exhausted. - * @returns {Promise<*>} The result of the transaction. + * @param {Promise} promise - The promise to be resolved or rejected. + * @returns {Promise<[Error|null, any|null]>} A promise that resolves to an array where the first element + * is the error (if any) or null, and the second element is the resolved data or null. */ -async function transactionWithRetry( - prisma, - statementsOrFn, - { - maxRetries = 5, - isolationLevel = Prisma.TransactionIsolationLevel.RepeatableRead, - ...kwargs - } = {}, -) { - let retries = 0; - const isFunction = typeof statementsOrFn === 'function'; - const isArray = Array.isArray(statementsOrFn); - - if (!isFunction && !isArray) { - throw new TypeError('Invalid transaction input. Must be a function or an array of statements.'); - } - if (!prisma) { - throw new Error('Prisma client is not defined'); - } - if (maxRetries < 1) { - throw new Error('maxRetries must be greater than 0'); - } - while (retries < maxRetries) { - try { - if (isFunction) { - // Interactive transaction - // eslint-disable-next-line no-await-in-loop - return await prisma.$transaction(statementsOrFn, { isolationLevel, ...kwargs }); - } - // Batch transaction - // eslint-disable-next-line no-await-in-loop - return await prisma.$transaction(statementsOrFn, { isolationLevel, ...kwargs }); - } catch (error) { - if (error?.code === 'P2034') { - // P2034: "Transaction failed due to a write conflict or a deadlock. Please retry your transaction" - console.warn(`Transaction failed with error code P2034. Retrying (${retries + 1}/${maxRetries})...`); - retries += 1; - } else { - throw error; - } +async function safeAwait(promise, { logError = false } = {}) { + try { + const data = typeof promise.then === 'function' ? await promise : promise; + return [null, data]; + } catch (error) { + if (logError) { + console.error('Error occurred:', error); } + return [error, null]; } - // if we reach here, it means we have exhausted all retries - throw new TransactionRetryError(maxRetries); -} - -function base64urlEncode(buf) { - return buf - .toString('base64') // Regular base64 encoder - .replace(/=/g, '') // Remove any trailing '='s - .replace(/\+/g, '-') // Replace '+' with '-' - .replace(/\//g, '_'); // Replace '/' with '_' } module.exports = { @@ -321,7 +270,6 @@ module.exports = { numericStringsToNumbers, decodeJWT, readUsersFromJSON, - transactionWithRetry, - TransactionRetryError, base64urlEncode, + safeAwait, }; diff --git a/docker-compose.yml b/docker-compose.yml index 7025788a6..be141a0b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ name: bioloop services: ui: - image: node:19 + image: node:21 volumes: - ./ui/:/opt/sca/app - ui_modules:/opt/sca/app/node_modules @@ -17,7 +17,7 @@ services: npm install && exec /opt/sca/app/node_modules/.bin/vite --host api: - image: node:19 + image: node:21 # user: ${APP_UID}:${APP_GID} # build: # context: ./api @@ -43,7 +43,7 @@ services: - | npm install \ && npx prisma generate client \ - && exec /opt/sca/app/node_modules/.bin/nodemon --signal SIGTERM src/index.js + && exec /opt/sca/app/node_modules/.bin/nodemon --signal SIGTERM src/cluster.js extra_hosts: @@ -70,20 +70,20 @@ services: expose: - 5432 - nginx: - image: nginx:1.25 - ports: - - 80:80 - - 443:443 - volumes: - - ./nginx/src:/usr/share/nginx/html # Mount for Nginx static files - - ./nginx/conf:/etc/nginx/conf.d # Mount for Nginx configuration files - - ./data:/opt/sca/data:ro # Mount for data directory - - ./ui/dist:/opt/sca/ui:ro # Mount for UI static files - - ./nginx/logs:/var/log/nginx # Mount for Nginx logs - - ./ui/.cert:/etc/nginx/certs:ro # Mount for SSL certificates - extra_hosts: - - "host.docker.internal:host-gateway" # for connecting to services running on localhost of the host network + # nginx: + # image: nginx:1.25 + # ports: + # - 80:80 + # - 443:443 + # volumes: + # - ./nginx/src:/usr/share/nginx/html # Mount for Nginx static files + # - ./nginx/conf:/etc/nginx/conf.d # Mount for Nginx configuration files + # - ./data:/opt/sca/data:ro # Mount for data directory + # - ./ui/dist:/opt/sca/ui:ro # Mount for UI static files + # - ./nginx/logs:/var/log/nginx # Mount for Nginx logs + # - ./ui/.cert:/etc/nginx/certs:ro # Mount for SSL certificates + # extra_hosts: + # - "host.docker.internal:host-gateway" # for connecting to services running on localhost of the host network postgres_exporter: image: prometheuscommunity/postgres-exporter @@ -122,7 +122,21 @@ services: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin - + docs: + image: node:21 + volumes: + - ./package.json:/opt/sca/app/package.json + - ./package-lock.json:/opt/sca/app/package-lock.json + - ./docs/:/opt/sca/app/docs + - docs_modules:/opt/sca/app/node_modules + ports: + - 127.0.0.1:5173:5173 + working_dir: /opt/sca/app + entrypoint: + - sh + - -c + - | + npm install && exec /opt/sca/app/node_modules/.bin/vitepress dev docs --host volumes: ui_modules: @@ -132,4 +146,5 @@ volumes: external: false grafana_data: - prometheus_data: \ No newline at end of file + prometheus_data: + docs_modules: \ No newline at end of file diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index e9f53e1a3..91e400712 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -1,10 +1,13 @@ -import { defineConfig } from 'vitepress' +import { defineConfig } from 'vitepress'; +import { withSidebar } from 'vitepress-sidebar'; // https://vitepress.dev/reference/site-config -export default defineConfig({ +const vitePressOptions = { title: "Bioloop", base: "/bioloop/docs/", description: "Bioloop Documentation", + head: [['link', { rel: 'icon', href: '/bioloop/docs/favicon.ico' }]], + lastUpdated: true, // Enable last updated timestamp themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ @@ -39,7 +42,15 @@ export default defineConfig({ socialLinks: [ { icon: 'github', link: 'https://github.com/IUSCA/bioloop' } - ] + ], + + search: { + provider: 'local' + }, + + editLink: { + pattern: 'https://github.com/IUSCA/bioloop/edit/main/docs/:path' + } }, ignoreDeadLinks: [ // ignore exact url "/playground" @@ -53,4 +64,21 @@ export default defineConfig({ return url.toLowerCase().includes('ignore') } ] -}) +}; + + +const vitePressSidebarOptions = { + // VitePress Sidebar's options here... + documentRootPath: '/docs', + collapsed: true, + capitalizeFirst: true, + includeFolderIndexFile: false, + useTitleFromFileHeading: true, + useTitleFromFrontmatter: true, + useFolderTitleFromIndexFile: true, + frontmatterOrderDefaultValue: 100, + sortMenusByFrontmatterOrder: true +}; + +export default defineConfig(withSidebar(vitePressOptions, vitePressSidebarOptions)); +// export default vitePressOptions; \ No newline at end of file diff --git a/docs/.vitepress/dist/404.html b/docs/.vitepress/dist/404.html index 0b5542fff..2dd7a5396 100644 --- a/docs/.vitepress/dist/404.html +++ b/docs/.vitepress/dist/404.html @@ -5,17 +5,19 @@ 404 | Bioloop - - + + + - - + + + -
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.
- +
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/cluster.html b/docs/.vitepress/dist/api/01-core/cluster.html new file mode 100644 index 000000000..b7cc14ab3 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/cluster.html @@ -0,0 +1,36 @@ + + + + + + Cluster Management | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Cluster Management

The manage_cluster function is a utility for managing a cluster of worker processes in a Node.js application. It provides features such as scaling, restart limits, and graceful shutdowns, making it easier to build robust and scalable applications.

This system is designed to efficiently manage multiple worker processes in a Node.js application. It leverages the node:cluster module to distribute workloads across available CPU cores, ensuring optimal utilization of system resources. This feature is particularly useful for high-performance applications that need to handle a large number of concurrent requests.

Without this system, the application would run as a single process, potentially underutilizing multi-core CPUs and becoming a bottleneck under heavy load.

Overview

The manage_cluster function allows you to:

  • Run a master process to manage worker processes.
  • Define custom logic for both master and worker processes.
  • Automatically restart workers within configurable limits.
  • Gracefully shut down workers on receiving termination signals.

Configuration Options

The function accepts an options object with the following properties:

  • master: A callback function to execute in the master process (optional).
  • worker: A callback function to execute in each worker process (required).
  • beforeApplicationFork: A callback function to execute before forking workers (optional).
  • count: The number of worker processes to spawn (default: 2).
  • max_restarts: Maximum number of worker restarts allowed within the interval (default: 3).
  • max_restarts_interval: Time interval (in milliseconds) for the restart limit (default: 10000).
  • grace: Grace period (in milliseconds) for workers to shut down gracefully (default: 5000).
  • signals: List of signals to listen for to trigger shutdown (default: ['SIGINT', 'SIGTERM']).

Example Usage

javascript
const manage_cluster = require('./core/cluster-manager');
+
+manage_cluster({
+  master: () => console.log('Master process running'),
+  worker: () => setInterval(() => console.log('Worker process running'), 1000),
+  count: 4,
+  max_restarts: 5,
+  max_restarts_interval: 15000,
+  grace: 3000,
+  signals: ['SIGINT', 'SIGTERM', 'SIGHUP'],
+});

In this example:

  • The master process logs a message when it starts.
  • Each worker process logs a message every second.
  • The cluster spawns 4 workers and allows up to 5 restarts within a 15-second interval.
  • Workers are given 3 seconds to shut down gracefully when a termination signal is received.

Metrics Integration

The master process can also expose aggregated metrics using the prom-client library. This is demonstrated in the cluster.js file, where a metrics server is set up to listen on a configurable port.

Refer to the cluster.js file for a complete example of integrating metrics with the cluster manager.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/configuration.html b/docs/.vitepress/dist/api/01-core/configuration.html new file mode 100644 index 000000000..6d1369b50 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/configuration.html @@ -0,0 +1,44 @@ + + + + + + Configuration | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Configuration

The configuration system is a critical part of the application, enabling developers to manage environment-specific settings and maintain clean, maintainable code. It uses the config module to load and manage configuration files, ensuring that the application behaves consistently across different environments.

Purpose of the Configuration System

The configuration system exists to centralize and standardize the way application settings are managed. Without it, developers would need to hardcode settings or rely on ad-hoc methods to manage environment-specific configurations, leading to brittle and error-prone code.

By using this system:

  • Developers can easily override settings for different environments (e.g., development, production).
  • Sensitive information, such as API keys and database credentials, can be securely managed using environment variables.
  • The application becomes easier to maintain and extend, as configuration logic is decoupled from the application logic.

How It Fits Into the System

The configuration system integrates seamlessly with the application by:

  • Loading settings from JSON files located in the ./config/ directory.
  • Allowing overrides via environment variables, command-line arguments, or external sources.
  • Providing a consistent API for accessing configuration values throughout the application.

This ensures that all parts of the application use the same source of truth for configuration, reducing duplication and potential inconsistencies.

Configuration Files

The following configuration files are used:

  • default.json: Contains default settings for the application.
  • {NODE_ENV}.json (e.g., production.json): Contains environment-specific overrides.
  • custom-environment-variables.json: Maps configuration properties to environment variables.

Precedence of Configuration

The configuration system resolves settings in the following order of precedence:

  1. Command-line arguments
  2. Environment variables
  3. {NODE_ENV}.json (e.g., production.json)
  4. default.json

This precedence ensures that the most specific settings are applied, while falling back to defaults when necessary.

Environment Variables

The application uses the dotenv-safe module to load environment variables from a .env file. This ensures that all required variables are defined and prevents runtime errors caused by missing configurations.

Example .env File

env
DATABASE_PASSWORD=your_database_password
+WORKFLOW_AUTH_TOKEN=your_auth_token
+OAUTH_BASE_URL=https://example.com/oauth

Loading Environment Variables

To load environment variables, the following code is used:

javascript
require('dotenv-safe').config();

Ensure that a .env.example file exists to document required variables and their expected format.

Step-by-Step Instructions for Usage

  1. Define Default Settings: Add default settings in default.json under the ./config/ directory. For example:

    json
    {
    +  "express": {
    +    "port": 3030,
    +    "host": "localhost"
    +  }
    +}
  2. Add Environment-Specific Overrides: Create a file named {NODE_ENV}.json (e.g., production.json) and override specific settings:

    json
    {
    +  "express": {
    +    "port": 8080
    +  }
    +}
  3. Map Environment Variables: Use custom-environment-variables.json to map sensitive settings to environment variables:

    json
    {
    +  "express": {
    +    "port": "EXPRESS_PORT"
    +  }
    +}
  4. Create a .env File: Define the required environment variables in a .env file:

    env
    EXPRESS_PORT=3030
  5. Load Configuration in Code: Access configuration values in your application using the config module:

    javascript
    require('dotenv-safe').config();
    +const config = require('config');
    +const port = config.get('express.port');
    +console.log(`Server running on port ${port}`);

foo bar

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/cron-task-scheduling.html b/docs/.vitepress/dist/api/01-core/cron-task-scheduling.html new file mode 100644 index 000000000..03c885f5f --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/cron-task-scheduling.html @@ -0,0 +1,41 @@ + + + + + + Scheduling Tasks | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Scheduling Tasks

The cron task scheduling feature allows you to define and execute recurring tasks at specified intervals in the primary process of the cluster. This feature is useful for automating maintenance, cleanup, and other background operations in your application.

How It Works

The registerCronJobs function in the /api/src/core/cron.js file is responsible for registering all cron jobs. It uses the node-cron library to define and schedule tasks. Each task is implemented as a separate module, ensuring modularity and reusability.

Usage Instructions

  1. Define a New Task:

    • Create a new file in the cron directory (e.g., exampleTask.cron.js).
    • Export a run async function that contains the task logic.
    javascript
    module.exports.run = async (logger) => {
    +  logger.info('Running example task...');
    +  // Task logic here
    +};
  2. Register the Task:

    • Open the cron.js file.
    • Use the cron.schedule method to define the schedule and link the task.
    javascript
    // filepath: /Users/deduggi/Documents/SCA/bioloop/api/src/core/cron.js
    +const cron = require('node-cron');
    +const { createTaskLogger } = require('./logger');
    +
    +function registerCronJobs() {
    +  cron.schedule('* * * * *', () => {
    +    const task = require('../cron/exampleTask.cron');
    +    const taskLogger = createTaskLogger('exampleTask');
    +    task.run(taskLogger);
    +  });
    +}
    +
    +module.exports = registerCronJobs;

Cron tasks are registered using the beforeApplicationFork lifecycle hook ensuring that they run in the primary process of the cluster.

The createTaskLogger function creates a logger instance for each task, allowing you to log task-specific messages. The logger is passed to the task function as an argument. The log file for each task is stored in the logs directory with the task name as the filename.

Best Practices

  • Use meaningful names for tasks and loggers to simplify debugging.
  • Avoid long-running tasks in cron jobs; delegate heavy processing to worker queues if needed.
  • Test tasks thoroughly to ensure they don’t interfere with application performance.
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/error-handling.html b/docs/.vitepress/dist/api/01-core/error-handling.html new file mode 100644 index 000000000..5e1838153 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/error-handling.html @@ -0,0 +1,105 @@ + + + + + + Error Handling | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Error Handling

Source: Express Error Handling

Error handling is a critical part of any robust application. This feature ensures that errors are properly caught, logged, and handled in a way that provides meaningful feedback to the client while maintaining the integrity of the server. Without proper error handling, unexpected issues could crash the server or expose sensitive information to clients.

This document explains the error-handling mechanisms implemented in this codebase, how they fit into the overall system, and how they help maintain clean, maintainable code.

Asynchronous Error Handler

Express (versions below 5) does not automatically catch errors thrown in asynchronous code. To address this, an asyncHandler middleware is used to wrap asynchronous route handlers. This middleware ensures that any errors are passed to the default error handler.

Why It Exists

Without this middleware, developers would need to manually wrap every asynchronous route handler in a try-catch block, leading to repetitive and error-prone code.

Usage

Wrap your asynchronous route handlers with asyncHandler to automatically catch errors and pass them to the error handler.

javascript
const asyncHandler = require('../middleware/asyncHandler');
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+    const user = await userService.findActiveUserBy(
+      'username', req.query.username
+    );
+    res.json(user);
+}));

This replaces the need for manual try-catch blocks:

javascript
router.get('/user', async (req, res, next) => {
+  try {
+    const user = await userService.findActiveUserBy('username', req.query.username);
+    res.json(user);
+  } catch (err) {
+    next(err);
+  }
+});

The Default Error Handler

The default error handler is the last middleware in the stack. It ensures that all unhandled errors are processed and a proper response is sent to the client.

Key Features

  • Sets res.statusCode based on err.status or defaults to 500.
  • Sends a generic error message in production or the stack trace in development.
  • Prevents sensitive information from being exposed to clients.

Custom Default Error Handler

The custom error handler (errorHandler) logs errors to the console and sends appropriate responses to clients:

  • For HTTP errors (e.g., createError(400, 'foo bar')), the client receives the message and status code.
  • For non-HTTP errors (e.g., new Error('business logic error')), a generic message is sent to the client.

Example

javascript
app.use(errorHandler);

Custom Error Handlers

Custom error handlers are used to handle specific types of errors before they reach the default error handler.

404 Handler

The notFound middleware catches requests to unknown routes and forwards a 404 error.

javascript
app.use(notFound);

Prisma Not Found Error Handler

Prisma's query engine may throw opaque errors when a resource is not found. The prismaNotFoundHandler middleware intercepts these errors and converts them into HTTP 404 responses.

Example

Before refactoring:

javascript
router.delete('/:username', asyncHandler(async (req, res, next) => {
+  try {
+    const deletedUser = await userService.softDeleteUser(req.params.username);
+    res.json(deletedUser);
+  } catch (e) {
+    if (e instanceof Prisma.PrismaClientKnownRequestError && e?.meta?.cause?.includes('not found')) {
+      return next(createError.NotFound());
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(prismaNotFoundHandler);
+
+router.delete('/:username', asyncHandler(async (req, res, next) => {
+  const deletedUser = await userService.softDeleteUser(req.params.username);
+  res.json(deletedUser);
+}));

Prisma Constraint Violation Handler

Handles database constraint violations (e.g., unique constraints) and sends appropriate HTTP responses.

Example

Before refactoring:

javascript
router.post('/', asyncHandler(async (req, res, next) => {
+  try {
+    const newUser = await userService.createUser(req.body);
+    res.json(newUser);
+  } catch (e) {
+    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+      return next(createError.Conflict('User already exists'));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(prismaConstraintFailedHandler);
+
+router.post('/', asyncHandler(async (req, res, next) => {
+  const newUser = await userService.createUser(req.body);
+  res.json(newUser);
+}));

Assertion Error Handler

Catches AssertionError instances and sends a 400 Bad Request response.

Example

Before refactoring:

javascript
const assert = require('assert');
+router.post('/', asyncHandler(async (req, res, next) => {
+  try {
+    assert(req.body.username, 'Username is required');
+    const newUser = await userService.createUser(req.body);
+    res.json(newUser);
+  } catch (e) {
+    if (e instanceof assert.AssertionError) {
+      return next(createError.BadRequest(e.message));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(assertionErrorHandler);
+
+router.post('/', asyncHandler(async (req, res, next) => {
+  assert(req.body.username, 'Username is required');
+  const newUser = await userService.createUser(req.body);
+  res.json(newUser);
+}));

Axios Error Handler

Handles errors from Axios HTTP requests, logs them, and sends a 500 Internal Server Error response.

Example

Before refactoring:

javascript
const axios = require('axios');
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+  try {
+    const response = await axios.get('https://api.example.com/user');
+    res.json(response.data);
+  } catch (e) {
+    if (e.response) {
+      return next(createError(e.response.status, e.response.statusText));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(axiosErrorHandler);
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+  const response = await axios.get('https://api.example.com/user');
+  res.json(response.data);
+}));

Integration into the System

Error-handling middleware is registered in app.js in the following order:

  1. notFound for unknown routes.
  2. Prisma-specific handlers (prismaNotFoundHandler, prismaConstraintFailedHandler).
  3. Other custom handlers (assertionErrorHandler, axiosErrorHandler).
  4. errorHandler as the final fallback.

This layered approach ensures that errors are handled at the appropriate level, keeping the codebase clean and maintainable.

Summary

By centralizing error handling, this system:

  • Reduces repetitive code.
  • Improves maintainability.
  • Ensures consistent error responses.
  • Protects sensitive information.

Follow the examples above to integrate error handling into your routes and middleware.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/events.html b/docs/.vitepress/dist/api/01-core/events.html new file mode 100644 index 000000000..93d0f8618 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/events.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/index.html b/docs/.vitepress/dist/api/01-core/index.html new file mode 100644 index 000000000..3b63247d0 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/index.html @@ -0,0 +1,26 @@ + + + + + + Core | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/lifecycle-hooks.html b/docs/.vitepress/dist/api/01-core/lifecycle-hooks.html new file mode 100644 index 000000000..79f55e215 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/lifecycle-hooks.html @@ -0,0 +1,34 @@ + + + + + + Lifecycle Hooks | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Lifecycle Hooks

The lifecycle hooks in this project are designed to manage specific tasks during the application's lifecycle. These hooks ensure that critical operations are performed at the right time, such as during startup, shutdown, or before forking worker processes.

Location of Lifecycle Hooks

The lifecycle hooks are implemented in the file located at:

src/core/lifecycle.js

Etiquette for Editing/Updating Lifecycle Hooks

  • Do not add your function body to lifecycle.js.
  • Instead, define new functions or logic in separate files/modules and call them from the respective lifecycle hook in lifecycle.js.
  • This approach ensures modularity, readability, and easier testing of individual components.

For example:

javascript
// Define your logic in a separate file
+async function customLogic() {
+  // ...custom logic...
+}
+
+// Call it in the lifecycle hook
+async function onApplicationBootstrap() {
+  await customLogic();
+}

Hooks Overview

beforeApplicationFork

  • Purpose: Executes tasks that need to run in the master process before forking worker processes.
  • Use Case: Perform one-time setup tasks such as generating Swagger documentation or registering cron jobs.
  • Error Handling: Errors in this hook are logged, and the process exits if critical tasks fail.

onApplicationBootstrap

  • Purpose: Executes tasks during the application bootstrap phase, typically after the application has started.
  • Use Case: Log warnings or perform checks based on configuration settings.
  • Error Handling: Errors in this hook are logged, and the process exits if critical tasks fail.

beforeApplicationShutdown

  • Purpose: Executes tasks in worker processes before the server shuts down.
  • Use Case: Perform cleanup tasks or prepare the application for shutdown.
  • Error Handling: Errors are logged, but the shutdown process continues.

onApplicationShutdown

  • Purpose: Executes tasks in worker processes after the server has shut down.
  • Use Case: Final cleanup or logging after the application has fully stopped.
  • Error Handling: Errors are logged, but the process exits regardless.

Integration

These hooks are used in the application lifecycle to ensure proper initialization and cleanup. For example:

  • beforeApplicationFork is called in the master process before forking workers.
  • onApplicationBootstrap is invoked during the startup phase.
  • beforeApplicationShutdown and onApplicationShutdown are used during the shutdown process to handle cleanup tasks.

By leveraging these hooks and following the guidelines for editing them, the application ensures a clean and predictable lifecycle management process.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/queue.html b/docs/.vitepress/dist/api/01-core/queue.html new file mode 100644 index 000000000..7c3031e23 --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/queue.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/01-core/validation.html b/docs/.vitepress/dist/api/01-core/validation.html new file mode 100644 index 000000000..8572b1dfd --- /dev/null +++ b/docs/.vitepress/dist/api/01-core/validation.html @@ -0,0 +1,120 @@ + + + + + + Request Validation | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Request Validation

Request validation ensures that incoming HTTP requests contain the expected data in the correct format. This feature uses express-validator to validate the request's query parameters, route parameters, or body content. It helps enforce data integrity and prevents invalid or malicious data from propagating through the system. express-validator wraps the extensive collection of validators and sanitizers offered by validator.js.

Without this validation layer, developers would need to write repetitive and error-prone checks in every route handler, leading to cluttered and less maintainable code. By centralizing validation logic, this feature promotes clean, declarative, and reusable code.

Benefits

  • Improved Code Quality: Reduces repetitive validation code in route handlers.
  • Error Handling: Centralizes validation error handling, making it easier to maintain.
  • Declarative Syntax: Encourages a declarative approach to validation, improving readability.
  • Scalability: Simplifies adding new routes with consistent validation logic.

Usage Instructions

To use the validation feature, follow these steps:

  1. Import the validate function and the necessary validation methods from express-validator.
  2. Define the validation rules for the request's body, query, or params.
  3. Wrap the validation rules with the validate function and include it as middleware in your route definition.
  4. Handle the request in the route handler, assuming the data is already validated.

see the example here.

Comparison with different approaches

Manual Validation

Manual validation involves writing custom logic directly in the route handler to check and sanitize incoming data. While this approach provides flexibility, it often leads to repetitive, error-prone code that can clutter route handlers and make them harder to maintain.

javascript
app.post(
+  '/user',
+  (req, res) => {
+    if (!req.body.username || !req.body.password) {
+      return res.status(400).json({ error: 'Username and password are required' });
+    }
+
+    if (!utils.isEmail(req.body.username)) {
+      return res.status(400).json({ error: 'Invalid email address' });
+    }
+
+    if (req.body.password.length < 5) {
+      return res.status(400).json({ error: 'Password must be at least 5 characters long' });
+    }
+
+    const age = parseInt(req.body.age, 10);
+    if (isNaN(age) || age < 18) {
+      return res.status(400).json({ error: 'Age must be a number greater than or equal to 18' });
+    }
+
+    User.create({
+      username: req.body.username,
+      password: req.body.password,
+    }).then(user => res.json(user));
+  },
+);

Using express-validator

By leveraging express-validator, you can define validation rules declaratively, reducing boilerplate code and improving readability.

javascript
app.post(
+  '/user',
+  body('username').isEmail(),
+  body('password').isLength({ min: 5 }),
+  body('age').isInt({ min: 18 }).toInt(),
+  (req, res) => {
+    // Finds the validation errors in this request and wraps them in an object with handy functions
+    const errors = validationResult(req);
+    if (!errors.isEmpty()) {
+      return res.status(400).json({ errors: errors.array() });
+    }
+
+    User.create({
+      username: req.body.username,
+      password: req.body.password,
+      age: req.body.age,
+    }).then(user => res.json(user));
+  },
+);

Using validate

The validate function further simplifies the use of express-validator by wrapping validation rules and handling errors automatically. It returns a 400 Bad Request response if validation fails, reducing the need for manual error handling in route handlers.

javascript
const { validate } = require('middleware/validators');
+const asyncHandler = require('middleware/asyncHandler');
+
+app.post(
+  '/user',
+  validate([
+    body('username').isEmail(),
+    body('password').isLength({ min: 5 }),
+    body('age').isInt({ min: 18 }).toInt()
+  ]),
+  asyncHandler(async (req, res) => {
+    const user = await User.create({
+      username: req.body.username,
+      password: req.body.password,
+      age: req.body.age,
+    });
+    res.json(user);
+  }),
+);

Explanation of the Code

  1. Validation Rules: The body('username').isEmail(), body('password').isLength({ min: 5 }), body('age').isInt({ min: 18 }).toInt() define the validation logic for the username, password,and age fields.
  2. validate Middleware: Wraps the validation and sanitization rules and handles errors automatically, returning a 400 Bad Request response if validation fails.
  3. Async Handler: The asyncHandler middleware ensures proper error handling for asynchronous operations in the route handler.

By using the validate function, you can focus on implementing business logic in your route handlers while ensuring that all incoming data is valid and secure. Also ensures that correct error responses are sent back to the client when validation fails.

More Examples

javascript
const { validate } = require('middleware/validators');
+const {
+  query, param, body, checkSchema,
+} = require('express-validator');
+
+
+validate([
+  // integer between 1 and 100
+  query('limit').isInt({ min: 1, max: 100 }).toInt(), 
+
+  // list of allowed values
+  query('type').isIn(['RAW_DATA', 'DATA_PRODUCT']).optional(), 
+  query('sort_order').default('desc').isIn(['asc', 'desc'])
+
+  // boolean value with default
+  // converts 'true' and 'false' strings to boolean
+  query('deleted').toBoolean().default(false),
+
+  // date in ISO8601 format
+  query('created_at_start').isISO8601()
+
+  // convert to BigInt
+  body('du_size').optional().notEmpty().customSanitizer(BigInt)
+
+  // Array & size limits
+  body('datasets').isArray({ min: 1, max: 100 }),
+
+  // validate objects in array
+  body('datasets.*.name').notEmpty(),
+
+  // String length
+  body('name').optional().isLength({ min: 5 }),
+
+]),
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/02-data/cache.html b/docs/.vitepress/dist/api/02-data/cache.html new file mode 100644 index 000000000..e563ee156 --- /dev/null +++ b/docs/.vitepress/dist/api/02-data/cache.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/02-data/index.html b/docs/.vitepress/dist/api/02-data/index.html new file mode 100644 index 000000000..02cdcc8b4 --- /dev/null +++ b/docs/.vitepress/dist/api/02-data/index.html @@ -0,0 +1,26 @@ + + + + + + Data | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/02-data/prisma.html b/docs/.vitepress/dist/api/02-data/prisma.html new file mode 100644 index 000000000..a46ba47d0 --- /dev/null +++ b/docs/.vitepress/dist/api/02-data/prisma.html @@ -0,0 +1,62 @@ + + + + + + ORM | Bioloop + + + + + + + + + + + + + + + +
Skip to content

ORM

Prisma.js is used as the ORM (Object-Relational Mapping) and schema migration tool in this project. It simplifies database interactions by providing a type-safe API and automates schema migrations, ensuring consistency across environments.

The DATABASE_URL environment variable is used to connect to the database. This is configured in the .env file.

  • Database Schema: Defined in prisma/schema.prisma.
  • Seeding: Handled by prisma/seed.js with additional scripts in the seed_data directory.
  • Migration Commands:
    • npx prisma migrate dev: Creates a new migration during development.
    • npx prisma migrate deploy: Applies migrations in production.
  • Seeding Command:
    • npx prisma db seed: Seeds the database with initial data.
  • Initialization: The db.js file contains the setup for the Prisma client.

Gotchas

Auto Timestamp

now() sets the current timestamp in the UTC time zone. Both createdAt and updatedAt fields are in UTC timezone. If the database is set to a different timezone, you may see future timestamps when connected to the db using a tool like dbeaver.

This is not a problem when using prisma queries, as prisma automatically converts the timestamps to the local timezone.

prisma
model Post {
+  id               Int                @id @default(autoincrement())
+  title            String
+  content          String?
+  published        Boolean?           @default(false)
+  createdAt        DateTime           @default(now()) @db.Timestamp(6)
+  updatedAt        DateTime           @updatedAt @default(now()) @db.Timestamp(6)
+}
  • Important Note: When performing raw SQL queries, avoid using createdAt < now(). Instead, use createdAt < timezone('utc', now()) to ensure consistent time zone handling.

Aliasing and Excluding Columns

Prisma does not support aliasing columns (like SQL's AS keyword) or excluding specific columns directly. Instead, you can transform the returned objects in JavaScript.

Example:

javascript
const _ = require('lodash');
+
+function transformUser(user) {
+  return _(user)
+    .set('roles', user.user_role.map((obj) => obj.roles.name)) // Transform and set new keys
+    .omit(['password', 'id', 'user_role']) // Remove unwanted keys
+    .value();
+}

For more details, refer to:

Avoid SELECT * Queries

Fetching all columns can negatively impact performance due to deserialization overhead, increased network transmission, and inefficiencies with non-inline columns (e.g., blobs, JSON).

Bad Example:

javascript
const users = await prisma.user.findMany();

Good Example:

javascript
const users = await prisma.user.findMany({
+  select: {
+    id: true,
+    name: true,
+    email: true,
+  },
+});

For more information, see this source.

Sorting with nulls last

Prisma's sorting behavior depends on whether a column is nullable. For nullable fields, you must explicitly specify nulls: 'last' or nulls: 'first'. For non-nullable fields, including this option will throw an error.

Example:

javascript
const buildOrderByObject = (field, sortOrder, nullsLast = true) => {
+  const nullable_order_by_fields = ['du_size', 'size'];
+
+  if (!field || !sortOrder) {
+    return {};
+  }
+  if (nullable_order_by_fields.includes(field)) {
+    return {
+      [field]: { sort: sortOrder, nulls: nullsLast ? 'last' : 'first' },
+    };
+  }
+  return {
+    [field]: sortOrder,
+  };
+};

Logging Queries

Enable logging to debug and monitor Prisma queries:

javascript
const prisma = new PrismaClient({
+  log: ['query', 'info', 'warn', 'error'],
+});

This will log all queries, warnings, and errors generated by the Prisma client.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/03-security/authentication.html b/docs/.vitepress/dist/api/03-security/authentication.html new file mode 100644 index 000000000..4d8c25bfc --- /dev/null +++ b/docs/.vitepress/dist/api/03-security/authentication.html @@ -0,0 +1,31 @@ + + + + + + Authentication | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Authentication

The API uses IU CAS authnetication model.

All the routes and sub-routers added after the authenticate middleware in index router require authentication. The routes that do not require authentication such as auth routes are added before this.

The authenticate middleware, parses the Authorization header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as req.user

To add authentication to a single route:

javascript
const { authenticate } = require('../middleware/auth');
+
+router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => {
+  const user = await userService.findActiveUserBy('username', req.user.username);
+  // ...
+}))
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/03-security/authorization.html b/docs/.vitepress/dist/api/03-security/authorization.html new file mode 100644 index 000000000..c1d8038c7 --- /dev/null +++ b/docs/.vitepress/dist/api/03-security/authorization.html @@ -0,0 +1,82 @@ + + + + + + Authorization | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Authorization

Role Based Access Control

accesscontrol library is used to provide role based authorization to routes (resources).

Roles in this application:

  • user
  • operator
  • admin
  • superadmin

Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in services/accesscontrols.js.

The goal of the accessControl middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource.

A simple use case:

Objective: Users with user role are only permitted to read and update thier own profile. Whereas, users with admin role can create new users, read & update any user's profile, and delete any user.

Role design:

  • roles: admin, user
  • actions: CRUD
  • resource: user
javascript
{
+  admin: {
+    user: {
+      'create:any': ['*'],
+      'read:any': ['*'],
+      'update:any': ['*'],
+      'delete:any': ['*'],
+    },
+  },
+  user: {
+    user: {
+      'read:own': ['*'],
+      'update:own': ['*'],
+    },
+  },
+}

Permission check:

Code to check if the requester to is authorized to GET /users/dduck. This route is protected by authenticate middleware which attaches the requester profile to req.user if the token is valid.

javascript
const { authenticate } = require('../middleware/auth');
+
+router.get('/:username',
+  authenticate,
+  asyncHandler(async (req, res, next) => {
+    
+    const roles = req.user.roles;
+    const resourceOwner = req.params.username;
+    const requester = req.user?.username;
+
+    const permission = (requester === resourceOwner)
+        ? ac.can(roles).readOwn('user')
+        : ac.can(roles).readAny('user');
+    
+    if (!permission.granted) {
+      return next(createError(403)); // Forbidden
+    }
+    else {
+      const user = await userService.findActiveUserBy('username', req.params.username);
+      if (user) { return res.json(user); }
+      return next(createError.NotFound());
+    }
+  }),
+);

readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only user role and is requesting the profile of other users, the request will be denied.

AccessControl Middleware Usage

accessControl middleware is a generic function to handle authorization for any action or resource with optional ownership checking.

The above code can be written consicely with the help of accessControl middleware.

routes/*.js

javascript
// import middleware
+const { authenticate, accessControl } = require('../middleware/auth');
+
+// configre the middleware to authorize requests to user resource
+// resource ownership is checked by default
+// throws 403 if not authorized
+const isPermittedTo = accessControl('user');
+
+//
+router.get(
+  '/:username',
+  authenticate,
+  isPermittedTo('read', { checkOwnerShip: true }),
+  asyncHandler(async (req, res, next) => {
+    const user = await userService.findActiveUserBy('username', req.params.username);
+    if (user) { return res.json(user); }
+    return next(createError.NotFound());
+  }),
+);
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/03-security/cookies.html b/docs/.vitepress/dist/api/03-security/cookies.html new file mode 100644 index 000000000..2569d456f --- /dev/null +++ b/docs/.vitepress/dist/api/03-security/cookies.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/03-security/cors.html b/docs/.vitepress/dist/api/03-security/cors.html new file mode 100644 index 000000000..f105978a6 --- /dev/null +++ b/docs/.vitepress/dist/api/03-security/cors.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/03-security/index.html b/docs/.vitepress/dist/api/03-security/index.html new file mode 100644 index 000000000..ce9b556f0 --- /dev/null +++ b/docs/.vitepress/dist/api/03-security/index.html @@ -0,0 +1,26 @@ + + + + + + Security | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/04-deployment/docker-image-design.html b/docs/.vitepress/dist/api/04-deployment/docker-image-design.html new file mode 100644 index 000000000..92d823cb3 --- /dev/null +++ b/docs/.vitepress/dist/api/04-deployment/docker-image-design.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/04-deployment/index.html b/docs/.vitepress/dist/api/04-deployment/index.html new file mode 100644 index 000000000..f01e33608 --- /dev/null +++ b/docs/.vitepress/dist/api/04-deployment/index.html @@ -0,0 +1,26 @@ + + + + + + Deployment | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/05-performance/compression.html b/docs/.vitepress/dist/api/05-performance/compression.html new file mode 100644 index 000000000..d50885c79 --- /dev/null +++ b/docs/.vitepress/dist/api/05-performance/compression.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/05-performance/http-caching.html b/docs/.vitepress/dist/api/05-performance/http-caching.html new file mode 100644 index 000000000..3798d4f11 --- /dev/null +++ b/docs/.vitepress/dist/api/05-performance/http-caching.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/05-performance/index.html b/docs/.vitepress/dist/api/05-performance/index.html new file mode 100644 index 000000000..77b555b45 --- /dev/null +++ b/docs/.vitepress/dist/api/05-performance/index.html @@ -0,0 +1,26 @@ + + + + + + Performance | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/05-performance/instrumentation.html b/docs/.vitepress/dist/api/05-performance/instrumentation.html new file mode 100644 index 000000000..6245b143c --- /dev/null +++ b/docs/.vitepress/dist/api/05-performance/instrumentation.html @@ -0,0 +1,50 @@ + + + + + + Instrumentation | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Instrumentation

Instrumentation is the process of collecting and storing data about the performance of your application. This data can be used to identify performance bottlenecks, monitor the health of your application, and make informed decisions about how to improve performance.

prom-client is a popular library for instrumenting Node.js applications with Prometheus metrics. It provides a simple and efficient way to collect metrics and expose them for monitoring and alerting.

Metrics Middleware

The metricsMiddleware is a middleware function provided by the express-prom-bundle library. It is responsible for collecting HTTP request metrics, such as response times, status codes, and request paths. These metrics are exposed in a format compatible with Prometheus, enabling easy integration with monitoring systems.

Configuration

The metricsMiddleware is configured in the core/metrics.js file. Key configurations include:

  • Autoregister: Automatically registers metrics unless clustering is enabled.
  • Include Method: Captures the HTTP method (e.g., GET, POST).
  • Include Path: Captures the request path.
  • Normalize Path: Normalizes paths to avoid high cardinality (e.g., /users/:id instead of /users/123).
  • Buckets: Defines histogram buckets for response times, ranging from 30ms to 30s.

Metrics Collected

The middleware collects the following metrics:

  • HTTP Request Duration: Measures the time taken to process requests in a histogram format.
  • HTTP Status Codes: Aggregates status codes into categories (e.g., 2xx, 4xx).
  • Request Paths: Tracks metrics per normalized path.
plaintext
# HELP http_request_duration_seconds duration histogram of http responses labeled with: status_code, method, path
+# TYPE http_request_duration_seconds histogram
+http_request_duration_seconds_bucket{le="0.03",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="0.1",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="0.3",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="1",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="3",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="10",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="30",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="+Inf",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_sum{status_code="2xx",method="GET",path="/datasets/"} 0.038533292
+http_request_duration_seconds_count{status_code="2xx",method="GET",path="/datasets/"} 2

Default metrics about node.js process are also collected. To view all metrics:

bash
api> curl locahost:9999/metrics > metrics.prom

Usage

The middleware is added to the Express application in app.js:

javascript
const { metricsMiddleware } = require('./core/metrics');
+app.use(metricsMiddleware);

This ensures that all incoming requests are automatically instrumented.

Custom Metrics

core/metrics.js

javascript
const authFailures = new client.Counter({
+  name: 'auth_failures_total',
+  help: 'Total number of failed authentication attempts',
+  labelNames: ['auth_method', 'reason', 'client_id'],
+});

Add your custom metric to the metrics.js file. This example creates a counter metric to track the total number of failed authentication attempts. You can define custom labels to provide additional context for the metric. This metric will be automatically registered and sent to the Prometheus server on every scrape.

Include the Metric in Your Code

services/authService.js

javascript
const metrics = require('../core/metrics');
+
+function authenticateUser(username, password) {
+  if (!isValidCredentials(username, password)) {
+    metrics.authFailures.inc({ auth_method: 'password', reason: 'invalid_credentials', client_id: 'web' });
+    throw new Error('Invalid credentials');
+  }
+}

Clustered Metrics Aggregation

In a clustered environment, metrics from all worker processes are aggregated in the master process. This ensures that metrics are consistent and accessible from a single endpoint.

Implementation

The aggregation is implemented in cluster.js:

  • Master Process: Exposes the /metrics endpoint on a separate port for Prometheus to scrape aggregated metrics.
  • Worker Processes: Collect metrics locally and send them to the master process.

Example configuration in cluster.js:

javascript
const promBundle = require('express-prom-bundle');
+metricsApp.use('/metrics', promBundle.clusterMetrics());

Without this setup, metrics would be fragmented across worker processes, making it difficult to monitor the entire system.

Benefits

  • Centralized metrics collection in clustered environments.
  • Simplifies monitoring and alerting.
  • Ensures accurate and consistent metrics across all processes.
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/05-performance/nodejs_metrics.html b/docs/.vitepress/dist/api/05-performance/nodejs_metrics.html new file mode 100644 index 000000000..8272e5866 --- /dev/null +++ b/docs/.vitepress/dist/api/05-performance/nodejs_metrics.html @@ -0,0 +1,26 @@ + + + + + + Node.js Metrics Documentation | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Node.js Metrics Documentation

This document provides an overview of the performance metrics collected by the application.

nodejs_gc_duration_seconds

Type: Histogram
Source: Derived from perf_hooks.PerformanceObserver (Node.js Documentation)

Description:
Measures the duration of garbage collection (GC) events, categorized by type: major, minor, incremental, or weakcb.


nodejs_active_resources

Type: Gauge
Source: Derived from process.getActiveResourcesInfo()

Description:
Tracks the number of active resources currently keeping the event loop alive.

  • nodejs_active_resources: Count of unique active resources.
  • nodejs_active_resources_total: Total count of all active resources.

Usage:

  • Monitor the number of active resources to identify potential event loop bottlenecks.
  • Set thresholds to alert when the number of active resources exceeds a defined limit.

Deprecated Metrics: nodejs_active_requests and nodejs_active_handles

Status: Deprecated in Node.js v23 (Deprecation Notice)
Replacement: Use nodejs_active_resources and nodejs_active_resources_total.


process_start_time_seconds

Type: Gauge
Source: Derived from process.uptime()

Description:
Tracks the number of seconds since the Node.js process started.


process_open_fds

Type: Gauge
Source: Derived from fs.readdirSync('/proc/self/fd').length - 1

Description:
Monitors the number of open file descriptors used by the Node.js process.

Usage:

  • Identify potential file descriptor leaks or excessive file usage.
  • Compare with process_max_fds to ensure the process is not nearing the OS limit.

Caveats:

  • readdirSync is a synchronous operation and may block the event loop if there are many file descriptors.
  • /proc/self/fd is in-memory (procfs), so it is faster than disk I/O but still incurs system call overhead.

process_max_fds

Type: Gauge
Source: Derived from /proc/self/limits

Description:
Represents the maximum number of file descriptors the process can open, as configured by the OS.

Usage:

  • Compare process_open_fds with process_max_fds to detect if the process is nearing the limit.
  • Helps identify potential file descriptor management issues.

process_cpu_seconds_total

Type: Counter
Source: Derived from @opentelemetry/api

Description:
Tracks the total CPU time (user + system) consumed by the process.
TODO: Add implementation details.


osMemoryHeapLinux

Type: Gauge
Source: Derived from /proc/self/status

Metrics:

  • process_resident_memory_bytes (VmRSS): Physical memory in use. (real footprint in RAM).
  • process_virtual_memory_bytes (VmSize): Total virtual memory allocated (includes swapped-out and unused portions).
  • nodejs_heap_size_total_bytes (VmData): Heap memory size.

Usage:

  • Monitor memory usage to detect potential memory leaks or excessive memory consumption.

heapSpacesSizeAndUsed

Type: Gauge
Source: Derived from v8.getHeapSpaceStatistics()

Metrics:

  • nodejs_heap_space_size_total_bytes: Total size of each heap space.
  • nodejs_heap_space_size_used_bytes: Used size of each heap space.
  • nodejs_heap_space_size_available_bytes: Available size in each heap space.

Heap Spaces:

  • new_space: The new space is where new objects are allocated. It is a semi-space garbage collector.
  • old_space: The old space is where long-lived objects are allocated. It is a mark-sweep garbage collector.
  • code_space: The code space is where compiled code is stored.
  • map_space: The map space is where object property maps are stored.
  • large_object_space: The large object space is where large objects are allocated.

Usage:

  • Monitor memory usage per heap space to identify memory bottlenecks or leaks.
  • Focus on new_space for early detection of memory issues.

heapSizeAndUsed

Type: Gauge
Source: Derived from process.memoryUsage()

Metrics:

  • nodejs_heap_size_total_bytes: The total heap size allocated for the Node.js (V8) process.
  • nodejs_heap_size_used_bytes: The amount of heap memory used by the Node.js (V8) process.
  • nodejs_external_memory_bytes: refers to the memory usage of C++ objects bound to JavaScript objects managed by V8.

Usage:

  • Monitor overall heap usage to detect memory leaks or excessive memory consumption.
  • Compare with heapSpacesSizeAndUsed for detailed heap space analysis.

Caveats:

  • process.memoryUsage() iterates over memory pages, which may block the event loop for large heaps.

Here's a refined and professional version of your documentation:


eventLoopLag

Type: Gauge

Metrics

  • nodejs_eventloop_lag_seconds
  • nodejs_eventloop_lag_min_seconds
  • nodejs_eventloop_lag_max_seconds
  • nodejs_eventloop_lag_mean_seconds
  • nodejs_eventloop_lag_stddev_seconds
  • nodejs_eventloop_lag_p50_seconds
  • nodejs_eventloop_lag_p90_seconds
  • nodejs_eventloop_lag_p95_seconds

Description

eventLoopLag measures the delay between scheduling a timer (setImmediate) and its corresponding callback execution. This provides insight into how long the event loop is blocked by other operations, serving as an indicator of potential performance bottlenecks in a Node.js application.

Computation of nodejs_eventloop_lag_seconds

The nodejs_eventloop_lag_seconds metric is computed as follows (reference):

  1. A Gauge is created in eventLoopLag.js with a custom collect method.
  2. When metrics are requested, the collect method is invoked, capturing a high-resolution timestamp. This occurs at the start of the metrics collection process.
  3. The method then schedules another measurement using setImmediate. Since this executes in the next event loop iteration, it occurs after metrics collection and reporting have completed.
  4. As the collect method does not return a promise, the computed value is only recorded in the subsequent metrics collection cycle.

This means the metric measures the delay from the start of metrics collection until the next event loop iteration. However, it is not updated in real time and only reflects values from the previous collection cycle.

Computation of Other Metrics

Other event loop lag metrics (e.g., min, max, mean, percentiles) are derived from perf_hooks.monitorEventLoopDelay().

  • The minimum measurable delay depends on the timer resolution, which defaults to 10ms but can be adjusted using the eventLoopMonitoringPrecision configuration option.

Use Cases

  • Performance Monitoring: Helps detect high event loop lag, which may indicate that the application is being blocked by long-running operations.
  • Bottleneck Identification: A consistently high event loop lag suggests potential issues such as synchronous operations blocking the event loop.

Limitations

  • Delayed Updates:
    • nodejs_eventloop_lag_seconds is only updated during metrics collection. The reported value reflects the lag from the previous cycle, not the current one.
    • In high-throughput applications (e.g., Express-based servers), event loop lag typically increases in short bursts (e.g., during sudden spikes in requests). If metrics are collected every 30 seconds, the reported value may not accurately represent real-time lag.
  • Resolution Constraints:
    • Metrics derived from perf_hooks.monitorEventLoopDelay() have a 10ms resolution by default. While this provides continuous monitoring, it may not capture sub-millisecond variations in event loop lag.
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/06-integrations/api-clients.html b/docs/.vitepress/dist/api/06-integrations/api-clients.html new file mode 100644 index 000000000..463f9f010 --- /dev/null +++ b/docs/.vitepress/dist/api/06-integrations/api-clients.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/06-integrations/index.html b/docs/.vitepress/dist/api/06-integrations/index.html new file mode 100644 index 000000000..92f384379 --- /dev/null +++ b/docs/.vitepress/dist/api/06-integrations/index.html @@ -0,0 +1,26 @@ + + + + + + Integrations | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/06-integrations/swagger-openapi.html b/docs/.vitepress/dist/api/06-integrations/swagger-openapi.html new file mode 100644 index 000000000..bd334e529 --- /dev/null +++ b/docs/.vitepress/dist/api/06-integrations/swagger-openapi.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content

OpenAPI Documentation

Auto-generated OpenAPI documentation for the API routes.

  1. Add // #swagger.tags = ['<sub-router>'] comment to the code of the route handler and replace sub-router with a valid name that describes the family of routes (ex: User, Dataset, etc).
  2. Run npm run swagger-autogen to generate the documentation.
  3. Visit http://<api-host>:<api-port>/docs

Files:

  • swagger.js - script that generates the documentation. Configures the output file, the router to generate routes and other common config.
  • swagger_output.json - generated routes, not included in the version control.

Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/07-development/auto-reload-server.html b/docs/.vitepress/dist/api/07-development/auto-reload-server.html new file mode 100644 index 000000000..6cb12e8c1 --- /dev/null +++ b/docs/.vitepress/dist/api/07-development/auto-reload-server.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/07-development/db-seed-data.html b/docs/.vitepress/dist/api/07-development/db-seed-data.html new file mode 100644 index 000000000..a094684f9 --- /dev/null +++ b/docs/.vitepress/dist/api/07-development/db-seed-data.html @@ -0,0 +1,26 @@ + + + + + + Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/07-development/index.html b/docs/.vitepress/dist/api/07-development/index.html new file mode 100644 index 000000000..4ae763838 --- /dev/null +++ b/docs/.vitepress/dist/api/07-development/index.html @@ -0,0 +1,26 @@ + + + + + + Development | Bioloop + + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/07-development/linting.html b/docs/.vitepress/dist/api/07-development/linting.html new file mode 100644 index 000000000..9f4f5793e --- /dev/null +++ b/docs/.vitepress/dist/api/07-development/linting.html @@ -0,0 +1,26 @@ + + + + + + Linting | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Linting

Eslint rules inherited from eslint-config-airbnb-base and Lodash-fp ruleset.

Consistent Coding styles with editorconfig

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/index.html b/docs/.vitepress/dist/api/index.html index 5b7bdafaa..2091a9fab 100644 --- a/docs/.vitepress/dist/api/index.html +++ b/docs/.vitepress/dist/api/index.html @@ -5,150 +5,22 @@ API | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

API

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

Running using docker

In the developement environment with docker this is the content of .env file

bash
NODE_ENV=docker
-DATABASE_PASSWORD='example'
-DATABASE_URL="postgresql://appuser:example@postgres:5432/app?schema=public"

From the project root run: docker compose up postgres api -d to start both the API server and the database

Running on host machine

Start a postgres db server on localhost and create a database app and user appuser (password: example) with write premissions to public schema. In this local environment, the content of .env file is:

bash
NODE_ENV=default
-DATABASE_PASSWORD='example'
-DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"

Run pnpm install and pnpm start to start the API server.

Features:

Developer Exprience

  • Auto reload: nodemon index.js
  • Linting
    • auto highligt linting errors
    • format on save
  • Consistent Coding styles with editorconfig

Production deployment:

  • pm2
  • docker

Assumptions:

  • there is a reverse proxy which handles security headers as we are not using helmet module.

Typical request flow through the Express Server

  1. Express creates a request object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
  2. app.js - The body, query parameters, and cookies are parsed and converted to objects and req object is updated.
  3. app.js - CORS?
  4. Index Router - Intial routing is performed to select a sub-router to send the request to.
  5. Authentication - Validate JWT and attach user profile to req.user or send 401 error response*.
  6. AceessControl - Determine whether the requester has enough permissions to perform the desired operation on a particular resource and attach the permission object to req.permission or send 403 error response.
  7. Request Validation - Validate if the request query, params, or the body is in expected format or send 400 error.
  8. Async Handler - Envolpe the business logic route middleware to catch async error and propagate them to global error handler.
  9. Route Handler - Business Logic - create and send the response.
  10. Compression - Apply gZip compression to the response body.
  11. Express server sets some default headers and sends the response to the client.

When something goes wrong

  1. 404 Handler - Handle routing failures and send 404 error response.
  2. Prisma Not Found handler - Handle not found prisma errors and send 404 error response.
  3. Global error handler - Handle all other errors and send 500 error response.
  4. Compression - Apply gZip compression to the response body.
  5. Express server sets some default headers and sends the response to the client.

* For the routes that are registered before authenticate such as /health and /auth, this middleware not invoked.

Project Structure

files in src/

  • index.js - import app and start
  • app.js - create and configure express application
  • routes/index.js - main router
  • routes/*.js - modular routes
  • middleware/*.js - express middleware functions
  • services/*.js - common code specific to this project seperated by usage in router
  • services/logger.js - winston logger
  • config/*.json - hierarchical configuration
  • prisma/schema.prisma - Data definitions
  • prisma/seed.js - code to initialize tables with some data
  • utils/index.js - non-specific common code

Error Handling

Source: Express Error Handling

  • Express automatically handles errors thrown in the synchronous code, however it cannot catch errors thrown from asynchronous code (in versions below 5). These have to be caught and passed on to the next function.
  • The error thrown from the sync code in the middleware are handled and passed on to the next automatically.
  • When next is called with any argument except 'route', express assumes it is due to an error and skips any remaining non-error handling routing and middleware functions.

Asynchronous Error Handler

asyncMiddleware in middleware/error.js

Usage: Wrap the route handler middleware with asyncHandler to produce a middleware funtion that can catch the asynchronous error and pass on to the default error handler.

javascript
const asyncHandler = require('../middleware/asyncHandler');
-
-router.get('/user', asyncHandler(async (req, res, next) => {
-    const user = await userService.findActiveUserBy('username', req.query.username);
-    res.json(user)
-}))

instead of

javascript
router.get('/user', async (req, res, next) => {
-  try {
-    const user = await userService.findActiveUserBy('username', req.query.username);
-    res.json(user)
-  } catch(err) {
-    next(err)
-  }
-})

The Default Error Handler

  • The default error handler is added at the end of the middleware function stack
  • The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
  • The res.statusMessage is set according to the status code.
  • The body will be the HTML of the status code message when in production environment, otherwise will be err.stack. (environment variable NODE_ENV=production)

Custom Default Error Handler

  • errorHandler in middleware/error.js
  • Logs error to console
  • send actual message to client only if err.expose is true otherwise send a generic Internal server error. For http errors such as (throw createError(400, 'foo bar')), the client receives {"message":"foo bar"} with status code to 400.
  • For non http errors such as throw new Error('business logic error'), only the err.message is set others are not. For such error, this handler will send a generic message. Client's will not see business logic error in thier response object.
  • Does not log to console stack trace for 4xx errors

404 handler

http-errors module:

  • Helps to create http specific error objects which can be thrown or passed to next
javascript
err = createError(404, 'user not found')
-return next(err)
javascript
return next(createError.NotFound())

this will automatically set correct error message based on the constructor.

  • Create an error with expose being true: createError(502, 'foo', { expose: true })

Prisma Not Found Error Handler

Prisma returns opaque error objects from the underlying query engine when DB queries fail. One such common error that must be handled everytime a DB query is made is the Not Found error.

HTTP semantics require that if a resource cannot be found either while retrieving, updating or deleting the response should be sent 404 status code. In order to achieve this, the errors from the prisma code have to be caught and analysed for the Not Found errors.

A typical example of handling a not found error and returning 404 response:

javascript
router.delete(
-  '/:username',
-  asyncHandler(async (req, res, next) => {
-    try{
-      const deletedUser = await userService.softDeleteUser(req.params.username);
-      res.json(deletedUser);
-    } catch(e) {
-      if (e instanceof Prisma.PrismaClientKnownRequestError) {
-        if (e?.meta?.cause?.includes('not found')) {
-          return next(createError.NotFound());
-        }
-      }
-      return next(e);
-    }
-  }),
-);

Here, the errors other than not found are propagated to the error handler by next(e).

The try-catch code can be refactored to a middleware that intercepts all errors before the Custom default error handler. The above code after refactoring looks like:

app.js

javascript
const { prismaNotFoundHandler } = require('./middleware/error');
-app.use(prismaNotFoundHandler);

routes/*.js

javascript
router.delete(
-  '/:username',
-  asyncHandler(async (req, res, next) => {
-    const deletedUser = await userService.softDeleteUser(req.params.username);
-    res.json(deletedUser);
-  }),
-);

Now, the routes' business logic code is cleaner and 404s are automatically when prisma ORM throws not found errors. If you want to send other HTTP status codes, intercept the prisma error in your route handler without propagating it.

Linting

Eslint rules inherited from eslint-config-airbnb-base and Lodash-fp ruleset.

Config

Uses config module - https://github.com/node-config/node-config

Configurations are stored in configuration files within your application, and can be overridden and extended by environment variables, command line parameters, or external sources.

config files: default.json, production.json, custom-environment-variables.json in ./config/ directory.

precdence of config: command line > environment > {NODE_ENV}.json > default.json

The properties to read and override from environment is defined in custom-environment-variables.json

Loading environment variables

javascript
require('dotenv-safe').config();

Authentication

The API uses IU CAS authnetication model.

All the routes and sub-routers added after the authenticate middleware in index router require authentication. The routes that do not require authentication such as auth routes are added before this.

The authenticate middleware, parses the Authorization header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as req.user

To add authentication to a single route:

javascript
const { authenticate } = require('../middleware/auth');
-
-router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => {
-  const user = await userService.findActiveUserBy('username', req.user.username);
-  // ...
-}))

Request Validation

Uses express-validator to validate if the request query, params, or the body is of the expected format and has acceptable values. This module helps to write declarative code that reduces repeatitive Spaghetti safety checking code inside the route handler. The route can now confidently presume that all of the required properties/keys of req.params, req.query, or req.body exist and have appropriate values and optional keys set to default values.

Using the validate higher order function, the error checking code is factored out from the route specific middleware functions.

javascript
app.post(
-  '/user',
-  body('username').isEmail(),
-  body('password').isLength({ min: 5 }),
-  (req, res) => {
-    // Finds the validation errors in this request and wraps them in an object with handy functions
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    User.create({
-      username: req.body.username,
-      password: req.body.password,
-    }).then(user => res.json(user));
-  },
-);

becomes

javascript
const validate = require('middleware/validators')
-app.post(
-  '/user',
-  validate([
-    body('username').isEmail(),
-    body('password').isLength({ min: 5 }),
-  ]),
-  asyncHandler(async (req, res) => {
-    const user = await User.create({
-      username: req.body.username,
-      password: req.body.password,
-    });
-    res.json(user);
-  },
-));

Authorization: Role Based Access Control

accesscontrol library is used to provide role based authorization to routes (resources).

Roles in this application:

  • user
  • operator
  • admin
  • superadmin

Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in services/accesscontrols.js.

The goal of the accessControl middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource.

A simple use case:

Objective: Users with user role are only permitted to read and update thier own profile. Whereas, users with admin role can create new users, read & update any user's profile, and delete any user.

Role design:

  • roles: admin, user
  • actions: CRUD
  • resource: user
javascript
{
-  admin: {
-    user: {
-      'create:any': ['*'],
-      'read:any': ['*'],
-      'update:any': ['*'],
-      'delete:any': ['*'],
-    },
-  },
-  user: {
-    user: {
-      'read:own': ['*'],
-      'update:own': ['*'],
-    },
-  },
-}

Permission check:

Code to check if the requester to is authorized to GET /users/dduck. This route is protected by authenticate middleware which attaches the requester profile to req.user if the token is valid.

javascript
const { authenticate } = require('../middleware/auth');
-
-router.get('/:username',
-  authenticate,
-  asyncHandler(async (req, res, next) => {
-    
-    const roles = req.user.roles;
-    const resourceOwner = req.params.username;
-    const requester = req.user?.username;
-
-    const permission = (requester === resourceOwner)
-        ? ac.can(roles).readOwn('user')
-        : ac.can(roles).readAny('user');
-    
-    if (!permission.granted) {
-      return next(createError(403)); // Forbidden
-    }
-    else {
-      const user = await userService.findActiveUserBy('username', req.params.username);
-      if (user) { return res.json(user); }
-      return next(createError.NotFound());
-    }
-  }),
-);

readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only user role and is requesting the profile of other users, the request will be denied.

AccessControl Middleware Usage

accessControl middleware is a generic function to handle authorization for any action or resource with optional ownership checking.

The above code can be written consicely with the help of accessControl middleware.

routes/*.js

javascript
// import middleware
-const { authenticate, accessControl } = require('../middleware/auth');
-
-// configre the middleware to authorize requests to user resource
-// resource ownership is checked by default
-// throws 403 if not authorized
-const isPermittedTo = accessControl('user');
-
-//
-router.get(
-  '/:username',
-  authenticate,
-  isPermittedTo('read', { checkOwnerShip: true }),
-  asyncHandler(async (req, res, next) => {
-    const user = await userService.findActiveUserBy('username', req.params.username);
-    if (user) { return res.json(user); }
-    return next(createError.NotFound());
-  }),
-);

Auto-generated Swagger Documentation

  1. Add // #swagger.tags = ['<sub-router>'] comment to the code of the route handler and replace sub-router with a valid name that describes the family of routes (ex: User, Dataset, etc).
  2. Run npm run swagger-autogen to generate the documentation.
  3. Visit http://<api-host>:<api-port>/docs

Files:

  • swagger.js - script that generates the documentation. Configures the output file, the router to generate routes and other common config.
  • swagger_output.json - generated routes, not included in the version control.

Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284

- +
Skip to content
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/api/introduction.html b/docs/.vitepress/dist/api/introduction.html new file mode 100644 index 000000000..366cbefb3 --- /dev/null +++ b/docs/.vitepress/dist/api/introduction.html @@ -0,0 +1,26 @@ + + + + + + Introduction | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Introduction

Developing robust and maintainable backend services requires a structured approach without unnecessary complexity. This framework extends Express.js by providing a set of utility functions that facilitate the development of production-ready applications. No additional abstractions have been introduced, ensuring that developers familiar with Express can use it without a learning curve. Essential features such as validation, authentication, logging, and metrics are integrated, offering a streamlined development experience. A fully configured development environment can be initialized with a single command, incorporating linting, hot reloading, and best-practice defaults.

Philosophy

Minimal Abstractions

Rather than introducing new layers of abstraction, the framework enhances Express through structured utility functions. Full control over request handling is maintained while offering built-in tools for middleware management, configuration, and error handling.

Comprehensive Feature Set

A variety of essential features required for modern web applications, including authentication, logging, and observability, have been integrated. This approach allows teams to focus on application logic rather than spending time configuring third-party packages.

Adherence to Best Practices

The framework is structured to promote modular and maintainable code. The use of Prisma for database management, OpenAPI documentation, and structured logging ensures consistency and maintainability across projects.

Optimized Developer Experience

A complete development environment can be set up using a single command through Docker Compose. Automated linting, nodemon-based reloads, and process clustering have been included to improve efficiency and reduce development overhead.

By emphasizing simplicity, flexibility, and adherence to best practices, this framework enables the development of scalable and maintainable Express applications while minimizing complexity.

Example

routes.index.js Router Explained

routes/resources.js Middleware Explained

Typical Request Flow Through the Express Server

StepComponentDescription
1Express ServerReceives an HTTP request
2MiddlewareParses request body, query, and cookies
3MiddlewareEnforces CORS policies
4RouterRoutes request to appropriate sub-router
5AuthenticationValidates JWT, attaches user to request
6Access ControlChecks permissions
7ValidationEnsures request format is correct
8Async Error HandlerWraps route handlers for error management
9Business LogicExecutes request-specific logic
10MiddlewareApplies gzip compression
11Express ServerSends response to client

Error Handling Steps:

StepComponentDescription
10404Handle routing failures
11Custom Error HandlersHandle specific errors
12Global Error HandlerHandle all other errors
13MiddlewareApplies gzip compression
14Express ServerSends response to client

Detailed Steps:

  1. Express creates a request object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
  2. src/app.js - The body, query parameters, and cookies are parsed and converted to objects, and the req object is updated.
  3. src/app.js - Apply CORS policies to handle cross-origin requests.
  4. Main Router (src/routers/index.js) - Initial routing is performed to select a sub-router to send the request to.
  5. Authentication - Validate JWT and attach the user profile to req.user or send a 401 error response*.
  6. Access Control - Determine whether the requester has sufficient permissions to perform the desired operation on a particular resource. Attach the permission object to req.permission or send a 403 error response.
  7. Request Validation - Validate if the request query, parameters, or body is in the expected format, or send a 400 error response.
  8. Async Handler - Wrap the business logic route middleware to catch asynchronous errors and propagate them to the global error handler.
  9. Route Handler - Business Logic - Execute the business logic and create the response.
  10. Compression - Apply gzip compression to the response body.
  11. Express server sets default headers and sends the response to the client.

When Something Goes Wrong

  1. 404 Handler - Handle routing failures and send a 404 error response.
  2. Custom Error Handlers - Handle prisma, assertions, axios errors and send appropriate error responses.
  3. Global Error Handler - Handle all other errors and send a 500 error response.
  4. Compression - Apply gzip compression to the response body.
  5. Express server sets default headers and sends the response to the client.

* For routes registered before the authenticate middleware, such as /health and /auth, this middleware is not invoked.

Project Structure

  • src/index.js - Entry point of the application; imports and starts the application.
  • src/cluster.js - Implements clustering for load balancing across multiple CPU cores.
  • src/app.js - Configures and initializes the Express application.
  • src/routes/index.js - Main router that consolidates all route modules.
  • src/routes/*.js - Individual route modules implementing specific API endpoints.
  • src/middleware/*.js - Express middleware functions.
  • src/services/*.js - Houses core business logic separate from the routing layer.
  • src/core/*.js - Essential to application logic but not business-specific.
  • src/cron/*.js - Scheduled tasks run at specific intervals.
  • src/scripts/*.js - Standalone scripts that need to be executed manually.
  • config/*.json - Hierarchical configuration files.
  • prisma/schema.prisma - Defines the database schema using Prisma ORM.
  • prisma/seed.js - Script for seeding initial data into the database.
  • utils/index.js - Reusable functions that are not tied to business logic.
  • keys/genKeys.sh - Script for generating JWT keys.
  • .env - Environment-specific configuration file used for managing secrets and runtime settings.
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/api/middleware-explained.png b/docs/.vitepress/dist/api/middleware-explained.png new file mode 100644 index 000000000..cf24e7aa7 Binary files /dev/null and b/docs/.vitepress/dist/api/middleware-explained.png differ diff --git a/docs/.vitepress/dist/api/router-explained.png b/docs/.vitepress/dist/api/router-explained.png new file mode 100644 index 000000000..d1800bac6 Binary files /dev/null and b/docs/.vitepress/dist/api/router-explained.png differ diff --git a/docs/.vitepress/dist/architecture.html b/docs/.vitepress/dist/architecture.html index b024cfc53..c9558e120 100644 --- a/docs/.vitepress/dist/architecture.html +++ b/docs/.vitepress/dist/architecture.html @@ -3,22 +3,24 @@ - Bioloop + Architecture | Bioloop - - + + + - - - - - + + + + + + -
Skip to content
- +
Skip to content
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.js b/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.js new file mode 100644 index 000000000..1b0d4379a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.js @@ -0,0 +1,11 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const d=JSON.parse('{"title":"Cluster Management","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/cluster.md","filePath":"api/01-core/cluster.md","lastUpdated":null}'),n={name:"api/01-core/cluster.md"};function l(r,s,o,h,p,c){return e(),a("div",null,s[0]||(s[0]=[t(`

Cluster Management

The manage_cluster function is a utility for managing a cluster of worker processes in a Node.js application. It provides features such as scaling, restart limits, and graceful shutdowns, making it easier to build robust and scalable applications.

This system is designed to efficiently manage multiple worker processes in a Node.js application. It leverages the node:cluster module to distribute workloads across available CPU cores, ensuring optimal utilization of system resources. This feature is particularly useful for high-performance applications that need to handle a large number of concurrent requests.

Without this system, the application would run as a single process, potentially underutilizing multi-core CPUs and becoming a bottleneck under heavy load.

Overview

The manage_cluster function allows you to:

Configuration Options

The function accepts an options object with the following properties:

Example Usage

javascript
const manage_cluster = require('./core/cluster-manager');
+
+manage_cluster({
+  master: () => console.log('Master process running'),
+  worker: () => setInterval(() => console.log('Worker process running'), 1000),
+  count: 4,
+  max_restarts: 5,
+  max_restarts_interval: 15000,
+  grace: 3000,
+  signals: ['SIGINT', 'SIGTERM', 'SIGHUP'],
+});

In this example:

Metrics Integration

The master process can also expose aggregated metrics using the prom-client library. This is demonstrated in the cluster.js file, where a metrics server is set up to listen on a configurable port.

Refer to the cluster.js file for a complete example of integrating metrics with the cluster manager.

`,17)]))}const g=i(n,[["render",l]]);export{d as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.lean.js b/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.lean.js new file mode 100644 index 000000000..febf9724d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_cluster.md.BgictSao.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const d=JSON.parse('{"title":"Cluster Management","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/cluster.md","filePath":"api/01-core/cluster.md","lastUpdated":null}'),n={name:"api/01-core/cluster.md"};function l(r,s,o,h,p,c){return e(),a("div",null,s[0]||(s[0]=[t("",17)]))}const g=i(n,[["render",l]]);export{d as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.js b/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.js new file mode 100644 index 000000000..e1a12b7d3 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.js @@ -0,0 +1,19 @@ +import{_ as s,c as a,o as e,ag as n}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Configuration","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/configuration.md","filePath":"api/01-core/configuration.md","lastUpdated":null}'),t={name:"api/01-core/configuration.md"};function o(l,i,p,r,h,d){return e(),a("div",null,i[0]||(i[0]=[n(`

Configuration

The configuration system is a critical part of the application, enabling developers to manage environment-specific settings and maintain clean, maintainable code. It uses the config module to load and manage configuration files, ensuring that the application behaves consistently across different environments.

Purpose of the Configuration System

The configuration system exists to centralize and standardize the way application settings are managed. Without it, developers would need to hardcode settings or rely on ad-hoc methods to manage environment-specific configurations, leading to brittle and error-prone code.

By using this system:

How It Fits Into the System

The configuration system integrates seamlessly with the application by:

This ensures that all parts of the application use the same source of truth for configuration, reducing duplication and potential inconsistencies.

Configuration Files

The following configuration files are used:

Precedence of Configuration

The configuration system resolves settings in the following order of precedence:

  1. Command-line arguments
  2. Environment variables
  3. {NODE_ENV}.json (e.g., production.json)
  4. default.json

This precedence ensures that the most specific settings are applied, while falling back to defaults when necessary.

Environment Variables

The application uses the dotenv-safe module to load environment variables from a .env file. This ensures that all required variables are defined and prevents runtime errors caused by missing configurations.

Example .env File

env
DATABASE_PASSWORD=your_database_password
+WORKFLOW_AUTH_TOKEN=your_auth_token
+OAUTH_BASE_URL=https://example.com/oauth

Loading Environment Variables

To load environment variables, the following code is used:

javascript
require('dotenv-safe').config();

Ensure that a .env.example file exists to document required variables and their expected format.

Step-by-Step Instructions for Usage

  1. Define Default Settings: Add default settings in default.json under the ./config/ directory. For example:

    json
    {
    +  "express": {
    +    "port": 3030,
    +    "host": "localhost"
    +  }
    +}
  2. Add Environment-Specific Overrides: Create a file named {NODE_ENV}.json (e.g., production.json) and override specific settings:

    json
    {
    +  "express": {
    +    "port": 8080
    +  }
    +}
  3. Map Environment Variables: Use custom-environment-variables.json to map sensitive settings to environment variables:

    json
    {
    +  "express": {
    +    "port": "EXPRESS_PORT"
    +  }
    +}
  4. Create a .env File: Define the required environment variables in a .env file:

    env
    EXPRESS_PORT=3030
  5. Load Configuration in Code: Access configuration values in your application using the config module:

    javascript
    require('dotenv-safe').config();
    +const config = require('config');
    +const port = config.get('express.port');
    +console.log(\`Server running on port \${port}\`);

foo bar

`,28)]))}const g=s(t,[["render",o]]);export{k as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.lean.js b/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.lean.js new file mode 100644 index 000000000..d9b6b9d52 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_configuration.md.CKqmrnT_.lean.js @@ -0,0 +1 @@ +import{_ as s,c as a,o as e,ag as n}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Configuration","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/configuration.md","filePath":"api/01-core/configuration.md","lastUpdated":null}'),t={name:"api/01-core/configuration.md"};function o(l,i,p,r,h,d){return e(),a("div",null,i[0]||(i[0]=[n("",28)]))}const g=s(t,[["render",o]]);export{k as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.js b/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.js new file mode 100644 index 000000000..936716e1e --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.js @@ -0,0 +1,16 @@ +import{_ as i,c as a,o as e,ag as n}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Scheduling Tasks","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/cron-task-scheduling.md","filePath":"api/01-core/cron-task-scheduling.md","lastUpdated":null}'),t={name:"api/01-core/cron-task-scheduling.md"};function l(h,s,k,p,r,o){return e(),a("div",null,s[0]||(s[0]=[n(`

Scheduling Tasks

The cron task scheduling feature allows you to define and execute recurring tasks at specified intervals in the primary process of the cluster. This feature is useful for automating maintenance, cleanup, and other background operations in your application.

How It Works

The registerCronJobs function in the /api/src/core/cron.js file is responsible for registering all cron jobs. It uses the node-cron library to define and schedule tasks. Each task is implemented as a separate module, ensuring modularity and reusability.

Usage Instructions

  1. Define a New Task:

    javascript
    module.exports.run = async (logger) => {
    +  logger.info('Running example task...');
    +  // Task logic here
    +};
  2. Register the Task:

    javascript
    // filepath: /Users/deduggi/Documents/SCA/bioloop/api/src/core/cron.js
    +const cron = require('node-cron');
    +const { createTaskLogger } = require('./logger');
    +
    +function registerCronJobs() {
    +  cron.schedule('* * * * *', () => {
    +    const task = require('../cron/exampleTask.cron');
    +    const taskLogger = createTaskLogger('exampleTask');
    +    task.run(taskLogger);
    +  });
    +}
    +
    +module.exports = registerCronJobs;

Cron tasks are registered using the beforeApplicationFork lifecycle hook ensuring that they run in the primary process of the cluster.

The createTaskLogger function creates a logger instance for each task, allowing you to log task-specific messages. The logger is passed to the task function as an argument. The log file for each task is stored in the logs directory with the task name as the filename.

Best Practices

`,10)]))}const g=i(t,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.lean.js b/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.lean.js new file mode 100644 index 000000000..6e5b2dd6a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_cron-task-scheduling.md.Dm3QHNiN.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as n}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Scheduling Tasks","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/cron-task-scheduling.md","filePath":"api/01-core/cron-task-scheduling.md","lastUpdated":null}'),t={name:"api/01-core/cron-task-scheduling.md"};function l(h,s,k,p,r,o){return e(),a("div",null,s[0]||(s[0]=[n("",10)]))}const g=i(t,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.js b/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.js new file mode 100644 index 000000000..38cbddd6e --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.js @@ -0,0 +1,80 @@ +import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"Error Handling","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/error-handling.md","filePath":"api/01-core/error-handling.md","lastUpdated":null}'),h={name:"api/01-core/error-handling.md"};function t(l,s,p,k,r,E){return n(),a("div",null,s[0]||(s[0]=[e(`

Error Handling

Source: Express Error Handling

Error handling is a critical part of any robust application. This feature ensures that errors are properly caught, logged, and handled in a way that provides meaningful feedback to the client while maintaining the integrity of the server. Without proper error handling, unexpected issues could crash the server or expose sensitive information to clients.

This document explains the error-handling mechanisms implemented in this codebase, how they fit into the overall system, and how they help maintain clean, maintainable code.

Asynchronous Error Handler

Express (versions below 5) does not automatically catch errors thrown in asynchronous code. To address this, an asyncHandler middleware is used to wrap asynchronous route handlers. This middleware ensures that any errors are passed to the default error handler.

Why It Exists

Without this middleware, developers would need to manually wrap every asynchronous route handler in a try-catch block, leading to repetitive and error-prone code.

Usage

Wrap your asynchronous route handlers with asyncHandler to automatically catch errors and pass them to the error handler.

javascript
const asyncHandler = require('../middleware/asyncHandler');
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+    const user = await userService.findActiveUserBy(
+      'username', req.query.username
+    );
+    res.json(user);
+}));

This replaces the need for manual try-catch blocks:

javascript
router.get('/user', async (req, res, next) => {
+  try {
+    const user = await userService.findActiveUserBy('username', req.query.username);
+    res.json(user);
+  } catch (err) {
+    next(err);
+  }
+});

The Default Error Handler

The default error handler is the last middleware in the stack. It ensures that all unhandled errors are processed and a proper response is sent to the client.

Key Features

Custom Default Error Handler

The custom error handler (errorHandler) logs errors to the console and sends appropriate responses to clients:

Example

javascript
app.use(errorHandler);

Custom Error Handlers

Custom error handlers are used to handle specific types of errors before they reach the default error handler.

404 Handler

The notFound middleware catches requests to unknown routes and forwards a 404 error.

javascript
app.use(notFound);

Prisma Not Found Error Handler

Prisma's query engine may throw opaque errors when a resource is not found. The prismaNotFoundHandler middleware intercepts these errors and converts them into HTTP 404 responses.

Example

Before refactoring:

javascript
router.delete('/:username', asyncHandler(async (req, res, next) => {
+  try {
+    const deletedUser = await userService.softDeleteUser(req.params.username);
+    res.json(deletedUser);
+  } catch (e) {
+    if (e instanceof Prisma.PrismaClientKnownRequestError && e?.meta?.cause?.includes('not found')) {
+      return next(createError.NotFound());
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(prismaNotFoundHandler);
+
+router.delete('/:username', asyncHandler(async (req, res, next) => {
+  const deletedUser = await userService.softDeleteUser(req.params.username);
+  res.json(deletedUser);
+}));

Prisma Constraint Violation Handler

Handles database constraint violations (e.g., unique constraints) and sends appropriate HTTP responses.

Example

Before refactoring:

javascript
router.post('/', asyncHandler(async (req, res, next) => {
+  try {
+    const newUser = await userService.createUser(req.body);
+    res.json(newUser);
+  } catch (e) {
+    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
+      return next(createError.Conflict('User already exists'));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(prismaConstraintFailedHandler);
+
+router.post('/', asyncHandler(async (req, res, next) => {
+  const newUser = await userService.createUser(req.body);
+  res.json(newUser);
+}));

Assertion Error Handler

Catches AssertionError instances and sends a 400 Bad Request response.

Example

Before refactoring:

javascript
const assert = require('assert');
+router.post('/', asyncHandler(async (req, res, next) => {
+  try {
+    assert(req.body.username, 'Username is required');
+    const newUser = await userService.createUser(req.body);
+    res.json(newUser);
+  } catch (e) {
+    if (e instanceof assert.AssertionError) {
+      return next(createError.BadRequest(e.message));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(assertionErrorHandler);
+
+router.post('/', asyncHandler(async (req, res, next) => {
+  assert(req.body.username, 'Username is required');
+  const newUser = await userService.createUser(req.body);
+  res.json(newUser);
+}));

Axios Error Handler

Handles errors from Axios HTTP requests, logs them, and sends a 500 Internal Server Error response.

Example

Before refactoring:

javascript
const axios = require('axios');
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+  try {
+    const response = await axios.get('https://api.example.com/user');
+    res.json(response.data);
+  } catch (e) {
+    if (e.response) {
+      return next(createError(e.response.status, e.response.statusText));
+    }
+    return next(e);
+  }
+}));

After refactoring:

javascript
app.use(axiosErrorHandler);
+
+router.get('/user', asyncHandler(async (req, res, next) => {
+  const response = await axios.get('https://api.example.com/user');
+  res.json(response.data);
+}));

Integration into the System

Error-handling middleware is registered in app.js in the following order:

  1. notFound for unknown routes.
  2. Prisma-specific handlers (prismaNotFoundHandler, prismaConstraintFailedHandler).
  3. Other custom handlers (assertionErrorHandler, axiosErrorHandler).
  4. errorHandler as the final fallback.

This layered approach ensures that errors are handled at the appropriate level, keeping the codebase clean and maintainable.

Summary

By centralizing error handling, this system:

Follow the examples above to integrate error handling into your routes and middleware.

`,63)]))}const g=i(h,[["render",t]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.lean.js b/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.lean.js new file mode 100644 index 000000000..feb59f182 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_error-handling.md.C54wHFwA.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"Error Handling","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/error-handling.md","filePath":"api/01-core/error-handling.md","lastUpdated":null}'),h={name:"api/01-core/error-handling.md"};function t(l,s,p,k,r,E){return n(),a("div",null,s[0]||(s[0]=[e("",63)]))}const g=i(h,[["render",t]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.js b/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.js new file mode 100644 index 000000000..5b7fe61cd --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/events.md","filePath":"api/01-core/events.md","lastUpdated":null}'),r={name:"api/01-core/events.md"};function s(n,o,c,p,i,d){return a(),t("div")}const m=e(r,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.lean.js b/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.lean.js new file mode 100644 index 000000000..5b7fe61cd --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_events.md.CZaq614g.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/events.md","filePath":"api/01-core/events.md","lastUpdated":null}'),r={name:"api/01-core/events.md"};function s(n,o,c,p,i,d){return a(),t("div")}const m=e(r,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.js b/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.js new file mode 100644 index 000000000..8667da4d0 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Core","description":"","frontmatter":{"title":"Core"},"headers":[],"relativePath":"api/01-core/index.md","filePath":"api/01-core/index.md","lastUpdated":null}'),r={name:"api/01-core/index.md"};function o(n,c,i,s,d,p){return a(),t("div")}const m=e(r,[["render",o]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.lean.js b/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.lean.js new file mode 100644 index 000000000..8667da4d0 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_index.md.C4HMl5sN.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Core","description":"","frontmatter":{"title":"Core"},"headers":[],"relativePath":"api/01-core/index.md","filePath":"api/01-core/index.md","lastUpdated":null}'),r={name:"api/01-core/index.md"};function o(n,c,i,s,d,p){return a(),t("div")}const m=e(r,[["render",o]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.js b/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.js new file mode 100644 index 000000000..e347b1635 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as s,ag as o}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Lifecycle Hooks","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/lifecycle-hooks.md","filePath":"api/01-core/lifecycle-hooks.md","lastUpdated":null}'),t={name:"api/01-core/lifecycle-hooks.md"};function n(l,e,r,c,p,h){return s(),a("div",null,e[0]||(e[0]=[o('

Lifecycle Hooks

The lifecycle hooks in this project are designed to manage specific tasks during the application's lifecycle. These hooks ensure that critical operations are performed at the right time, such as during startup, shutdown, or before forking worker processes.

Location of Lifecycle Hooks

The lifecycle hooks are implemented in the file located at:

src/core/lifecycle.js

Etiquette for Editing/Updating Lifecycle Hooks

For example:

javascript
// Define your logic in a separate file\nasync function customLogic() {\n  // ...custom logic...\n}\n\n// Call it in the lifecycle hook\nasync function onApplicationBootstrap() {\n  await customLogic();\n}

Hooks Overview

beforeApplicationFork

onApplicationBootstrap

beforeApplicationShutdown

onApplicationShutdown

Integration

These hooks are used in the application lifecycle to ensure proper initialization and cleanup. For example:

By leveraging these hooks and following the guidelines for editing them, the application ensures a clean and predictable lifecycle management process.

',22)]))}const g=i(t,[["render",n]]);export{k as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.lean.js b/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.lean.js new file mode 100644 index 000000000..295422848 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_lifecycle-hooks.md.ByaWgTP1.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as s,ag as o}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Lifecycle Hooks","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/lifecycle-hooks.md","filePath":"api/01-core/lifecycle-hooks.md","lastUpdated":null}'),t={name:"api/01-core/lifecycle-hooks.md"};function n(l,e,r,c,p,h){return s(),a("div",null,e[0]||(e[0]=[o("",22)]))}const g=i(t,[["render",n]]);export{k as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.js b/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.js new file mode 100644 index 000000000..60a02d557 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/queue.md","filePath":"api/01-core/queue.md","lastUpdated":null}'),r={name:"api/01-core/queue.md"};function o(c,s,n,p,i,u){return a(),t("div")}const l=e(r,[["render",o]]);export{_ as __pageData,l as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.lean.js b/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.lean.js new file mode 100644 index 000000000..60a02d557 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_queue.md.DrnDJfm4.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/queue.md","filePath":"api/01-core/queue.md","lastUpdated":null}'),r={name:"api/01-core/queue.md"};function o(c,s,n,p,i,u){return a(),t("div")}const l=e(r,[["render",o]]);export{_ as __pageData,l as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.js b/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.js new file mode 100644 index 000000000..15ba82442 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.js @@ -0,0 +1,95 @@ +import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"Request Validation","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/validation.md","filePath":"api/01-core/validation.md","lastUpdated":null}'),t={name:"api/01-core/validation.md"};function l(e,s,k,p,r,E){return n(),a("div",null,s[0]||(s[0]=[h(`

Request Validation

Request validation ensures that incoming HTTP requests contain the expected data in the correct format. This feature uses express-validator to validate the request's query parameters, route parameters, or body content. It helps enforce data integrity and prevents invalid or malicious data from propagating through the system. express-validator wraps the extensive collection of validators and sanitizers offered by validator.js.

Without this validation layer, developers would need to write repetitive and error-prone checks in every route handler, leading to cluttered and less maintainable code. By centralizing validation logic, this feature promotes clean, declarative, and reusable code.

Benefits

Usage Instructions

To use the validation feature, follow these steps:

  1. Import the validate function and the necessary validation methods from express-validator.
  2. Define the validation rules for the request's body, query, or params.
  3. Wrap the validation rules with the validate function and include it as middleware in your route definition.
  4. Handle the request in the route handler, assuming the data is already validated.

see the example here.

Comparison with different approaches

Manual Validation

Manual validation involves writing custom logic directly in the route handler to check and sanitize incoming data. While this approach provides flexibility, it often leads to repetitive, error-prone code that can clutter route handlers and make them harder to maintain.

javascript
app.post(
+  '/user',
+  (req, res) => {
+    if (!req.body.username || !req.body.password) {
+      return res.status(400).json({ error: 'Username and password are required' });
+    }
+
+    if (!utils.isEmail(req.body.username)) {
+      return res.status(400).json({ error: 'Invalid email address' });
+    }
+
+    if (req.body.password.length < 5) {
+      return res.status(400).json({ error: 'Password must be at least 5 characters long' });
+    }
+
+    const age = parseInt(req.body.age, 10);
+    if (isNaN(age) || age < 18) {
+      return res.status(400).json({ error: 'Age must be a number greater than or equal to 18' });
+    }
+
+    User.create({
+      username: req.body.username,
+      password: req.body.password,
+    }).then(user => res.json(user));
+  },
+);

Using express-validator

By leveraging express-validator, you can define validation rules declaratively, reducing boilerplate code and improving readability.

javascript
app.post(
+  '/user',
+  body('username').isEmail(),
+  body('password').isLength({ min: 5 }),
+  body('age').isInt({ min: 18 }).toInt(),
+  (req, res) => {
+    // Finds the validation errors in this request and wraps them in an object with handy functions
+    const errors = validationResult(req);
+    if (!errors.isEmpty()) {
+      return res.status(400).json({ errors: errors.array() });
+    }
+
+    User.create({
+      username: req.body.username,
+      password: req.body.password,
+      age: req.body.age,
+    }).then(user => res.json(user));
+  },
+);

Using validate

The validate function further simplifies the use of express-validator by wrapping validation rules and handling errors automatically. It returns a 400 Bad Request response if validation fails, reducing the need for manual error handling in route handlers.

javascript
const { validate } = require('middleware/validators');
+const asyncHandler = require('middleware/asyncHandler');
+
+app.post(
+  '/user',
+  validate([
+    body('username').isEmail(),
+    body('password').isLength({ min: 5 }),
+    body('age').isInt({ min: 18 }).toInt()
+  ]),
+  asyncHandler(async (req, res) => {
+    const user = await User.create({
+      username: req.body.username,
+      password: req.body.password,
+      age: req.body.age,
+    });
+    res.json(user);
+  }),
+);

Explanation of the Code

  1. Validation Rules: The body('username').isEmail(), body('password').isLength({ min: 5 }), body('age').isInt({ min: 18 }).toInt() define the validation logic for the username, password,and age fields.
  2. validate Middleware: Wraps the validation and sanitization rules and handles errors automatically, returning a 400 Bad Request response if validation fails.
  3. Async Handler: The asyncHandler middleware ensures proper error handling for asynchronous operations in the route handler.

By using the validate function, you can focus on implementing business logic in your route handlers while ensuring that all incoming data is valid and secure. Also ensures that correct error responses are sent back to the client when validation fails.

More Examples

javascript
const { validate } = require('middleware/validators');
+const {
+  query, param, body, checkSchema,
+} = require('express-validator');
+
+
+validate([
+  // integer between 1 and 100
+  query('limit').isInt({ min: 1, max: 100 }).toInt(), 
+
+  // list of allowed values
+  query('type').isIn(['RAW_DATA', 'DATA_PRODUCT']).optional(), 
+  query('sort_order').default('desc').isIn(['asc', 'desc'])
+
+  // boolean value with default
+  // converts 'true' and 'false' strings to boolean
+  query('deleted').toBoolean().default(false),
+
+  // date in ISO8601 format
+  query('created_at_start').isISO8601()
+
+  // convert to BigInt
+  body('du_size').optional().notEmpty().customSanitizer(BigInt)
+
+  // Array & size limits
+  body('datasets').isArray({ min: 1, max: 100 }),
+
+  // validate objects in array
+  body('datasets.*.name').notEmpty(),
+
+  // String length
+  body('name').optional().isLength({ min: 5 }),
+
+]),
`,24)]))}const y=i(t,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.lean.js b/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.lean.js new file mode 100644 index 000000000..32ad937e7 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_01-core_validation.md.CumxzlHO.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"Request Validation","description":"","frontmatter":{},"headers":[],"relativePath":"api/01-core/validation.md","filePath":"api/01-core/validation.md","lastUpdated":null}'),t={name:"api/01-core/validation.md"};function l(e,s,k,p,r,E){return n(),a("div",null,s[0]||(s[0]=[h("",24)]))}const y=i(t,[["render",l]]);export{g as __pageData,y as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.js b/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.js new file mode 100644 index 000000000..83d9e04d9 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.js @@ -0,0 +1 @@ +import{_ as a,c as e,o as t}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/02-data/cache.md","filePath":"api/02-data/cache.md","lastUpdated":null}'),c={name:"api/02-data/cache.md"};function r(o,s,n,d,p,i){return t(),e("div")}const m=a(c,[["render",r]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.lean.js b/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.lean.js new file mode 100644 index 000000000..83d9e04d9 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_cache.md.BTpvCcGr.lean.js @@ -0,0 +1 @@ +import{_ as a,c as e,o as t}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/02-data/cache.md","filePath":"api/02-data/cache.md","lastUpdated":null}'),c={name:"api/02-data/cache.md"};function r(o,s,n,d,p,i){return t(),e("div")}const m=a(c,[["render",r]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.js b/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.js new file mode 100644 index 000000000..87d158427 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.js @@ -0,0 +1 @@ +import{_ as a,c as t,o as e}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Data","description":"","frontmatter":{"title":"Data"},"headers":[],"relativePath":"api/02-data/index.md","filePath":"api/02-data/index.md","lastUpdated":null}'),n={name:"api/02-data/index.md"};function d(i,r,o,s,c,p){return e(),t("div")}const m=a(n,[["render",d]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.lean.js b/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.lean.js new file mode 100644 index 000000000..87d158427 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_index.md.Boa6RJkG.lean.js @@ -0,0 +1 @@ +import{_ as a,c as t,o as e}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Data","description":"","frontmatter":{"title":"Data"},"headers":[],"relativePath":"api/02-data/index.md","filePath":"api/02-data/index.md","lastUpdated":null}'),n={name:"api/02-data/index.md"};function d(i,r,o,s,c,p){return e(),t("div")}const m=a(n,[["render",d]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.js b/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.js new file mode 100644 index 000000000..e5cc003cd --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.js @@ -0,0 +1,37 @@ +import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"ORM","description":"","frontmatter":{},"headers":[],"relativePath":"api/02-data/prisma.md","filePath":"api/02-data/prisma.md","lastUpdated":null}'),e={name:"api/02-data/prisma.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`

ORM

Prisma.js is used as the ORM (Object-Relational Mapping) and schema migration tool in this project. It simplifies database interactions by providing a type-safe API and automates schema migrations, ensuring consistency across environments.

The DATABASE_URL environment variable is used to connect to the database. This is configured in the .env file.

Gotchas

Auto Timestamp

now() sets the current timestamp in the UTC time zone. Both createdAt and updatedAt fields are in UTC timezone. If the database is set to a different timezone, you may see future timestamps when connected to the db using a tool like dbeaver.

This is not a problem when using prisma queries, as prisma automatically converts the timestamps to the local timezone.

prisma
model Post {
+  id               Int                @id @default(autoincrement())
+  title            String
+  content          String?
+  published        Boolean?           @default(false)
+  createdAt        DateTime           @default(now()) @db.Timestamp(6)
+  updatedAt        DateTime           @updatedAt @default(now()) @db.Timestamp(6)
+}

Aliasing and Excluding Columns

Prisma does not support aliasing columns (like SQL's AS keyword) or excluding specific columns directly. Instead, you can transform the returned objects in JavaScript.

Example:

javascript
const _ = require('lodash');
+
+function transformUser(user) {
+  return _(user)
+    .set('roles', user.user_role.map((obj) => obj.roles.name)) // Transform and set new keys
+    .omit(['password', 'id', 'user_role']) // Remove unwanted keys
+    .value();
+}

For more details, refer to:

Avoid SELECT * Queries

Fetching all columns can negatively impact performance due to deserialization overhead, increased network transmission, and inefficiencies with non-inline columns (e.g., blobs, JSON).

Bad Example:

javascript
const users = await prisma.user.findMany();

Good Example:

javascript
const users = await prisma.user.findMany({
+  select: {
+    id: true,
+    name: true,
+    email: true,
+  },
+});

For more information, see this source.

Sorting with nulls last

Prisma's sorting behavior depends on whether a column is nullable. For nullable fields, you must explicitly specify nulls: 'last' or nulls: 'first'. For non-nullable fields, including this option will throw an error.

Example:

javascript
const buildOrderByObject = (field, sortOrder, nullsLast = true) => {
+  const nullable_order_by_fields = ['du_size', 'size'];
+
+  if (!field || !sortOrder) {
+    return {};
+  }
+  if (nullable_order_by_fields.includes(field)) {
+    return {
+      [field]: { sort: sortOrder, nulls: nullsLast ? 'last' : 'first' },
+    };
+  }
+  return {
+    [field]: sortOrder,
+  };
+};

Logging Queries

Enable logging to debug and monitor Prisma queries:

javascript
const prisma = new PrismaClient({
+  log: ['query', 'info', 'warn', 'error'],
+});

This will log all queries, warnings, and errors generated by the Prisma client.

`,31)]))}const g=i(e,[["render",l]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.lean.js b/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.lean.js new file mode 100644 index 000000000..b09f4882d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_02-data_prisma.md.BUPmvYLB.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"ORM","description":"","frontmatter":{},"headers":[],"relativePath":"api/02-data/prisma.md","filePath":"api/02-data/prisma.md","lastUpdated":null}'),e={name:"api/02-data/prisma.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t("",31)]))}const g=i(e,[["render",l]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.js b/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.js new file mode 100644 index 000000000..693a0aff3 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.js @@ -0,0 +1,6 @@ +import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.C9SxlbOG.js";const n="/bioloop/docs/api_auth.png",E=JSON.parse('{"title":"Authentication","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/authentication.md","filePath":"api/03-security/authentication.md","lastUpdated":null}'),h={name:"api/03-security/authentication.md"};function r(p,s,l,k,d,o){return t(),a("div",null,s[0]||(s[0]=[e('

Authentication

The API uses IU CAS authnetication model.

All the routes and sub-routers added after the authenticate middleware in index router require authentication. The routes that do not require authentication such as auth routes are added before this.

The authenticate middleware, parses the Authorization header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as req.user

To add authentication to a single route:

javascript
const { authenticate } = require('../middleware/auth');
+
+router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => {
+  const user = await userService.findActiveUserBy('username', req.user.username);
+  // ...
+}))
`,7)]))}const u=i(h,[["render",r]]);export{E as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.lean.js b/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.lean.js new file mode 100644 index 000000000..0a6a92362 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_authentication.md.C9rOmhTI.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.C9SxlbOG.js";const n="/bioloop/docs/api_auth.png",E=JSON.parse('{"title":"Authentication","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/authentication.md","filePath":"api/03-security/authentication.md","lastUpdated":null}'),h={name:"api/03-security/authentication.md"};function r(p,s,l,k,d,o){return t(),a("div",null,s[0]||(s[0]=[e("",7)]))}const u=i(h,[["render",r]]);export{E as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.js b/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.js new file mode 100644 index 000000000..ab12a1780 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.js @@ -0,0 +1,57 @@ +import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"Authorization","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/authorization.md","filePath":"api/03-security/authorization.md","lastUpdated":null}'),h={name:"api/03-security/authorization.md"};function t(l,s,p,k,r,E){return n(),a("div",null,s[0]||(s[0]=[e(`

Authorization

Role Based Access Control

accesscontrol library is used to provide role based authorization to routes (resources).

Roles in this application:

Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in services/accesscontrols.js.

The goal of the accessControl middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource.

A simple use case:

Objective: Users with user role are only permitted to read and update thier own profile. Whereas, users with admin role can create new users, read & update any user's profile, and delete any user.

Role design:

javascript
{
+  admin: {
+    user: {
+      'create:any': ['*'],
+      'read:any': ['*'],
+      'update:any': ['*'],
+      'delete:any': ['*'],
+    },
+  },
+  user: {
+    user: {
+      'read:own': ['*'],
+      'update:own': ['*'],
+    },
+  },
+}

Permission check:

Code to check if the requester to is authorized to GET /users/dduck. This route is protected by authenticate middleware which attaches the requester profile to req.user if the token is valid.

javascript
const { authenticate } = require('../middleware/auth');
+
+router.get('/:username',
+  authenticate,
+  asyncHandler(async (req, res, next) => {
+    
+    const roles = req.user.roles;
+    const resourceOwner = req.params.username;
+    const requester = req.user?.username;
+
+    const permission = (requester === resourceOwner)
+        ? ac.can(roles).readOwn('user')
+        : ac.can(roles).readAny('user');
+    
+    if (!permission.granted) {
+      return next(createError(403)); // Forbidden
+    }
+    else {
+      const user = await userService.findActiveUserBy('username', req.params.username);
+      if (user) { return res.json(user); }
+      return next(createError.NotFound());
+    }
+  }),
+);

readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only user role and is requesting the profile of other users, the request will be denied.

AccessControl Middleware Usage

accessControl middleware is a generic function to handle authorization for any action or resource with optional ownership checking.

The above code can be written consicely with the help of accessControl middleware.

routes/*.js

javascript
// import middleware
+const { authenticate, accessControl } = require('../middleware/auth');
+
+// configre the middleware to authorize requests to user resource
+// resource ownership is checked by default
+// throws 403 if not authorized
+const isPermittedTo = accessControl('user');
+
+//
+router.get(
+  '/:username',
+  authenticate,
+  isPermittedTo('read', { checkOwnerShip: true }),
+  asyncHandler(async (req, res, next) => {
+    const user = await userService.findActiveUserBy('username', req.params.username);
+    if (user) { return res.json(user); }
+    return next(createError.NotFound());
+  }),
+);
`,21)]))}const g=i(h,[["render",t]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.lean.js b/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.lean.js new file mode 100644 index 000000000..09c68eb6c --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_authorization.md.BzhJzT5t.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C9SxlbOG.js";const o=JSON.parse('{"title":"Authorization","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/authorization.md","filePath":"api/03-security/authorization.md","lastUpdated":null}'),h={name:"api/03-security/authorization.md"};function t(l,s,p,k,r,E){return n(),a("div",null,s[0]||(s[0]=[e("",21)]))}const g=i(h,[["render",t]]);export{o as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.js b/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.js new file mode 100644 index 000000000..1f7e2d8b5 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/cookies.md","filePath":"api/03-security/cookies.md","lastUpdated":null}'),o={name:"api/03-security/cookies.md"};function s(c,r,i,n,p,d){return a(),t("div")}const m=e(o,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.lean.js b/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.lean.js new file mode 100644 index 000000000..1f7e2d8b5 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_cookies.md.DdGBHNzP.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/cookies.md","filePath":"api/03-security/cookies.md","lastUpdated":null}'),o={name:"api/03-security/cookies.md"};function s(c,r,i,n,p,d){return a(),t("div")}const m=e(o,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.js b/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.js new file mode 100644 index 000000000..f4ced9917 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/cors.md","filePath":"api/03-security/cors.md","lastUpdated":null}'),r={name:"api/03-security/cors.md"};function s(c,o,i,n,p,d){return a(),t("div")}const m=e(r,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.lean.js b/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.lean.js new file mode 100644 index 000000000..f4ced9917 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_cors.md.D0kQQrT6.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/03-security/cors.md","filePath":"api/03-security/cors.md","lastUpdated":null}'),r={name:"api/03-security/cors.md"};function s(c,o,i,n,p,d){return a(),t("div")}const m=e(r,[["render",s]]);export{l as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.js b/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.js new file mode 100644 index 000000000..a3999870d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Security","description":"","frontmatter":{"title":"Security"},"headers":[],"relativePath":"api/03-security/index.md","filePath":"api/03-security/index.md","lastUpdated":null}'),i={name:"api/03-security/index.md"};function r(c,n,s,o,d,p){return a(),t("div")}const u=e(i,[["render",r]]);export{_ as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.lean.js b/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.lean.js new file mode 100644 index 000000000..a3999870d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_03-security_index.md.iFnvHb70.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Security","description":"","frontmatter":{"title":"Security"},"headers":[],"relativePath":"api/03-security/index.md","filePath":"api/03-security/index.md","lastUpdated":null}'),i={name:"api/03-security/index.md"};function r(c,n,s,o,d,p){return a(),t("div")}const u=e(i,[["render",r]]);export{_ as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.js b/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.js new file mode 100644 index 000000000..4a80f857c --- /dev/null +++ b/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/04-deployment/docker-image-design.md","filePath":"api/04-deployment/docker-image-design.md","lastUpdated":null}'),o={name:"api/04-deployment/docker-image-design.md"};function n(d,r,s,i,c,p){return a(),t("div")}const _=e(o,[["render",n]]);export{l as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.lean.js b/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.lean.js new file mode 100644 index 000000000..4a80f857c --- /dev/null +++ b/docs/.vitepress/dist/assets/api_04-deployment_docker-image-design.md.C59MbThu.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/04-deployment/docker-image-design.md","filePath":"api/04-deployment/docker-image-design.md","lastUpdated":null}'),o={name:"api/04-deployment/docker-image-design.md"};function n(d,r,s,i,c,p){return a(),t("div")}const _=e(o,[["render",n]]);export{l as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.js b/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.js new file mode 100644 index 000000000..13864ba9a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Deployment","description":"","frontmatter":{"title":"Deployment"},"headers":[],"relativePath":"api/04-deployment/index.md","filePath":"api/04-deployment/index.md","lastUpdated":null}'),n={name:"api/04-deployment/index.md"};function o(p,d,i,r,s,l){return a(),t("div")}const _=e(n,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.lean.js b/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.lean.js new file mode 100644 index 000000000..13864ba9a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_04-deployment_index.md.jH9Gr9hU.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Deployment","description":"","frontmatter":{"title":"Deployment"},"headers":[],"relativePath":"api/04-deployment/index.md","filePath":"api/04-deployment/index.md","lastUpdated":null}'),n={name:"api/04-deployment/index.md"};function o(p,d,i,r,s,l){return a(),t("div")}const _=e(n,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.js b/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.js new file mode 100644 index 000000000..41cd17f46 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.js @@ -0,0 +1 @@ +import{_ as e,c as a,o}from"./chunks/framework.C9SxlbOG.js";const f=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/compression.md","filePath":"api/05-performance/compression.md","lastUpdated":null}'),r={name:"api/05-performance/compression.md"};function t(s,n,c,p,i,m){return o(),a("div")}const _=e(r,[["render",t]]);export{f as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.lean.js b/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.lean.js new file mode 100644 index 000000000..41cd17f46 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_compression.md.fzQEzXTQ.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,o}from"./chunks/framework.C9SxlbOG.js";const f=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/compression.md","filePath":"api/05-performance/compression.md","lastUpdated":null}'),r={name:"api/05-performance/compression.md"};function t(s,n,c,p,i,m){return o(),a("div")}const _=e(r,[["render",t]]);export{f as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.js b/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.js new file mode 100644 index 000000000..f49c63b4d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/http-caching.md","filePath":"api/05-performance/http-caching.md","lastUpdated":null}'),c={name:"api/05-performance/http-caching.md"};function r(n,p,o,i,s,d){return a(),e("div")}const f=t(c,[["render",r]]);export{m as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.lean.js b/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.lean.js new file mode 100644 index 000000000..f49c63b4d --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_http-caching.md.B8lPsk0r.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/http-caching.md","filePath":"api/05-performance/http-caching.md","lastUpdated":null}'),c={name:"api/05-performance/http-caching.md"};function r(n,p,o,i,s,d){return a(),e("div")}const f=t(c,[["render",r]]);export{m as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.js b/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.js new file mode 100644 index 000000000..07e229a75 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.js @@ -0,0 +1 @@ +import{_ as e,c as a,o as t}from"./chunks/framework.C9SxlbOG.js";const f=JSON.parse('{"title":"Performance","description":"","frontmatter":{"title":"Performance"},"headers":[],"relativePath":"api/05-performance/index.md","filePath":"api/05-performance/index.md","lastUpdated":null}'),r={name:"api/05-performance/index.md"};function n(o,c,i,p,s,d){return t(),a("div")}const l=e(r,[["render",n]]);export{f as __pageData,l as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.lean.js b/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.lean.js new file mode 100644 index 000000000..07e229a75 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_index.md.BSpkGXQl.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,o as t}from"./chunks/framework.C9SxlbOG.js";const f=JSON.parse('{"title":"Performance","description":"","frontmatter":{"title":"Performance"},"headers":[],"relativePath":"api/05-performance/index.md","filePath":"api/05-performance/index.md","lastUpdated":null}'),r={name:"api/05-performance/index.md"};function n(o,c,i,p,s,d){return t(),a("div")}const l=e(r,[["render",n]]);export{f as __pageData,l as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.js b/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.js new file mode 100644 index 000000000..64d02a2d9 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.js @@ -0,0 +1,25 @@ +import{_ as i,c as t,o as a,ag as e}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Instrumentation","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/instrumentation.md","filePath":"api/05-performance/instrumentation.md","lastUpdated":null}'),n={name:"api/05-performance/instrumentation.md"};function l(o,s,r,h,p,d){return a(),t("div",null,s[0]||(s[0]=[e(`

Instrumentation

Instrumentation is the process of collecting and storing data about the performance of your application. This data can be used to identify performance bottlenecks, monitor the health of your application, and make informed decisions about how to improve performance.

prom-client is a popular library for instrumenting Node.js applications with Prometheus metrics. It provides a simple and efficient way to collect metrics and expose them for monitoring and alerting.

Metrics Middleware

The metricsMiddleware is a middleware function provided by the express-prom-bundle library. It is responsible for collecting HTTP request metrics, such as response times, status codes, and request paths. These metrics are exposed in a format compatible with Prometheus, enabling easy integration with monitoring systems.

Configuration

The metricsMiddleware is configured in the core/metrics.js file. Key configurations include:

Metrics Collected

The middleware collects the following metrics:

plaintext
# HELP http_request_duration_seconds duration histogram of http responses labeled with: status_code, method, path
+# TYPE http_request_duration_seconds histogram
+http_request_duration_seconds_bucket{le="0.03",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="0.1",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="0.3",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="1",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="3",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="10",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="30",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_bucket{le="+Inf",status_code="2xx",method="GET",path="/datasets/"} 2
+http_request_duration_seconds_sum{status_code="2xx",method="GET",path="/datasets/"} 0.038533292
+http_request_duration_seconds_count{status_code="2xx",method="GET",path="/datasets/"} 2

Default metrics about node.js process are also collected. To view all metrics:

bash
api> curl locahost:9999/metrics > metrics.prom

Usage

The middleware is added to the Express application in app.js:

javascript
const { metricsMiddleware } = require('./core/metrics');
+app.use(metricsMiddleware);

This ensures that all incoming requests are automatically instrumented.

Custom Metrics

core/metrics.js

javascript
const authFailures = new client.Counter({
+  name: 'auth_failures_total',
+  help: 'Total number of failed authentication attempts',
+  labelNames: ['auth_method', 'reason', 'client_id'],
+});

Add your custom metric to the metrics.js file. This example creates a counter metric to track the total number of failed authentication attempts. You can define custom labels to provide additional context for the metric. This metric will be automatically registered and sent to the Prometheus server on every scrape.

Include the Metric in Your Code

services/authService.js

javascript
const metrics = require('../core/metrics');
+
+function authenticateUser(username, password) {
+  if (!isValidCredentials(username, password)) {
+    metrics.authFailures.inc({ auth_method: 'password', reason: 'invalid_credentials', client_id: 'web' });
+    throw new Error('Invalid credentials');
+  }
+}

Clustered Metrics Aggregation

In a clustered environment, metrics from all worker processes are aggregated in the master process. This ensures that metrics are consistent and accessible from a single endpoint.

Implementation

The aggregation is implemented in cluster.js:

Example configuration in cluster.js:

javascript
const promBundle = require('express-prom-bundle');
+metricsApp.use('/metrics', promBundle.clusterMetrics());

Without this setup, metrics would be fragmented across worker processes, making it difficult to monitor the entire system.

Benefits

`,35)]))}const u=i(n,[["render",l]]);export{k as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.lean.js b/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.lean.js new file mode 100644 index 000000000..869069504 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_instrumentation.md.DG3ieIXj.lean.js @@ -0,0 +1 @@ +import{_ as i,c as t,o as a,ag as e}from"./chunks/framework.C9SxlbOG.js";const k=JSON.parse('{"title":"Instrumentation","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/instrumentation.md","filePath":"api/05-performance/instrumentation.md","lastUpdated":null}'),n={name:"api/05-performance/instrumentation.md"};function l(o,s,r,h,p,d){return a(),t("div",null,s[0]||(s[0]=[e("",35)]))}const u=i(n,[["render",l]]);export{k as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.js b/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.js new file mode 100644 index 000000000..f91553669 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.js @@ -0,0 +1 @@ +import{_ as o,c as s,o as t,ag as r}from"./chunks/framework.C9SxlbOG.js";const u=JSON.parse('{"title":"Node.js Metrics Documentation","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/nodejs_metrics.md","filePath":"api/05-performance/nodejs_metrics.md","lastUpdated":null}'),a={name:"api/05-performance/nodejs_metrics.md"};function i(n,e,c,d,l,p){return t(),s("div",null,e[0]||(e[0]=[r('

Node.js Metrics Documentation

This document provides an overview of the performance metrics collected by the application.

nodejs_gc_duration_seconds

Type: Histogram
Source: Derived from perf_hooks.PerformanceObserver (Node.js Documentation)

Description:
Measures the duration of garbage collection (GC) events, categorized by type: major, minor, incremental, or weakcb.


nodejs_active_resources

Type: Gauge
Source: Derived from process.getActiveResourcesInfo()

Description:
Tracks the number of active resources currently keeping the event loop alive.

Usage:


Deprecated Metrics: nodejs_active_requests and nodejs_active_handles

Status: Deprecated in Node.js v23 (Deprecation Notice)
Replacement: Use nodejs_active_resources and nodejs_active_resources_total.


process_start_time_seconds

Type: Gauge
Source: Derived from process.uptime()

Description:
Tracks the number of seconds since the Node.js process started.


process_open_fds

Type: Gauge
Source: Derived from fs.readdirSync('/proc/self/fd').length - 1

Description:
Monitors the number of open file descriptors used by the Node.js process.

Usage:

Caveats:


process_max_fds

Type: Gauge
Source: Derived from /proc/self/limits

Description:
Represents the maximum number of file descriptors the process can open, as configured by the OS.

Usage:


process_cpu_seconds_total

Type: Counter
Source: Derived from @opentelemetry/api

Description:
Tracks the total CPU time (user + system) consumed by the process.
TODO: Add implementation details.


osMemoryHeapLinux

Type: Gauge
Source: Derived from /proc/self/status

Metrics:

Usage:


heapSpacesSizeAndUsed

Type: Gauge
Source: Derived from v8.getHeapSpaceStatistics()

Metrics:

Heap Spaces:

Usage:


heapSizeAndUsed

Type: Gauge
Source: Derived from process.memoryUsage()

Metrics:

Usage:

Caveats:


Here's a refined and professional version of your documentation:


eventLoopLag

Type: Gauge

Metrics

Description

eventLoopLag measures the delay between scheduling a timer (setImmediate) and its corresponding callback execution. This provides insight into how long the event loop is blocked by other operations, serving as an indicator of potential performance bottlenecks in a Node.js application.

Computation of nodejs_eventloop_lag_seconds

The nodejs_eventloop_lag_seconds metric is computed as follows (reference):

  1. A Gauge is created in eventLoopLag.js with a custom collect method.
  2. When metrics are requested, the collect method is invoked, capturing a high-resolution timestamp. This occurs at the start of the metrics collection process.
  3. The method then schedules another measurement using setImmediate. Since this executes in the next event loop iteration, it occurs after metrics collection and reporting have completed.
  4. As the collect method does not return a promise, the computed value is only recorded in the subsequent metrics collection cycle.

This means the metric measures the delay from the start of metrics collection until the next event loop iteration. However, it is not updated in real time and only reflects values from the previous collection cycle.

Computation of Other Metrics

Other event loop lag metrics (e.g., min, max, mean, percentiles) are derived from perf_hooks.monitorEventLoopDelay().

Use Cases

Limitations

',82)]))}const m=o(a,[["render",i]]);export{u as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.lean.js b/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.lean.js new file mode 100644 index 000000000..85fe5c899 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_05-performance_nodejs_metrics.md.BOsU2dK9.lean.js @@ -0,0 +1 @@ +import{_ as o,c as s,o as t,ag as r}from"./chunks/framework.C9SxlbOG.js";const u=JSON.parse('{"title":"Node.js Metrics Documentation","description":"","frontmatter":{},"headers":[],"relativePath":"api/05-performance/nodejs_metrics.md","filePath":"api/05-performance/nodejs_metrics.md","lastUpdated":null}'),a={name:"api/05-performance/nodejs_metrics.md"};function i(n,e,c,d,l,p){return t(),s("div",null,e[0]||(e[0]=[r("",82)]))}const m=o(a,[["render",i]]);export{u as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.js b/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.js new file mode 100644 index 000000000..342f56aae --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/06-integrations/api-clients.md","filePath":"api/06-integrations/api-clients.md","lastUpdated":null}'),i={name:"api/06-integrations/api-clients.md"};function n(s,r,o,p,c,l){return a(),e("div")}const m=t(i,[["render",n]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.lean.js b/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.lean.js new file mode 100644 index 000000000..342f56aae --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_api-clients.md.Dsrc0cL2.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/06-integrations/api-clients.md","filePath":"api/06-integrations/api-clients.md","lastUpdated":null}'),i={name:"api/06-integrations/api-clients.md"};function n(s,r,o,p,c,l){return a(),e("div")}const m=t(i,[["render",n]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.js b/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.js new file mode 100644 index 000000000..d810f5538 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Integrations","description":"","frontmatter":{"title":"Integrations"},"headers":[],"relativePath":"api/06-integrations/index.md","filePath":"api/06-integrations/index.md","lastUpdated":null}'),n={name:"api/06-integrations/index.md"};function i(r,o,s,d,c,p){return a(),e("div")}const m=t(n,[["render",i]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.lean.js b/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.lean.js new file mode 100644 index 000000000..d810f5538 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_index.md.DTDGGRYD.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Integrations","description":"","frontmatter":{"title":"Integrations"},"headers":[],"relativePath":"api/06-integrations/index.md","filePath":"api/06-integrations/index.md","lastUpdated":null}'),n={name:"api/06-integrations/index.md"};function i(r,o,s,d,c,p){return a(),e("div")}const m=t(n,[["render",i]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.js b/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.js new file mode 100644 index 000000000..c270b8ecd --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.js @@ -0,0 +1 @@ +import{_ as t,c as o,o as a,ag as n}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/06-integrations/swagger-openapi.md","filePath":"api/06-integrations/swagger-openapi.md","lastUpdated":null}'),i={name:"api/06-integrations/swagger-openapi.md"};function r(s,e,d,c,p,u){return a(),o("div",null,e[0]||(e[0]=[n('

OpenAPI Documentation

Auto-generated OpenAPI documentation for the API routes.

  1. Add // #swagger.tags = ['<sub-router>'] comment to the code of the route handler and replace sub-router with a valid name that describes the family of routes (ex: User, Dataset, etc).
  2. Run npm run swagger-autogen to generate the documentation.
  3. Visit http://<api-host>:<api-port>/docs

Files:

Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284

',6)]))}const m=t(i,[["render",r]]);export{g as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.lean.js b/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.lean.js new file mode 100644 index 000000000..a89670d72 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_06-integrations_swagger-openapi.md.D81zKQ3O.lean.js @@ -0,0 +1 @@ +import{_ as t,c as o,o as a,ag as n}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/06-integrations/swagger-openapi.md","filePath":"api/06-integrations/swagger-openapi.md","lastUpdated":null}'),i={name:"api/06-integrations/swagger-openapi.md"};function r(s,e,d,c,p,u){return a(),o("div",null,e[0]||(e[0]=[n("",6)]))}const m=t(i,[["render",r]]);export{g as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.js b/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.js new file mode 100644 index 000000000..dd9d1914c --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/auto-reload-server.md","filePath":"api/07-development/auto-reload-server.md","lastUpdated":null}'),r={name:"api/07-development/auto-reload-server.md"};function o(s,d,n,p,l,c){return a(),t("div")}const _=e(r,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.lean.js b/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.lean.js new file mode 100644 index 000000000..dd9d1914c --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_auto-reload-server.md.BS8klrx_.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/auto-reload-server.md","filePath":"api/07-development/auto-reload-server.md","lastUpdated":null}'),r={name:"api/07-development/auto-reload-server.md"};function o(s,d,n,p,l,c){return a(),t("div")}const _=e(r,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.js b/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.js new file mode 100644 index 000000000..d4dac5821 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/db-seed-data.md","filePath":"api/07-development/db-seed-data.md","lastUpdated":null}'),d={name:"api/07-development/db-seed-data.md"};function o(s,n,p,r,c,i){return a(),t("div")}const _=e(d,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.lean.js b/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.lean.js new file mode 100644 index 000000000..d4dac5821 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_db-seed-data.md.DqPXHVVw.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/db-seed-data.md","filePath":"api/07-development/db-seed-data.md","lastUpdated":null}'),d={name:"api/07-development/db-seed-data.md"};function o(s,n,p,r,c,i){return a(),t("div")}const _=e(d,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.js b/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.js new file mode 100644 index 000000000..9fd139361 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Development","description":"","frontmatter":{"title":"Development"},"headers":[],"relativePath":"api/07-development/index.md","filePath":"api/07-development/index.md","lastUpdated":null}'),n={name:"api/07-development/index.md"};function o(p,d,i,r,s,l){return a(),t("div")}const _=e(n,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.lean.js b/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.lean.js new file mode 100644 index 000000000..9fd139361 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_index.md.DLjX-kjo.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Development","description":"","frontmatter":{"title":"Development"},"headers":[],"relativePath":"api/07-development/index.md","filePath":"api/07-development/index.md","lastUpdated":null}'),n={name:"api/07-development/index.md"};function o(p,d,i,r,s,l){return a(),t("div")}const _=e(n,[["render",o]]);export{m as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.js b/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.js new file mode 100644 index 000000000..0457b5241 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.js @@ -0,0 +1 @@ +import{_ as a,c as i,o as l,j as e,a as n}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Linting","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/linting.md","filePath":"api/07-development/linting.md","lastUpdated":null}'),r={name:"api/07-development/linting.md"};function s(o,t,d,p,c,f){return l(),i("div",null,t[0]||(t[0]=[e("h1",{id:"linting",tabindex:"-1"},[n("Linting "),e("a",{class:"header-anchor",href:"#linting","aria-label":'Permalink to "Linting"'},"​")],-1),e("p",null,[n("Eslint rules inherited from "),e("code",null,"eslint-config-airbnb-base"),n(" and "),e("a",{href:"https://www.npmjs.com/package/eslint-plugin-lodash-fp",target:"_blank",rel:"noreferrer"},"Lodash-fp ruleset"),n(".")],-1),e("p",null,"Consistent Coding styles with editorconfig",-1)]))}const h=a(r,[["render",s]]);export{m as __pageData,h as default}; diff --git a/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.lean.js b/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.lean.js new file mode 100644 index 000000000..0457b5241 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_07-development_linting.md.ScDHITP5.lean.js @@ -0,0 +1 @@ +import{_ as a,c as i,o as l,j as e,a as n}from"./chunks/framework.C9SxlbOG.js";const m=JSON.parse('{"title":"Linting","description":"","frontmatter":{},"headers":[],"relativePath":"api/07-development/linting.md","filePath":"api/07-development/linting.md","lastUpdated":null}'),r={name:"api/07-development/linting.md"};function s(o,t,d,p,c,f){return l(),i("div",null,t[0]||(t[0]=[e("h1",{id:"linting",tabindex:"-1"},[n("Linting "),e("a",{class:"header-anchor",href:"#linting","aria-label":'Permalink to "Linting"'},"​")],-1),e("p",null,[n("Eslint rules inherited from "),e("code",null,"eslint-config-airbnb-base"),n(" and "),e("a",{href:"https://www.npmjs.com/package/eslint-plugin-lodash-fp",target:"_blank",rel:"noreferrer"},"Lodash-fp ruleset"),n(".")],-1),e("p",null,"Consistent Coding styles with editorconfig",-1)]))}const h=a(r,[["render",s]]);export{m as __pageData,h as default}; diff --git a/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.js b/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.js new file mode 100644 index 000000000..59c100e44 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"API","description":"","frontmatter":{"title":"API","order":3},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md","lastUpdated":null}'),n={name:"api/index.md"};function r(i,o,d,s,c,p){return a(),t("div")}const m=e(n,[["render",r]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.lean.js b/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.lean.js new file mode 100644 index 000000000..59c100e44 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_index.md.DLHSSN37.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"API","description":"","frontmatter":{"title":"API","order":3},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md","lastUpdated":null}'),n={name:"api/index.md"};function r(i,o,d,s,c,p){return a(),t("div")}const m=e(n,[["render",r]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.js b/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.js deleted file mode 100644 index 84c268912..000000000 --- a/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.js +++ /dev/null @@ -1,131 +0,0 @@ -import{_ as s,c as i,o as a,V as e,a2 as n}from"./chunks/framework.MXVb71fM.js";const y=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md"}'),t={name:"api/index.md"},h=e(`

API

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

Running using docker

In the developement environment with docker this is the content of .env file

bash
NODE_ENV=docker
-DATABASE_PASSWORD='example'
-DATABASE_URL="postgresql://appuser:example@postgres:5432/app?schema=public"

From the project root run: docker compose up postgres api -d to start both the API server and the database

Running on host machine

Start a postgres db server on localhost and create a database app and user appuser (password: example) with write premissions to public schema. In this local environment, the content of .env file is:

bash
NODE_ENV=default
-DATABASE_PASSWORD='example'
-DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"

Run pnpm install and pnpm start to start the API server.

Features:

Developer Exprience

Production deployment:

Assumptions:

Typical request flow through the Express Server

  1. Express creates a request object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
  2. app.js - The body, query parameters, and cookies are parsed and converted to objects and req object is updated.
  3. app.js - CORS?
  4. Index Router - Intial routing is performed to select a sub-router to send the request to.
  5. Authentication - Validate JWT and attach user profile to req.user or send 401 error response*.
  6. AceessControl - Determine whether the requester has enough permissions to perform the desired operation on a particular resource and attach the permission object to req.permission or send 403 error response.
  7. Request Validation - Validate if the request query, params, or the body is in expected format or send 400 error.
  8. Async Handler - Envolpe the business logic route middleware to catch async error and propagate them to global error handler.
  9. Route Handler - Business Logic - create and send the response.
  10. Compression - Apply gZip compression to the response body.
  11. Express server sets some default headers and sends the response to the client.

When something goes wrong

  1. 404 Handler - Handle routing failures and send 404 error response.
  2. Prisma Not Found handler - Handle not found prisma errors and send 404 error response.
  3. Global error handler - Handle all other errors and send 500 error response.
  4. Compression - Apply gZip compression to the response body.
  5. Express server sets some default headers and sends the response to the client.

* For the routes that are registered before authenticate such as /health and /auth, this middleware not invoked.

Project Structure

files in src/

Error Handling

Source: Express Error Handling

Asynchronous Error Handler

asyncMiddleware in middleware/error.js

Usage: Wrap the route handler middleware with asyncHandler to produce a middleware funtion that can catch the asynchronous error and pass on to the default error handler.

javascript
const asyncHandler = require('../middleware/asyncHandler');
-
-router.get('/user', asyncHandler(async (req, res, next) => {
-    const user = await userService.findActiveUserBy('username', req.query.username);
-    res.json(user)
-}))

instead of

javascript
router.get('/user', async (req, res, next) => {
-  try {
-    const user = await userService.findActiveUserBy('username', req.query.username);
-    res.json(user)
-  } catch(err) {
-    next(err)
-  }
-})

The Default Error Handler

Custom Default Error Handler

404 handler

http-errors module:

javascript
err = createError(404, 'user not found')
-return next(err)
javascript
return next(createError.NotFound())

this will automatically set correct error message based on the constructor.

Prisma Not Found Error Handler

Prisma returns opaque error objects from the underlying query engine when DB queries fail. One such common error that must be handled everytime a DB query is made is the Not Found error.

HTTP semantics require that if a resource cannot be found either while retrieving, updating or deleting the response should be sent 404 status code. In order to achieve this, the errors from the prisma code have to be caught and analysed for the Not Found errors.

A typical example of handling a not found error and returning 404 response:

javascript
router.delete(
-  '/:username',
-  asyncHandler(async (req, res, next) => {
-    try{
-      const deletedUser = await userService.softDeleteUser(req.params.username);
-      res.json(deletedUser);
-    } catch(e) {
-      if (e instanceof Prisma.PrismaClientKnownRequestError) {
-        if (e?.meta?.cause?.includes('not found')) {
-          return next(createError.NotFound());
-        }
-      }
-      return next(e);
-    }
-  }),
-);

Here, the errors other than not found are propagated to the error handler by next(e).

The try-catch code can be refactored to a middleware that intercepts all errors before the Custom default error handler. The above code after refactoring looks like:

app.js

javascript
const { prismaNotFoundHandler } = require('./middleware/error');
-app.use(prismaNotFoundHandler);

routes/*.js

javascript
router.delete(
-  '/:username',
-  asyncHandler(async (req, res, next) => {
-    const deletedUser = await userService.softDeleteUser(req.params.username);
-    res.json(deletedUser);
-  }),
-);

Now, the routes' business logic code is cleaner and 404s are automatically when prisma ORM throws not found errors. If you want to send other HTTP status codes, intercept the prisma error in your route handler without propagating it.

Linting

Eslint rules inherited from eslint-config-airbnb-base and Lodash-fp ruleset.

Config

Uses config module - https://github.com/node-config/node-config

Configurations are stored in configuration files within your application, and can be overridden and extended by environment variables, command line parameters, or external sources.

config files: default.json, production.json, custom-environment-variables.json in ./config/ directory.

precdence of config: command line > environment > {NODE_ENV}.json > default.json

The properties to read and override from environment is defined in custom-environment-variables.json

Loading environment variables

javascript
require('dotenv-safe').config();

Authentication

The API uses IU CAS authnetication model.

All the routes and sub-routers added after the authenticate middleware in index router require authentication. The routes that do not require authentication such as auth routes are added before this.

The authenticate middleware, parses the Authorization header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as req.user

To add authentication to a single route:

javascript
const { authenticate } = require('../middleware/auth');
-
-router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => {
-  const user = await userService.findActiveUserBy('username', req.user.username);
-  // ...
-}))

Request Validation

Uses express-validator to validate if the request query, params, or the body is of the expected format and has acceptable values. This module helps to write declarative code that reduces repeatitive Spaghetti safety checking code inside the route handler. The route can now confidently presume that all of the required properties/keys of req.params, req.query, or req.body exist and have appropriate values and optional keys set to default values.

Using the validate higher order function, the error checking code is factored out from the route specific middleware functions.

javascript
app.post(
-  '/user',
-  body('username').isEmail(),
-  body('password').isLength({ min: 5 }),
-  (req, res) => {
-    // Finds the validation errors in this request and wraps them in an object with handy functions
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    User.create({
-      username: req.body.username,
-      password: req.body.password,
-    }).then(user => res.json(user));
-  },
-);

becomes

javascript
const validate = require('middleware/validators')
-app.post(
-  '/user',
-  validate([
-    body('username').isEmail(),
-    body('password').isLength({ min: 5 }),
-  ]),
-  asyncHandler(async (req, res) => {
-    const user = await User.create({
-      username: req.body.username,
-      password: req.body.password,
-    });
-    res.json(user);
-  },
-));

Authorization: Role Based Access Control

accesscontrol library is used to provide role based authorization to routes (resources).

Roles in this application:

Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in services/accesscontrols.js.

The goal of the accessControl middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource.

A simple use case:

Objective: Users with user role are only permitted to read and update thier own profile. Whereas, users with admin role can create new users, read & update any user's profile, and delete any user.

Role design:

javascript
{
-  admin: {
-    user: {
-      'create:any': ['*'],
-      'read:any': ['*'],
-      'update:any': ['*'],
-      'delete:any': ['*'],
-    },
-  },
-  user: {
-    user: {
-      'read:own': ['*'],
-      'update:own': ['*'],
-    },
-  },
-}

Permission check:

Code to check if the requester to is authorized to GET /users/dduck. This route is protected by authenticate middleware which attaches the requester profile to req.user if the token is valid.

javascript
const { authenticate } = require('../middleware/auth');
-
-router.get('/:username',
-  authenticate,
-  asyncHandler(async (req, res, next) => {
-    
-    const roles = req.user.roles;
-    const resourceOwner = req.params.username;
-    const requester = req.user?.username;
-
-    const permission = (requester === resourceOwner)
-        ? ac.can(roles).readOwn('user')
-        : ac.can(roles).readAny('user');
-    
-    if (!permission.granted) {
-      return next(createError(403)); // Forbidden
-    }
-    else {
-      const user = await userService.findActiveUserBy('username', req.params.username);
-      if (user) { return res.json(user); }
-      return next(createError.NotFound());
-    }
-  }),
-);

readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only user role and is requesting the profile of other users, the request will be denied.

AccessControl Middleware Usage

accessControl middleware is a generic function to handle authorization for any action or resource with optional ownership checking.

The above code can be written consicely with the help of accessControl middleware.

routes/*.js

javascript
// import middleware
-const { authenticate, accessControl } = require('../middleware/auth');
-
-// configre the middleware to authorize requests to user resource
-// resource ownership is checked by default
-// throws 403 if not authorized
-const isPermittedTo = accessControl('user');
-
-//
-router.get(
-  '/:username',
-  authenticate,
-  isPermittedTo('read', { checkOwnerShip: true }),
-  asyncHandler(async (req, res, next) => {
-    const user = await userService.findActiveUserBy('username', req.params.username);
-    if (user) { return res.json(user); }
-    return next(createError.NotFound());
-  }),
-);

Auto-generated Swagger Documentation

  1. Add // #swagger.tags = ['<sub-router>'] comment to the code of the route handler and replace sub-router with a valid name that describes the family of routes (ex: User, Dataset, etc).
  2. Run npm run swagger-autogen to generate the documentation.
  3. Visit http://<api-host>:<api-port>/docs

Files:

Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284

`,111),l=[h];function r(p,k,d,o,E,c){return a(),i("div",null,l)}const u=s(t,[["render",r]]);export{y as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.lean.js b/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.lean.js deleted file mode 100644 index 41db999e2..000000000 --- a/docs/.vitepress/dist/assets/api_index.md.IzjkZa69.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,c as i,o as a,V as e,a2 as n}from"./chunks/framework.MXVb71fM.js";const y=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md"}'),t={name:"api/index.md"},h=e("",111),l=[h];function r(p,k,d,o,E,c){return a(),i("div",null,l)}const u=s(t,[["render",r]]);export{y as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.js b/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.js new file mode 100644 index 000000000..07e2eae26 --- /dev/null +++ b/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.js @@ -0,0 +1 @@ +import{_ as t,c as r,o,ag as a}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/api/router-explained.png",s="/bioloop/docs/api/middleware-explained.png",g=JSON.parse('{"title":"Introduction","description":"","frontmatter":{"title":"Introduction","order":0},"headers":[],"relativePath":"api/introduction.md","filePath":"api/introduction.md","lastUpdated":null}'),n={name:"api/introduction.md"};function d(l,e,c,p,h,u){return o(),r("div",null,e[0]||(e[0]=[a('

Introduction

Developing robust and maintainable backend services requires a structured approach without unnecessary complexity. This framework extends Express.js by providing a set of utility functions that facilitate the development of production-ready applications. No additional abstractions have been introduced, ensuring that developers familiar with Express can use it without a learning curve. Essential features such as validation, authentication, logging, and metrics are integrated, offering a streamlined development experience. A fully configured development environment can be initialized with a single command, incorporating linting, hot reloading, and best-practice defaults.

Philosophy

Minimal Abstractions

Rather than introducing new layers of abstraction, the framework enhances Express through structured utility functions. Full control over request handling is maintained while offering built-in tools for middleware management, configuration, and error handling.

Comprehensive Feature Set

A variety of essential features required for modern web applications, including authentication, logging, and observability, have been integrated. This approach allows teams to focus on application logic rather than spending time configuring third-party packages.

Adherence to Best Practices

The framework is structured to promote modular and maintainable code. The use of Prisma for database management, OpenAPI documentation, and structured logging ensures consistency and maintainability across projects.

Optimized Developer Experience

A complete development environment can be set up using a single command through Docker Compose. Automated linting, nodemon-based reloads, and process clustering have been included to improve efficiency and reduce development overhead.

By emphasizing simplicity, flexibility, and adherence to best practices, this framework enables the development of scalable and maintainable Express applications while minimizing complexity.

Example

routes.index.js Router Explained

routes/resources.js Middleware Explained

Typical Request Flow Through the Express Server

StepComponentDescription
1Express ServerReceives an HTTP request
2MiddlewareParses request body, query, and cookies
3MiddlewareEnforces CORS policies
4RouterRoutes request to appropriate sub-router
5AuthenticationValidates JWT, attaches user to request
6Access ControlChecks permissions
7ValidationEnsures request format is correct
8Async Error HandlerWraps route handlers for error management
9Business LogicExecutes request-specific logic
10MiddlewareApplies gzip compression
11Express ServerSends response to client

Error Handling Steps:

StepComponentDescription
10404Handle routing failures
11Custom Error HandlersHandle specific errors
12Global Error HandlerHandle all other errors
13MiddlewareApplies gzip compression
14Express ServerSends response to client

Detailed Steps:

  1. Express creates a request object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
  2. src/app.js - The body, query parameters, and cookies are parsed and converted to objects, and the req object is updated.
  3. src/app.js - Apply CORS policies to handle cross-origin requests.
  4. Main Router (src/routers/index.js) - Initial routing is performed to select a sub-router to send the request to.
  5. Authentication - Validate JWT and attach the user profile to req.user or send a 401 error response*.
  6. Access Control - Determine whether the requester has sufficient permissions to perform the desired operation on a particular resource. Attach the permission object to req.permission or send a 403 error response.
  7. Request Validation - Validate if the request query, parameters, or body is in the expected format, or send a 400 error response.
  8. Async Handler - Wrap the business logic route middleware to catch asynchronous errors and propagate them to the global error handler.
  9. Route Handler - Business Logic - Execute the business logic and create the response.
  10. Compression - Apply gzip compression to the response body.
  11. Express server sets default headers and sends the response to the client.

When Something Goes Wrong

  1. 404 Handler - Handle routing failures and send a 404 error response.
  2. Custom Error Handlers - Handle prisma, assertions, axios errors and send appropriate error responses.
  3. Global Error Handler - Handle all other errors and send a 500 error response.
  4. Compression - Apply gzip compression to the response body.
  5. Express server sets default headers and sends the response to the client.

* For routes registered before the authenticate middleware, such as /health and /auth, this middleware is not invoked.

Project Structure

',26)]))}const f=t(n,[["render",d]]);export{g as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.lean.js b/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.lean.js new file mode 100644 index 000000000..a3a79f81a --- /dev/null +++ b/docs/.vitepress/dist/assets/api_introduction.md.DohIzYo2.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,o,ag as a}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/api/router-explained.png",s="/bioloop/docs/api/middleware-explained.png",g=JSON.parse('{"title":"Introduction","description":"","frontmatter":{"title":"Introduction","order":0},"headers":[],"relativePath":"api/introduction.md","filePath":"api/introduction.md","lastUpdated":null}'),n={name:"api/introduction.md"};function d(l,e,c,p,h,u){return o(),r("div",null,e[0]||(e[0]=[a("",26)]))}const f=t(n,[["render",d]]);export{g as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/app.CuKA9ZJJ.js b/docs/.vitepress/dist/assets/app.CuKA9ZJJ.js new file mode 100644 index 000000000..67fad7d61 --- /dev/null +++ b/docs/.vitepress/dist/assets/app.CuKA9ZJJ.js @@ -0,0 +1 @@ +import{t as p}from"./chunks/theme.BqY363-_.js";import{R as s,a2 as i,a3 as u,a4 as c,a5 as l,a6 as f,a7 as d,a8 as m,a9 as h,aa as g,ab as A,d as v,u as y,v as C,s as P,ac as b,ad as w,ae as R,af as E}from"./chunks/framework.C9SxlbOG.js";function r(e){if(e.extends){const a=r(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const n=r(p),S=v({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{P(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&b(),w(),R(),n.setup&&n.setup(),()=>E(n.Layout)}});async function T(){globalThis.__VITEPRESS__=!0;const e=_(),a=D();a.provide(u,e);const t=c(e.route);return a.provide(l,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),n.enhanceApp&&await n.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function D(){return A(S)}function _(){let e=s;return h(a=>{let t=g(a),o=null;return t&&(e&&(t=t.replace(/\.js$/,".lean.js")),o=import(t)),s&&(e=!1),o},n.NotFound)}s&&T().then(({app:e,router:a,data:t})=>{a.go().then(()=>{i(a.route,t.site),e.mount("#app")})});export{T as createApp}; diff --git a/docs/.vitepress/dist/assets/app.vr6w8nme.js b/docs/.vitepress/dist/assets/app.vr6w8nme.js deleted file mode 100644 index ddfafe579..000000000 --- a/docs/.vitepress/dist/assets/app.vr6w8nme.js +++ /dev/null @@ -1,7 +0,0 @@ -import{j as s,ac as p,ad as u,ae as l,af as c,ag as f,ah as d,ai as m,aj as h,ak as A,al as g,am as v,d as P,u as y,l as C,z as w,an as _,ao as E,ap as R,aq as b}from"./chunks/framework.MXVb71fM.js";import{t as j}from"./chunks/theme.aZowWMZT.js";function i(e){if(e.extends){const a=i(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const o=i(j),D=P({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{w(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&_(),E(),R(),o.setup&&o.setup(),()=>b(o.Layout)}});async function L(){const e=S(),a=O();a.provide(u,e);const t=l(e.route);return a.provide(c,t),a.component("Content",f),a.component("ClientOnly",d),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),o.enhanceApp&&await o.enhanceApp({app:a,router:e,siteData:m}),{app:a,router:e,data:t}}function O(){return h(D)}function S(){let e=s,a;return A(t=>{let n=g(t),r=null;return n&&(e&&(a=n),(e||a===n)&&(n=n.replace(/\.js$/,".lean.js")),r=v(()=>import(n),__vite__mapDeps([]))),s&&(e=!1),r},o.NotFound)}s&&L().then(({app:e,router:a,data:t})=>{a.go().then(()=>{p(a.route,t.site),e.mount("#app")})});export{L as createApp}; -function __vite__mapDeps(indexes) { - if (!__vite__mapDeps.viteFileDeps) { - __vite__mapDeps.viteFileDeps = [] - } - return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) -} diff --git a/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.js b/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.js deleted file mode 100644 index af3c060b7..000000000 --- a/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as t,c as r,o as a,m as e,a as c,a3 as s,a4 as o}from"./chunks/framework.MXVb71fM.js";const k=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"architecture.md","filePath":"architecture.md"}'),i={name:"architecture.md"},n=e("h2",{id:"architecture",tabindex:"-1"},[c("Architecture "),e("a",{class:"header-anchor",href:"#architecture","aria-label":'Permalink to "Architecture"'},"​")],-1),_=e("img",{src:s},null,-1),h=e("br",null,null,-1),l=e("img",{src:o},null,-1),d=[n,_,h,l];function u(m,p,f,x,$,b){return a(),r("div",null,d)}const B=t(i,[["render",u]]);export{k as __pageData,B as default}; diff --git a/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.lean.js b/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.lean.js deleted file mode 100644 index af3c060b7..000000000 --- a/docs/.vitepress/dist/assets/architecture.md.6bDaH79s.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as t,c as r,o as a,m as e,a as c,a3 as s,a4 as o}from"./chunks/framework.MXVb71fM.js";const k=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"architecture.md","filePath":"architecture.md"}'),i={name:"architecture.md"},n=e("h2",{id:"architecture",tabindex:"-1"},[c("Architecture "),e("a",{class:"header-anchor",href:"#architecture","aria-label":'Permalink to "Architecture"'},"​")],-1),_=e("img",{src:s},null,-1),h=e("br",null,null,-1),l=e("img",{src:o},null,-1),d=[n,_,h,l];function u(m,p,f,x,$,b){return a(),r("div",null,d)}const B=t(i,[["render",u]]);export{k as __pageData,B as default}; diff --git a/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.js b/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.js new file mode 100644 index 000000000..ca8a3b58e --- /dev/null +++ b/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.js @@ -0,0 +1 @@ +import{_ as r,c as a,o as c,j as e,a as o}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/architecture.png",n="/bioloop/docs/app-celery-communication-diagram.png",f=JSON.parse('{"title":"Architecture","description":"","frontmatter":{"title":"Architecture","order":0},"headers":[],"relativePath":"architecture.md","filePath":"architecture.md","lastUpdated":null}'),s={name:"architecture.md"};function l(u,t,d,p,h,m){return c(),a("div",null,t[0]||(t[0]=[e("h1",{id:"architecture",tabindex:"-1"},[o("Architecture "),e("a",{class:"header-anchor",href:"#architecture","aria-label":'Permalink to "Architecture"'},"​")],-1),e("img",{src:i},null,-1),e("br",null,null,-1),e("img",{src:n},null,-1)]))}const g=r(s,[["render",l]]);export{f as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.lean.js b/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.lean.js new file mode 100644 index 000000000..ca8a3b58e --- /dev/null +++ b/docs/.vitepress/dist/assets/architecture.md.PvRzkUtL.lean.js @@ -0,0 +1 @@ +import{_ as r,c as a,o as c,j as e,a as o}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/architecture.png",n="/bioloop/docs/app-celery-communication-diagram.png",f=JSON.parse('{"title":"Architecture","description":"","frontmatter":{"title":"Architecture","order":0},"headers":[],"relativePath":"architecture.md","filePath":"architecture.md","lastUpdated":null}'),s={name:"architecture.md"};function l(u,t,d,p,h,m){return c(),a("div",null,t[0]||(t[0]=[e("h1",{id:"architecture",tabindex:"-1"},[o("Architecture "),e("a",{class:"header-anchor",href:"#architecture","aria-label":'Permalink to "Architecture"'},"​")],-1),e("img",{src:i},null,-1),e("br",null,null,-1),e("img",{src:n},null,-1)]))}const g=r(s,[["render",l]]);export{f as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BA45FNPv.js b/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BA45FNPv.js new file mode 100644 index 000000000..601d6a1df --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.BA45FNPv.js @@ -0,0 +1 @@ +const e='{"documentCount":234,"nextId":234,"documentIds":{"0":"/bioloop/docs/api/01-core/configuration.html#configuration","1":"/bioloop/docs/api/01-core/configuration.html#purpose-of-the-configuration-system","2":"/bioloop/docs/api/01-core/configuration.html#how-it-fits-into-the-system","3":"/bioloop/docs/api/01-core/configuration.html#configuration-files","4":"/bioloop/docs/api/01-core/configuration.html#precedence-of-configuration","5":"/bioloop/docs/api/01-core/configuration.html#environment-variables","6":"/bioloop/docs/api/01-core/configuration.html#example-env-file","7":"/bioloop/docs/api/01-core/configuration.html#loading-environment-variables","8":"/bioloop/docs/api/01-core/configuration.html#step-by-step-instructions-for-usage","9":"/bioloop/docs/api/01-core/cluster.html#cluster-management","10":"/bioloop/docs/api/01-core/cluster.html#overview","11":"/bioloop/docs/api/01-core/cluster.html#configuration-options","12":"/bioloop/docs/api/01-core/cluster.html#example-usage","13":"/bioloop/docs/api/01-core/cluster.html#metrics-integration","14":"/bioloop/docs/api/01-core/error-handling.html#error-handling","15":"/bioloop/docs/api/01-core/error-handling.html#asynchronous-error-handler","16":"/bioloop/docs/api/01-core/error-handling.html#why-it-exists","17":"/bioloop/docs/api/01-core/error-handling.html#usage","18":"/bioloop/docs/api/01-core/error-handling.html#the-default-error-handler","19":"/bioloop/docs/api/01-core/error-handling.html#key-features","20":"/bioloop/docs/api/01-core/error-handling.html#custom-default-error-handler","21":"/bioloop/docs/api/01-core/error-handling.html#example","22":"/bioloop/docs/api/01-core/error-handling.html#custom-error-handlers","23":"/bioloop/docs/api/01-core/error-handling.html#_404-handler","24":"/bioloop/docs/api/01-core/error-handling.html#prisma-not-found-error-handler","25":"/bioloop/docs/api/01-core/error-handling.html#example-1","26":"/bioloop/docs/api/01-core/error-handling.html#prisma-constraint-violation-handler","27":"/bioloop/docs/api/01-core/error-handling.html#example-2","28":"/bioloop/docs/api/01-core/error-handling.html#assertion-error-handler","29":"/bioloop/docs/api/01-core/error-handling.html#example-3","30":"/bioloop/docs/api/01-core/error-handling.html#axios-error-handler","31":"/bioloop/docs/api/01-core/error-handling.html#example-4","32":"/bioloop/docs/api/01-core/error-handling.html#integration-into-the-system","33":"/bioloop/docs/api/01-core/error-handling.html#summary","34":"/bioloop/docs/api/01-core/cron-task-scheduling.html#scheduling-tasks","35":"/bioloop/docs/api/01-core/cron-task-scheduling.html#how-it-works","36":"/bioloop/docs/api/01-core/cron-task-scheduling.html#usage-instructions","37":"/bioloop/docs/api/01-core/cron-task-scheduling.html#best-practices","38":"/bioloop/docs/api/01-core/lifecycle-hooks.html#lifecycle-hooks","39":"/bioloop/docs/api/01-core/lifecycle-hooks.html#location-of-lifecycle-hooks","40":"/bioloop/docs/api/01-core/lifecycle-hooks.html#etiquette-for-editing-updating-lifecycle-hooks","41":"/bioloop/docs/api/01-core/lifecycle-hooks.html#hooks-overview","42":"/bioloop/docs/api/01-core/lifecycle-hooks.html#beforeapplicationfork","43":"/bioloop/docs/api/01-core/lifecycle-hooks.html#onapplicationbootstrap","44":"/bioloop/docs/api/01-core/lifecycle-hooks.html#beforeapplicationshutdown","45":"/bioloop/docs/api/01-core/lifecycle-hooks.html#onapplicationshutdown","46":"/bioloop/docs/api/01-core/lifecycle-hooks.html#integration","47":"/bioloop/docs/api/01-core/validation.html#request-validation","48":"/bioloop/docs/api/01-core/validation.html#benefits","49":"/bioloop/docs/api/01-core/validation.html#usage-instructions","50":"/bioloop/docs/api/01-core/validation.html#comparison-with-different-approaches","51":"/bioloop/docs/api/01-core/validation.html#manual-validation","52":"/bioloop/docs/api/01-core/validation.html#using-express-validator","53":"/bioloop/docs/api/01-core/validation.html#using-validate","54":"/bioloop/docs/api/01-core/validation.html#explanation-of-the-code","55":"/bioloop/docs/api/01-core/validation.html#more-examples","56":"/bioloop/docs/api/02-data/prisma.html#orm","57":"/bioloop/docs/api/02-data/prisma.html#gotchas","58":"/bioloop/docs/api/02-data/prisma.html#auto-timestamp","59":"/bioloop/docs/api/02-data/prisma.html#aliasing-and-excluding-columns","60":"/bioloop/docs/api/02-data/prisma.html#avoid-select-queries","61":"/bioloop/docs/api/02-data/prisma.html#sorting-with-nulls-last","62":"/bioloop/docs/api/02-data/prisma.html#logging-queries","63":"/bioloop/docs/api/03-security/authentication.html#authentication","64":"/bioloop/docs/api/03-security/authorization.html#authorization","65":"/bioloop/docs/api/03-security/authorization.html#a-simple-use-case","66":"/bioloop/docs/api/03-security/authorization.html#accesscontrol-middleware-usage","67":"/bioloop/docs/api/05-performance/nodejs_metrics.html#node-js-metrics-documentation","68":"/bioloop/docs/api/05-performance/nodejs_metrics.html#nodejs-gc-duration-seconds","69":"/bioloop/docs/api/05-performance/nodejs_metrics.html#nodejs-active-resources","70":"/bioloop/docs/api/05-performance/nodejs_metrics.html#deprecated-metrics-nodejs-active-requests-and-nodejs-active-handles","71":"/bioloop/docs/api/05-performance/nodejs_metrics.html#process-start-time-seconds","72":"/bioloop/docs/api/05-performance/nodejs_metrics.html#process-open-fds","73":"/bioloop/docs/api/05-performance/nodejs_metrics.html#process-max-fds","74":"/bioloop/docs/api/05-performance/nodejs_metrics.html#process-cpu-seconds-total","75":"/bioloop/docs/api/05-performance/nodejs_metrics.html#osmemoryheaplinux","76":"/bioloop/docs/api/05-performance/nodejs_metrics.html#heapspacessizeandused","77":"/bioloop/docs/api/05-performance/nodejs_metrics.html#heapsizeandused","78":"/bioloop/docs/api/05-performance/nodejs_metrics.html#eventlooplag","79":"/bioloop/docs/api/05-performance/nodejs_metrics.html#metrics","80":"/bioloop/docs/api/05-performance/nodejs_metrics.html#description","81":"/bioloop/docs/api/05-performance/nodejs_metrics.html#computation-of-nodejs-eventloop-lag-seconds","82":"/bioloop/docs/api/05-performance/nodejs_metrics.html#computation-of-other-metrics","83":"/bioloop/docs/api/05-performance/nodejs_metrics.html#use-cases","84":"/bioloop/docs/api/05-performance/nodejs_metrics.html#limitations","85":"/bioloop/docs/api/05-performance/instrumentation.html#instrumentation","86":"/bioloop/docs/api/05-performance/instrumentation.html#metrics-middleware","87":"/bioloop/docs/api/05-performance/instrumentation.html#configuration","88":"/bioloop/docs/api/05-performance/instrumentation.html#metrics-collected","89":"/bioloop/docs/api/05-performance/instrumentation.html#usage","90":"/bioloop/docs/api/05-performance/instrumentation.html#custom-metrics","91":"/bioloop/docs/api/05-performance/instrumentation.html#include-the-metric-in-your-code","92":"/bioloop/docs/api/05-performance/instrumentation.html#clustered-metrics-aggregation","93":"/bioloop/docs/api/05-performance/instrumentation.html#implementation","94":"/bioloop/docs/api/05-performance/instrumentation.html#benefits","95":"/bioloop/docs/api/06-integrations/swagger-openapi.html#openapi-documentation","96":"/bioloop/docs/api/07-development/linting.html#linting","97":"/bioloop/docs/architecture.html#architecture","98":"/bioloop/docs/api/introduction.html#introduction","99":"/bioloop/docs/api/introduction.html#philosophy","100":"/bioloop/docs/api/introduction.html#minimal-abstractions","101":"/bioloop/docs/api/introduction.html#comprehensive-feature-set","102":"/bioloop/docs/api/introduction.html#adherence-to-best-practices","103":"/bioloop/docs/api/introduction.html#optimized-developer-experience","104":"/bioloop/docs/api/introduction.html#example","105":"/bioloop/docs/api/introduction.html#typical-request-flow-through-the-express-server","106":"/bioloop/docs/api/introduction.html#when-something-goes-wrong","107":"/bioloop/docs/api/introduction.html#project-structure","108":"/bioloop/docs/installation/install-docker.html#installation-guide","109":"/bioloop/docs/installation/install-docker.html#prerequisites","110":"/bioloop/docs/installation/install-docker.html#quick-start","111":"/bioloop/docs/installation/install-docker.html#development-setup-details","112":"/bioloop/docs/installation/install-docker.html#configuration","113":"/bioloop/docs/installation/install-docker.html#environment-variables","114":"/bioloop/docs/installation/install-docker.html#docker-configuration","115":"/bioloop/docs/installation/install-docker.html#database-setup","116":"/bioloop/docs/installation/install-docker.html#common-operations","117":"/bioloop/docs/installation/install-docker.html#starting-services","118":"/bioloop/docs/installation/install-docker.html#checking-status","119":"/bioloop/docs/installation/install-docker.html#stopping-services","120":"/bioloop/docs/installation/install-docker.html#development-tools","121":"/bioloop/docs/installation/install-docker.html#code-linting","122":"/bioloop/docs/installation/install-docker.html#testing","123":"/bioloop/docs/installation/install-docker.html#queue-system","124":"/bioloop/docs/installation/install-docker.html#troubleshooting-guide","125":"/bioloop/docs/installation/install-docker.html#common-issues","126":"/bioloop/docs/installation/install-docker.html#development-tips","127":"/bioloop/docs/metrics.html#metrics-and-monitoring","128":"/bioloop/docs/metrics.html#prometheus","129":"/bioloop/docs/metrics.html#configuration","130":"/bioloop/docs/metrics.html#without-prometheus","131":"/bioloop/docs/metrics.html#integration","132":"/bioloop/docs/metrics.html#grafana","133":"/bioloop/docs/metrics.html#features","134":"/bioloop/docs/metrics.html#configuration-1","135":"/bioloop/docs/metrics.html#authentication-and-authorization","136":"/bioloop/docs/metrics.html#integration-1","137":"/bioloop/docs/metrics.html#how-this-setup-helps","138":"/bioloop/docs/metrics.html#usage-instructions","139":"/bioloop/docs/installation/install-local.html#local-installation-guide","140":"/bioloop/docs/installation/install-local.html#overview","141":"/bioloop/docs/installation/install-local.html#steps-to-setup-api-and-run-natively-on-development-machine-not-using-docker","142":"/bioloop/docs/installation/install-local.html#steps-to-setup-ui-and-run-natively-on-development-machine-not-using-docker","143":"/bioloop/docs/installation/install-local.html#set-up-workers-locally","144":"/bioloop/docs/installation/install-local.html#setup-a-test-instance-of-workers-in-colo-nodes","145":"/bioloop/docs/ui/auth_explained.html#auth-explained","146":"/bioloop/docs/ui/auth_explained.html#objectives-for-auth-module","147":"/bioloop/docs/ui/auth_explained.html#overview-of-auth-flow","148":"/bioloop/docs/ui/auth_explained.html#vue-app-code-execution-order","149":"/bioloop/docs/ui/auth_explained.html#route-navigation-guard-logic","150":"/bioloop/docs/ui/auth_explained.html#login-code-flow-before-iu-login","151":"/bioloop/docs/ui/auth_explained.html#login-code-flow-after-iu-login","152":"/bioloop/docs/ui/auth_explained.html#code-flow-for-a-revisiting-logged-in-user","153":"/bioloop/docs/ui/auth_explained.html#code-flow-for-token-refresh","154":"/bioloop/docs/ui/auth_explained.html#cache-invalidation","155":"/bioloop/docs/secure_download.html#secure-download","156":"/bioloop/docs/secure_download.html#table-of-contents","157":"/bioloop/docs/secure_download.html#_1-introduction","158":"/bioloop/docs/secure_download.html#_2-requirements-and-limitations","159":"/bioloop/docs/secure_download.html#_2-1-limitations","160":"/bioloop/docs/secure_download.html#_3-staging-the-dataset","161":"/bioloop/docs/secure_download.html#uuid-generation","162":"/bioloop/docs/secure_download.html#_4-access-control","163":"/bioloop/docs/secure_download.html#_5-architecture-overview","164":"/bioloop/docs/secure_download.html#_6-downloading-a-dataset-file","165":"/bioloop/docs/template.html#create-a-repository","166":"/bioloop/docs/ui/overview.html#ui-overview","167":"/bioloop/docs/ui/overview.html#getting-started","168":"/bioloop/docs/ui/overview.html#running-using-docker","169":"/bioloop/docs/ui/overview.html#running-on-host-machine","170":"/bioloop/docs/ui/overview.html#features","171":"/bioloop/docs/ui/overview.html#icons","172":"/bioloop/docs/ui/overview.html#colors","173":"/bioloop/docs/ui/overview.html#notable-vuestic-classes","174":"/bioloop/docs/ui/overview.html#configuration","175":"/bioloop/docs/ui/overview.html#constants","176":"/bioloop/docs/ui/overview.html#configuration-vs-constants","177":"/bioloop/docs/ui/overview.html#authentication","178":"/bioloop/docs/ui/overview.html#authentication-controls-on-router","179":"/bioloop/docs/ui/overview.html#utility-components","180":"/bioloop/docs/ui/overview.html#coding-conventions","181":"/bioloop/docs/ui/overview.html#adding-additional-fonts","182":"/bioloop/docs/ui/overview.html#dates-and-times","183":"/bioloop/docs/ui/overview.html#feature-flags","184":"/bioloop/docs/ui/overview.html#navigational-breadcrumbs","185":"/bioloop/docs/ui/overview.html#http-api-error-handling-and-notifications","186":"/bioloop/docs/ui/util_components.html#utility-components","187":"/bioloop/docs/ui/util_components.html#autocomplete","188":"/bioloop/docs/ui/util_components.html#basic-usage","189":"/bioloop/docs/ui/util_components.html#with-slots","190":"/bioloop/docs/ui/util_components.html#async","191":"/bioloop/docs/ui/util_components.html#props","192":"/bioloop/docs/ui/util_components.html#events","193":"/bioloop/docs/ui/util_components.html#slots","194":"/bioloop/docs/ui/util_components.html#searchandselect","195":"/bioloop/docs/ui/util_components.html#basic-usage-1","196":"/bioloop/docs/ui/util_components.html#filters","197":"/bioloop/docs/ui/util_components.html#formatting-and-slots","198":"/bioloop/docs/ui/util_components.html#notes","199":"/bioloop/docs/ui/util_components.html#props-1","200":"/bioloop/docs/ui/util_components.html#slots-1","201":"/bioloop/docs/ui/util_components.html#maybe","202":"/bioloop/docs/ui/util_components.html#props-2","203":"/bioloop/docs/ui/util_components.html#copytext","204":"/bioloop/docs/ui/util_components.html#props-3","205":"/bioloop/docs/ui/util_components.html#binarystatuschip","206":"/bioloop/docs/ui/util_components.html#props-4","207":"/bioloop/docs/ui/util_components.html#envalert","208":"/bioloop/docs/ui/util_components.html#props-5","209":"/bioloop/docs/ui/util_components.html#usequerypersistence-composable","210":"/bioloop/docs/upload.html#upload-architecture","211":"/bioloop/docs/upload.html#_1-introduction","212":"/bioloop/docs/upload.html#_2-requirements-and-limitations","213":"/bioloop/docs/upload.html#_3-architecture-overview","214":"/bioloop/docs/upload.html#_4-the-upload","215":"/bioloop/docs/upload.html#_4-1-logging","216":"/bioloop/docs/upload.html#_4-2-steps","217":"/bioloop/docs/upload.html#_4-3-directory-structure","218":"/bioloop/docs/upload.html#_4-4-access-control","219":"/bioloop/docs/upload.html#example","220":"/bioloop/docs/upload.html#_4-5-status","221":"/bioloop/docs/upload.html#_5-processing","222":"/bioloop/docs/upload.html#_6-data-integrity","223":"/bioloop/docs/upload.html#_7-retry","224":"/bioloop/docs/worker/overview.html#worker-overview","225":"/bioloop/docs/worker/overview.html#coding-guidelines","226":"/bioloop/docs/worker/overview.html#hierarchical-config","227":"/bioloop/docs/worker/overview.html#celery-config","228":"/bioloop/docs/worker/overview.html#code-organization","229":"/bioloop/docs/worker/overview.html#parallel-tasks-limit","230":"/bioloop/docs/worker/overview.html#hot-module-replacement","231":"/bioloop/docs/worker/overview.html#deployment","232":"/bioloop/docs/worker/overview.html#testing-with-workers-running-on-local-machine","233":"/bioloop/docs/worker/overview.html#testing-with-workers-running-on-colo-node-and-rhythm-api"},"fieldIds":{"title":0,"titles":1,"text":2},"fieldLength":{"0":[1,1,36],"1":[5,1,69],"2":[6,1,53],"3":[2,1,27],"4":[3,2,38],"5":[2,1,31],"6":[3,3,16],"7":[3,3,28],"8":[5,1,78],"9":[2,1,83],"10":[1,2,33],"11":[2,2,60],"12":[2,2,65],"13":[2,2,39],"14":[2,1,66],"15":[3,2,36],"16":[3,4,25],"17":[1,4,46],"18":[4,2,26],"19":[2,5,30],"20":[4,5,38],"21":[1,5,4],"22":[3,2,18],"23":[2,4,16],"24":[5,4,27],"25":[1,8,41],"26":[4,4,14],"27":[1,8,41],"28":[3,4,11],"29":[1,6,40],"30":[3,4,17],"31":[1,6,37],"32":[4,2,45],"33":[1,2,29],"34":[2,1,35],"35":[3,2,37],"36":[2,5,93],"37":[2,5,34],"38":[2,1,35],"39":[4,2,14],"40":[6,4,50],"41":[2,2,1],"42":[1,3,43],"43":[1,3,38],"44":[1,3,29],"45":[1,3,31],"46":[1,2,48],"47":[2,1,76],"48":[1,2,37],"49":[2,2,47],"50":[4,2,1],"51":[2,6,88],"52":[3,6,60],"53":[2,6,65],"54":[4,6,80],"55":[2,2,75],"56":[1,1,78],"57":[1,1,1],"58":[2,2,74],"59":[4,2,59],"60":[3,2,45],"61":[4,2,49],"62":[2,2,28],"63":[1,1,70],"64":[1,1,63],"65":[5,1,100],"66":[3,1,71],"67":[4,1,13],"68":[4,4,29],"69":[3,4,41],"70":[7,7,16],"71":[4,4,18],"72":[3,4,74],"73":[3,4,41],"74":[4,4,24],"75":[1,4,47],"76":[1,4,66],"77":[1,4,69],"78":[1,4,3],"79":[1,11,12],"80":[1,11,40],"81":[6,11,81],"82":[4,11,39],"83":[2,11,33],"84":[1,11,76],"85":[1,1,51],"86":[2,1,42],"87":[1,3,55],"88":[2,3,83],"89":[1,3,28],"90":[2,1,57],"91":[6,3,29],"92":[3,1,24],"93":[1,4,59],"94":[1,4,17],"95":[2,1,72],"96":[1,1,18],"97":[1,1,1],"98":[1,1,75],"99":[1,1,1],"100":[2,2,34],"101":[3,2,36],"102":[4,2,25],"103":[3,2,51],"104":[1,1,5],"105":[7,1,160],"106":[4,8,52],"107":[2,1,101],"108":[2,1,17],"109":[1,2,19],"110":[2,2,61],"111":[3,2,36],"112":[1,2,1],"113":[2,3,34],"114":[2,3,17],"115":[2,2,26],"116":[2,2,1],"117":[2,4,12],"118":[2,4,15],"119":[2,4,12],"120":[2,2,1],"121":[2,4,41],"122":[1,4,33],"123":[2,2,19],"124":[2,2,1],"125":[2,3,26],"126":[2,3,43],"127":[3,1,28],"128":[1,3,43],"129":[1,4,19],"130":[2,4,26],"131":[1,4,24],"132":[1,3,24],"133":[1,4,31],"134":[1,4,35],"135":[3,4,45],"136":[1,4,16],"137":[4,3,48],"138":[2,3,80],"139":[3,1,19],"140":[1,3,30],"141":[14,3,140],"142":[14,3,54],"143":[4,3,163],"144":[9,3,129],"145":[2,1,1],"146":[4,2,51],"147":[4,2,9],"148":[5,2,1],"149":[4,2,1],"150":[5,2,1],"151":[5,2,1],"152":[8,2,1],"153":[5,2,43],"154":[2,2,134],"155":[2,1,1],"156":[3,2,13],"157":[2,2,40],"158":[4,2,40],"159":[3,6,55],"160":[4,2,168],"161":[2,9,61],"162":[3,2,137],"163":[3,2,103],"164":[5,5,95],"165":[3,1,101],"166":[2,1,1],"167":[2,2,51],"168":[3,4,33],"169":[4,4,21],"170":[1,2,53],"171":[1,2,72],"172":[1,2,95],"173":[3,2,11],"174":[1,2,112],"175":[1,2,34],"176":[3,3,35],"177":[1,2,62],"178":[4,3,40],"179":[2,2,12],"180":[2,2,9],"181":[3,2,25],"182":[3,2,81],"183":[2,2,65],"184":[2,2,74],"185":[6,2,57],"186":[2,1,1],"187":[1,2,1],"188":[2,3,29],"189":[2,3,57],"190":[1,3,49],"191":[1,3,80],"192":[1,3,31],"193":[1,3,38],"194":[1,2,54],"195":[2,3,122],"196":[1,3,167],"197":[3,3,195],"198":[1,3,39],"199":[1,3,157],"200":[1,3,29],"201":[1,2,28],"202":[1,3,4],"203":[1,2,34],"204":[1,3,3],"205":[1,2,26],"206":[1,3,19],"207":[1,2,43],"208":[1,3,19],"209":[2,2,61],"210":[2,1,1],"211":[2,2,28],"212":[4,2,80],"213":[3,2,109],"214":[3,2,1],"215":[3,4,33],"216":[3,4,97],"217":[4,4,81],"218":[3,4,94],"219":[1,6,22],"220":[3,4,72],"221":[2,2,47],"222":[3,2,31],"223":[2,2,61],"224":[2,1,1],"225":[2,2,1],"226":[2,4,68],"227":[2,4,25],"228":[2,4,22],"229":[3,4,43],"230":[3,4,18],"231":[1,2,44],"232":[7,2,112],"233":[10,2,160]},"averageFieldLength":[2.5555555555555562,2.952991452991452,45.393162393162406],"storedFields":{"0":{"title":"Configuration","titles":[]},"1":{"title":"Purpose of the Configuration System","titles":["Configuration"]},"2":{"title":"How It Fits Into the System","titles":["Configuration"]},"3":{"title":"Configuration Files","titles":["Configuration"]},"4":{"title":"Precedence of Configuration","titles":["Configuration","Configuration Files"]},"5":{"title":"Environment Variables","titles":["Configuration"]},"6":{"title":"Example .env File","titles":["Configuration","Environment Variables"]},"7":{"title":"Loading Environment Variables","titles":["Configuration","Environment Variables"]},"8":{"title":"Step-by-Step Instructions for Usage","titles":["Configuration"]},"9":{"title":"Cluster Management","titles":[]},"10":{"title":"Overview","titles":["Cluster Management"]},"11":{"title":"Configuration Options","titles":["Cluster Management"]},"12":{"title":"Example Usage","titles":["Cluster Management"]},"13":{"title":"Metrics Integration","titles":["Cluster Management"]},"14":{"title":"Error Handling","titles":[]},"15":{"title":"Asynchronous Error Handler","titles":["Error Handling"]},"16":{"title":"Why It Exists","titles":["Error Handling","Asynchronous Error Handler"]},"17":{"title":"Usage","titles":["Error Handling","Asynchronous Error Handler"]},"18":{"title":"The Default Error Handler","titles":["Error Handling"]},"19":{"title":"Key Features","titles":["Error Handling","The Default Error Handler"]},"20":{"title":"Custom Default Error Handler","titles":["Error Handling","The Default Error Handler"]},"21":{"title":"Example","titles":["Error Handling","The Default Error Handler"]},"22":{"title":"Custom Error Handlers","titles":["Error Handling"]},"23":{"title":"404 Handler","titles":["Error Handling","Custom Error Handlers"]},"24":{"title":"Prisma Not Found Error Handler","titles":["Error Handling","Custom Error Handlers"]},"25":{"title":"Example","titles":["Error Handling","Custom Error Handlers","Prisma Not Found Error Handler"]},"26":{"title":"Prisma Constraint Violation Handler","titles":["Error Handling","Custom Error Handlers"]},"27":{"title":"Example","titles":["Error Handling","Custom Error Handlers","Prisma Constraint Violation Handler"]},"28":{"title":"Assertion Error Handler","titles":["Error Handling","Custom Error Handlers"]},"29":{"title":"Example","titles":["Error Handling","Custom Error Handlers","Assertion Error Handler"]},"30":{"title":"Axios Error Handler","titles":["Error Handling","Custom Error Handlers"]},"31":{"title":"Example","titles":["Error Handling","Custom Error Handlers","Axios Error Handler"]},"32":{"title":"Integration into the System","titles":["Error Handling"]},"33":{"title":"Summary","titles":["Error Handling"]},"34":{"title":"Scheduling Tasks","titles":[]},"35":{"title":"How It Works","titles":["Scheduling Tasks"]},"36":{"title":"Usage Instructions","titles":["Scheduling Tasks","How It Works"]},"37":{"title":"Best Practices","titles":["Scheduling Tasks","How It Works"]},"38":{"title":"Lifecycle Hooks","titles":[]},"39":{"title":"Location of Lifecycle Hooks","titles":["Lifecycle Hooks"]},"40":{"title":"Etiquette for Editing/Updating Lifecycle Hooks","titles":["Lifecycle Hooks","Location of Lifecycle Hooks"]},"41":{"title":"Hooks Overview","titles":["Lifecycle Hooks"]},"42":{"title":"beforeApplicationFork","titles":["Lifecycle Hooks","Hooks Overview"]},"43":{"title":"onApplicationBootstrap","titles":["Lifecycle Hooks","Hooks Overview"]},"44":{"title":"beforeApplicationShutdown","titles":["Lifecycle Hooks","Hooks Overview"]},"45":{"title":"onApplicationShutdown","titles":["Lifecycle Hooks","Hooks Overview"]},"46":{"title":"Integration","titles":["Lifecycle Hooks"]},"47":{"title":"Request Validation","titles":[]},"48":{"title":"Benefits","titles":["Request Validation"]},"49":{"title":"Usage Instructions","titles":["Request Validation"]},"50":{"title":"Comparison with different approaches","titles":["Request Validation"]},"51":{"title":"Manual Validation","titles":["Request Validation","Comparison with different approaches"]},"52":{"title":"Using express-validator","titles":["Request Validation","Comparison with different approaches"]},"53":{"title":"Using validate","titles":["Request Validation","Comparison with different approaches"]},"54":{"title":"Explanation of the Code","titles":["Request Validation","Comparison with different approaches"]},"55":{"title":"More Examples","titles":["Request Validation"]},"56":{"title":"ORM","titles":[]},"57":{"title":"Gotchas","titles":["ORM"]},"58":{"title":"Auto Timestamp","titles":["ORM","Gotchas"]},"59":{"title":"Aliasing and Excluding Columns","titles":["ORM","Gotchas"]},"60":{"title":"Avoid SELECT * Queries","titles":["ORM","Gotchas"]},"61":{"title":"Sorting with nulls last","titles":["ORM","Gotchas"]},"62":{"title":"Logging Queries","titles":["ORM","Gotchas"]},"63":{"title":"Authentication","titles":[]},"64":{"title":"Authorization","titles":[]},"65":{"title":"A simple use case:","titles":["Authorization"]},"66":{"title":"AccessControl Middleware Usage","titles":["Authorization"]},"67":{"title":"Node.js Metrics Documentation","titles":[]},"68":{"title":"nodejs_gc_duration_seconds","titles":["Node.js Metrics Documentation"]},"69":{"title":"nodejs_active_resources","titles":["Node.js Metrics Documentation"]},"70":{"title":"Deprecated Metrics: nodejs_active_requests and nodejs_active_handles","titles":["Node.js Metrics Documentation","nodejs_active_resources"]},"71":{"title":"process_start_time_seconds","titles":["Node.js Metrics Documentation"]},"72":{"title":"process_open_fds","titles":["Node.js Metrics Documentation"]},"73":{"title":"process_max_fds","titles":["Node.js Metrics Documentation"]},"74":{"title":"process_cpu_seconds_total","titles":["Node.js Metrics Documentation"]},"75":{"title":"osMemoryHeapLinux","titles":["Node.js Metrics Documentation"]},"76":{"title":"heapSpacesSizeAndUsed","titles":["Node.js Metrics Documentation"]},"77":{"title":"heapSizeAndUsed","titles":["Node.js Metrics Documentation"]},"78":{"title":"eventLoopLag","titles":["Node.js Metrics Documentation"]},"79":{"title":"Metrics","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"80":{"title":"Description","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"81":{"title":"Computation of nodejs_eventloop_lag_seconds","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"82":{"title":"Computation of Other Metrics","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"83":{"title":"Use Cases","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"84":{"title":"Limitations","titles":["Node.js Metrics Documentation","eventLoopLag","Deprecated Metrics: nodejs_active_requests and nodejs_active_handles"]},"85":{"title":"Instrumentation","titles":[]},"86":{"title":"Metrics Middleware","titles":["Instrumentation"]},"87":{"title":"Configuration","titles":["Instrumentation","Metrics Middleware"]},"88":{"title":"Metrics Collected","titles":["Instrumentation","Metrics Middleware"]},"89":{"title":"Usage","titles":["Instrumentation","Metrics Middleware"]},"90":{"title":"Custom Metrics","titles":["Instrumentation"]},"91":{"title":"Include the Metric in Your Code","titles":["Instrumentation","Custom Metrics"]},"92":{"title":"Clustered Metrics Aggregation","titles":["Instrumentation"]},"93":{"title":"Implementation","titles":["Instrumentation","Clustered Metrics Aggregation"]},"94":{"title":"Benefits","titles":["Instrumentation","Clustered Metrics Aggregation"]},"95":{"title":"OpenAPI Documentation","titles":[]},"96":{"title":"Linting","titles":[]},"97":{"title":"Architecture","titles":[]},"98":{"title":"Introduction","titles":[]},"99":{"title":"Philosophy","titles":["Introduction"]},"100":{"title":"Minimal Abstractions","titles":["Introduction","Philosophy"]},"101":{"title":"Comprehensive Feature Set","titles":["Introduction","Philosophy"]},"102":{"title":"Adherence to Best Practices","titles":["Introduction","Philosophy"]},"103":{"title":"Optimized Developer Experience","titles":["Introduction","Philosophy"]},"104":{"title":"Example","titles":["Introduction"]},"105":{"title":"Typical Request Flow Through the Express Server","titles":["Introduction"]},"106":{"title":"When Something Goes Wrong","titles":["Introduction","Typical Request Flow Through the Express Server"]},"107":{"title":"Project Structure","titles":["Introduction"]},"108":{"title":"Installation Guide","titles":[]},"109":{"title":"Prerequisites","titles":["Installation Guide"]},"110":{"title":"Quick Start","titles":["Installation Guide"]},"111":{"title":"Development Setup Details","titles":["Installation Guide"]},"112":{"title":"Configuration","titles":["Installation Guide"]},"113":{"title":"Environment Variables","titles":["Installation Guide","Configuration"]},"114":{"title":"Docker Configuration","titles":["Installation Guide","Configuration"]},"115":{"title":"Database Setup","titles":["Installation Guide"]},"116":{"title":"Common Operations","titles":["Installation Guide"]},"117":{"title":"Starting Services","titles":["Installation Guide","Common Operations"]},"118":{"title":"Checking Status","titles":["Installation Guide","Common Operations"]},"119":{"title":"Stopping Services","titles":["Installation Guide","Common Operations"]},"120":{"title":"Development Tools","titles":["Installation Guide"]},"121":{"title":"Code Linting","titles":["Installation Guide","Development Tools"]},"122":{"title":"Testing","titles":["Installation Guide","Development Tools"]},"123":{"title":"Queue System","titles":["Installation Guide"]},"124":{"title":"Troubleshooting Guide","titles":["Installation Guide"]},"125":{"title":"Common Issues","titles":["Installation Guide","Troubleshooting Guide"]},"126":{"title":"Development Tips","titles":["Installation Guide","Troubleshooting Guide"]},"127":{"title":"Metrics and Monitoring","titles":[]},"128":{"title":"Prometheus","titles":["Metrics and Monitoring"]},"129":{"title":"Configuration","titles":["Metrics and Monitoring","Prometheus"]},"130":{"title":"Without Prometheus","titles":["Metrics and Monitoring","Prometheus"]},"131":{"title":"Integration","titles":["Metrics and Monitoring","Prometheus"]},"132":{"title":"Grafana","titles":["Metrics and Monitoring"]},"133":{"title":"Features","titles":["Metrics and Monitoring","Grafana"]},"134":{"title":"Configuration","titles":["Metrics and Monitoring","Grafana"]},"135":{"title":"Authentication and Authorization","titles":["Metrics and Monitoring","Grafana"]},"136":{"title":"Integration","titles":["Metrics and Monitoring","Grafana"]},"137":{"title":"How This Setup Helps","titles":["Metrics and Monitoring"]},"138":{"title":"Usage Instructions","titles":["Metrics and Monitoring"]},"139":{"title":"Local Installation Guide","titles":[]},"140":{"title":"Overview","titles":["Local Installation Guide"]},"141":{"title":"Steps to setup API and run natively on development machine (not using docker)","titles":["Local Installation Guide"]},"142":{"title":"Steps to setup UI and run natively on development machine (not using docker)","titles":["Local Installation Guide"]},"143":{"title":"Set Up Workers locally","titles":["Local Installation Guide"]},"144":{"title":"Setup a Test Instance of Workers in colo nodes","titles":["Local Installation Guide"]},"145":{"title":"Auth Explained","titles":[]},"146":{"title":"Objectives for Auth module","titles":["Auth Explained"]},"147":{"title":"Overview of Auth Flow","titles":["Auth Explained"]},"148":{"title":"Vue App Code Execution Order","titles":["Auth Explained"]},"149":{"title":"Route Navigation Guard Logic","titles":["Auth Explained"]},"150":{"title":"Login Code flow - Before IU Login","titles":["Auth Explained"]},"151":{"title":"Login Code flow - After IU Login","titles":["Auth Explained"]},"152":{"title":"Code flow for a Revisiting (logged in) User","titles":["Auth Explained"]},"153":{"title":"Code flow for token refresh","titles":["Auth Explained"]},"154":{"title":"Cache Invalidation","titles":["Auth Explained"]},"155":{"title":"Secure Download","titles":[]},"156":{"title":"Table of Contents","titles":["Secure Download"]},"157":{"title":"1. Introduction","titles":["Secure Download"]},"158":{"title":"2. Requirements and Limitations","titles":["Secure Download"]},"159":{"title":"2.1 Limitations","titles":["Secure Download","2. Requirements and Limitations"]},"160":{"title":"3. Staging the Dataset","titles":["Secure Download"]},"161":{"title":"UUID Generation","titles":["Secure Download","3. Staging the Dataset","2.1 Limitations"]},"162":{"title":"4. Access Control","titles":["Secure Download"]},"163":{"title":"5. Architecture Overview","titles":["Secure Download"]},"164":{"title":"6. Downloading a Dataset File","titles":["Secure Download","5. Architecture Overview"]},"165":{"title":"Create a repository","titles":[]},"166":{"title":"UI Overview","titles":[]},"167":{"title":"Getting Started","titles":["UI Overview"]},"168":{"title":"Running using docker","titles":["UI Overview","Getting Started"]},"169":{"title":"Running on host machine","titles":["UI Overview","Getting Started"]},"170":{"title":"Features","titles":["UI Overview"]},"171":{"title":"Icons","titles":["UI Overview"]},"172":{"title":"Colors","titles":["UI Overview"]},"173":{"title":"Notable Vuestic Classes","titles":["UI Overview"]},"174":{"title":"Configuration","titles":["UI Overview"]},"175":{"title":"Constants","titles":["UI Overview"]},"176":{"title":"Configuration vs Constants","titles":["UI Overview","Constants"]},"177":{"title":"Authentication","titles":["UI Overview"]},"178":{"title":"Authentication controls on router","titles":["UI Overview","Authentication"]},"179":{"title":"Utility Components","titles":["UI Overview"]},"180":{"title":"Coding Conventions","titles":["UI Overview"]},"181":{"title":"Adding Additional Fonts","titles":["UI Overview"]},"182":{"title":"Dates and Times","titles":["UI Overview"]},"183":{"title":"Feature Flags","titles":["UI Overview"]},"184":{"title":"Navigational Breadcrumbs","titles":["UI Overview"]},"185":{"title":"HTTP API Error Handling and Notifications","titles":["UI Overview"]},"186":{"title":"Utility Components","titles":[]},"187":{"title":"AutoComplete","titles":["Utility Components"]},"188":{"title":"Basic Usage","titles":["Utility Components","AutoComplete"]},"189":{"title":"With Slots","titles":["Utility Components","AutoComplete"]},"190":{"title":"Async","titles":["Utility Components","AutoComplete"]},"191":{"title":"Props","titles":["Utility Components","AutoComplete"]},"192":{"title":"Events","titles":["Utility Components","AutoComplete"]},"193":{"title":"Slots","titles":["Utility Components","AutoComplete"]},"194":{"title":"SearchAndSelect","titles":["Utility Components"]},"195":{"title":"Basic Usage","titles":["Utility Components","SearchAndSelect"]},"196":{"title":"Filters","titles":["Utility Components","SearchAndSelect"]},"197":{"title":"Formatting and Slots","titles":["Utility Components","SearchAndSelect"]},"198":{"title":"Notes","titles":["Utility Components","SearchAndSelect"]},"199":{"title":"Props","titles":["Utility Components","SearchAndSelect"]},"200":{"title":"Slots","titles":["Utility Components","SearchAndSelect"]},"201":{"title":"Maybe","titles":["Utility Components"]},"202":{"title":"Props","titles":["Utility Components","Maybe"]},"203":{"title":"CopyText","titles":["Utility Components"]},"204":{"title":"props","titles":["Utility Components","CopyText"]},"205":{"title":"BinaryStatusChip","titles":["Utility Components"]},"206":{"title":"Props","titles":["Utility Components","BinaryStatusChip"]},"207":{"title":"EnvAlert","titles":["Utility Components"]},"208":{"title":"Props","titles":["Utility Components","EnvAlert"]},"209":{"title":"useQueryPersistence Composable","titles":["Utility Components"]},"210":{"title":"Upload Architecture","titles":[]},"211":{"title":"1. Introduction","titles":["Upload Architecture"]},"212":{"title":"2. Requirements and Limitations","titles":["Upload Architecture"]},"213":{"title":"3. Architecture Overview","titles":["Upload Architecture"]},"214":{"title":"4. The Upload","titles":["Upload Architecture"]},"215":{"title":"4.1 Logging","titles":["Upload Architecture","4. The Upload"]},"216":{"title":"4.2 Steps","titles":["Upload Architecture","4. The Upload"]},"217":{"title":"4.3 Directory structure","titles":["Upload Architecture","4. The Upload"]},"218":{"title":"4.4 Access Control","titles":["Upload Architecture","4. The Upload"]},"219":{"title":"Example","titles":["Upload Architecture","4. The Upload","4.4 Access Control"]},"220":{"title":"4.5 Status","titles":["Upload Architecture","4. The Upload"]},"221":{"title":"5. Processing","titles":["Upload Architecture"]},"222":{"title":"6. Data Integrity","titles":["Upload Architecture"]},"223":{"title":"7. Retry","titles":["Upload Architecture"]},"224":{"title":"Worker Overview","titles":[]},"225":{"title":"Coding Guidelines","titles":["Worker Overview"]},"226":{"title":"Hierarchical Config","titles":["Worker Overview","Coding Guidelines"]},"227":{"title":"Celery config","titles":["Worker Overview","Coding Guidelines"]},"228":{"title":"Code Organization","titles":["Worker Overview","Coding Guidelines"]},"229":{"title":"Parallel tasks limit","titles":["Worker Overview","Coding Guidelines"]},"230":{"title":"Hot Module Replacement","titles":["Worker Overview","Coding Guidelines"]},"231":{"title":"Deployment","titles":["Worker Overview"]},"232":{"title":"Testing with workers running on local machine","titles":["Worker Overview"]},"233":{"title":"Testing with workers running on COLO node and Rhythm API","titles":["Worker Overview"]}},"dirtCount":0,"index":[["qc",{"2":{"233":3}}],["q",{"2":{"232":1}}],["quirks",{"2":{"143":1}}],["quick",{"0":{"110":1},"2":{"126":1}}],["quot",{"2":{"64":4,"141":2,"144":2,"160":12,"162":6,"165":18,"171":6,"177":8,"197":4,"199":6,"226":2,"233":4}}],["quality",{"2":{"48":1}}],["queue",{"0":{"123":1},"2":{"113":1,"123":2,"144":4,"232":4,"233":4}}],["queues",{"2":{"37":1,"123":1,"165":1,"232":3}}],["queries",{"0":{"60":1,"62":1},"2":{"58":2,"62":2,"138":1}}],["querying",{"2":{"164":1}}],["query",{"2":{"17":2,"24":1,"47":1,"49":1,"55":6,"62":1,"105":4,"154":1,"164":1,"209":9}}],["known",{"2":{"160":1}}],["kb",{"2":{"147":1}}],["keep",{"2":{"174":1,"209":1,"232":1}}],["keeping",{"2":{"32":1,"69":1}}],["keyout",{"2":{"110":1,"142":1}}],["keyword",{"2":{"59":1,"195":1,"196":1,"197":1}}],["key",{"0":{"19":1},"2":{"87":1,"110":1,"111":1,"141":1,"142":1,"174":2,"195":2,"196":2,"197":3,"200":1,"209":1}}],["keys",{"2":{"1":1,"59":2,"107":2,"110":1,"141":1,"164":1,"167":1}}],["~100gb",{"2":{"143":1}}],["~",{"2":{"126":1,"231":2}}],["|",{"2":{"125":1,"177":4,"199":2}}],["||",{"2":{"51":2,"61":1,"174":1,"189":2}}],["yaml",{"2":{"178":3,"184":1}}],["yml",{"2":{"114":2,"128":1,"129":2,"134":1,"138":1,"141":1,"144":1,"165":2,"233":1}}],["you",{"2":{"10":1,"34":1,"36":1,"52":1,"54":1,"58":1,"59":1,"61":1,"90":1,"108":1,"141":2,"143":2,"182":1,"209":1}}],["your",{"0":{"91":1},"2":{"8":1,"17":1,"33":1,"34":1,"40":2,"49":1,"54":1,"77":1,"85":2,"90":1,"139":1,"143":3,"170":1,"209":1}}],["98e8",{"2":{"160":1}}],["9",{"2":{"105":1,"143":2,"164":1}}],["9999",{"2":{"88":1}}],["8601",{"2":{"182":1}}],["8",{"2":{"105":1,"143":1,"164":1,"229":1}}],["8080",{"2":{"8":1}}],["72",{"2":{"220":1,"223":2}}],["767c88",{"2":{"172":1}}],["7f828b",{"2":{"172":1}}],["7",{"0":{"223":1},"2":{"105":1,"164":1,"165":1}}],[">",{"2":{"88":1,"172":8,"178":3,"184":1,"188":1,"189":4,"190":1,"195":1,"196":3,"197":3,"201":1,"203":1,"205":1,"207":1,"231":1}}],["00",{"2":{"182":1}}],["04",{"2":{"182":1}}],["06",{"2":{"182":4}}],["038533292",{"2":{"88":1}}],["03",{"2":{"88":1}}],["0",{"2":{"88":4,"122":2,"172":3,"184":1,"195":2,"196":2,"197":2,"206":1,"233":1}}],["+=",{"2":{"195":1,"196":1,"197":1}}],["+inf",{"2":{"88":1}}],["+",{"2":{"74":1,"178":1,"195":5,"196":6,"197":5}}],["6a5b1734",{"2":{"160":1}}],["6",{"0":{"164":1,"222":1},"2":{"58":2,"105":1,"164":1}}],["zone",{"2":{"58":2,"182":2}}],["xs",{"2":{"196":1}}],["x",{"2":{"125":1,"135":1,"160":1,"164":1,"203":1}}],["x509",{"2":{"110":1,"142":1}}],["x3c",{"2":{"51":2,"138":1,"141":1,"144":5,"165":2,"172":18,"178":6,"184":6,"188":5,"189":14,"190":5,"195":5,"196":11,"197":13,"201":3,"203":3,"205":3,"207":3,"232":1,"233":5}}],["x26",{"2":{"25":2,"27":2,"55":1,"121":4,"141":2,"142":2,"189":1,"195":2,"196":2,"197":2}}],["1a9e5d8d9f7c",{"2":{"160":1}}],["16+",{"2":{"109":1}}],["14t01",{"2":{"182":3}}],["14",{"2":{"105":1,"182":1}}],["130",{"2":{"182":1}}],["13",{"2":{"105":1,"182":1}}],["12000",{"2":{"182":1}}],["120",{"2":{"174":1}}],["127",{"2":{"122":1}}],["12",{"2":{"105":1,"164":1,"172":1}}],["123",{"2":{"87":1}}],["11",{"2":{"105":2,"164":1}}],["1",{"0":{"157":1,"159":1,"211":1,"215":1},"1":{"161":1},"2":{"55":3,"72":1,"88":2,"105":1,"122":1,"144":1,"164":1,"171":1,"184":2,"188":1,"189":1,"195":7,"196":8,"197":7,"206":1,"209":1,"232":2,"233":1}}],["18",{"2":{"51":2,"52":1,"53":1,"54":1,"182":4}}],["10ms",{"2":{"82":1,"84":1}}],["100",{"2":{"55":3,"203":1}}],["1000",{"2":{"12":1,"182":2}}],["10000",{"2":{"11":1}}],["10",{"2":{"51":1,"88":1,"105":2,"164":1,"195":1,"196":1,"197":1,"199":1,"209":1,"231":1,"232":1}}],["158de3",{"2":{"172":1}}],["154ec1",{"2":{"172":1}}],["15",{"2":{"12":1}}],["15000",{"2":{"12":1}}],["5xx",{"2":{"185":1}}],["50",{"2":{"195":1,"196":1,"197":1}}],["501z",{"2":{"182":3}}],["500",{"2":{"19":1,"30":1,"106":1}}],["5000",{"2":{"11":1}}],["5772",{"2":{"144":2,"233":2}}],["5672",{"2":{"144":2,"233":2}}],["5432",{"2":{"141":1}}],["5",{"0":{"163":1,"220":1,"221":1},"1":{"164":1},"2":{"12":2,"15":1,"51":2,"52":1,"53":1,"54":1,"55":1,"105":1,"164":1,"216":1,"231":1}}],["49a8ff",{"2":{"172":1}}],["443",{"2":{"167":2,"168":1}}],["4e47",{"2":{"160":1}}],["4xx",{"2":{"88":1,"185":1}}],["40",{"2":{"182":4}}],["4096",{"2":{"110":1,"142":1}}],["401",{"2":{"105":1}}],["403",{"2":{"65":1,"66":1,"105":1}}],["404",{"0":{"23":1},"2":{"23":1,"24":1,"105":1,"106":2,"185":1}}],["400",{"2":{"20":1,"28":1,"51":4,"52":1,"53":1,"54":1,"105":1}}],["4",{"0":{"162":1,"214":1,"215":1,"216":1,"217":1,"218":2,"220":1},"1":{"215":1,"216":1,"217":1,"218":1,"219":3,"220":1},"2":{"12":2,"105":1,"164":2,"217":1}}],["320px",{"2":{"195":1,"196":1,"197":1}}],["350px",{"2":{"195":1,"196":1,"197":1}}],["3h",{"2":{"182":1}}],["3d9209",{"2":{"172":1}}],["3130",{"2":{"144":2,"233":2}}],["30s",{"2":{"87":1}}],["30ms",{"2":{"87":1}}],["30",{"2":{"84":1,"88":1}}],["3000",{"2":{"12":1,"167":2,"168":1,"169":1}}],["3030",{"2":{"8":1,"122":1,"125":1,"142":1,"144":2,"233":2}}],["3",{"0":{"160":1,"213":1,"217":1},"1":{"161":1},"2":{"11":1,"12":1,"88":2,"105":1,"143":1,"164":1,"188":1,"196":1,"217":1,"223":1,"231":1,"232":2}}],["24",{"2":{"223":1}}],["20m",{"2":{"182":1}}],["2023",{"2":{"143":1,"182":5}}],["21",{"2":{"182":1}}],["262824",{"2":{"172":1}}],["2xl",{"2":{"171":1}}],["2xx",{"2":{"88":11}}],["28017",{"2":{"144":2,"233":2}}],["27017",{"2":{"144":2,"233":2}}],["2",{"0":{"158":1,"159":1,"212":1,"216":1},"1":{"159":1,"161":1},"2":{"11":1,"88":9,"105":1,"164":1,"171":1,"182":2,"184":1,"188":1,"196":1,"206":2,"212":1,"229":1}}],["`formatted",{"2":{"197":1}}],["`other",{"2":{"195":1,"196":1,"197":1}}],["`result",{"2":{"195":1,"196":1,"197":1}}],["`search",{"2":{"190":1}}],["`server",{"2":{"8":1}}],["`",{"2":{"8":1,"195":4,"196":6,"197":5,"217":1}}],["$",{"2":{"8":1,"123":1,"195":3,"196":5,"197":4}}],["==",{"2":{"172":1,"185":1,"220":1,"223":1}}],["===",{"2":{"27":1,"65":1,"183":1}}],["=>",{"2":{"12":3,"17":2,"25":2,"27":2,"29":2,"31":2,"36":2,"51":2,"52":2,"53":1,"59":1,"61":1,"63":1,"65":1,"66":1,"184":1,"185":3,"189":3,"190":3,"195":22,"196":23,"197":23,"209":1}}],["=",{"2":{"8":2,"12":1,"17":3,"25":2,"27":2,"29":3,"31":3,"36":6,"51":1,"52":1,"53":3,"55":2,"59":1,"60":2,"61":3,"62":1,"63":2,"65":6,"66":3,"89":1,"90":1,"91":1,"93":1,"95":1,"172":2,"184":6,"188":1,"189":5,"190":5,"191":2,"195":27,"196":31,"197":27,"209":2,"217":1}}],["job",{"2":{"228":1}}],["jobs",{"2":{"35":1,"37":1,"42":1}}],["jun",{"2":{"182":1}}],["june",{"2":{"143":1}}],["jwks",{"2":{"164":1}}],["jwt",{"2":{"63":2,"105":2,"107":1,"110":1,"113":1,"135":4,"162":6,"164":1,"165":1}}],["javascriptgetrecords",{"2":{"185":1}}],["javascriptimport",{"2":{"182":1,"185":1,"209":1}}],["javascript",{"2":{"36":1,"40":1,"59":1,"65":1,"66":1,"77":1,"172":1,"174":1}}],["javascriptmodule",{"2":{"36":1}}],["javascriptapp",{"2":{"21":1,"23":1,"25":1,"27":1,"29":1,"31":1,"51":1,"52":1}}],["javascriptrouter",{"2":{"17":1,"25":1,"27":1}}],["javascriptrequire",{"2":{"7":1,"8":1}}],["javascriptconst",{"2":{"12":1,"17":1,"29":1,"31":1,"53":1,"55":1,"59":1,"60":2,"61":1,"62":1,"63":1,"65":1,"89":1,"90":1,"91":1,"93":1}}],["jsconfig",{"2":{"170":1}}],["js",{"0":{"67":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"9":2,"13":2,"32":1,"35":1,"36":3,"39":1,"40":2,"47":1,"56":3,"64":1,"66":1,"68":1,"70":1,"71":1,"72":1,"77":2,"80":1,"81":1,"85":1,"87":1,"88":1,"89":1,"90":2,"91":1,"93":2,"95":2,"98":1,"104":2,"105":3,"107":12,"109":1,"115":1,"128":2,"131":1,"133":1,"135":1,"138":2,"143":1,"144":3,"165":2,"174":3,"175":1,"177":1,"181":1,"182":1,"183":2,"207":1,"229":1,"231":1,"233":3}}],["json",{"2":{"2":1,"3":4,"4":3,"8":7,"17":2,"25":2,"27":2,"29":2,"31":2,"51":5,"52":2,"53":1,"60":1,"65":1,"66":1,"95":1,"107":1,"162":1,"165":2,"170":2,"171":1,"177":1,"219":2}}],["uuids",{"2":{"161":1}}],["uuid",{"0":{"161":1},"2":{"160":2}}],["u",{"2":{"143":1}}],["uiclient",{"2":{"164":1}}],["uits",{"2":{"144":1}}],["ui",{"0":{"142":1,"166":1},"1":{"167":1,"168":1,"169":1,"170":1,"171":1,"172":1,"173":1,"174":1,"175":1,"176":1,"177":1,"178":1,"179":1,"180":1,"181":1,"182":1,"183":1,"184":1,"185":1},"2":{"110":4,"111":1,"113":1,"117":1,"121":2,"122":2,"135":1,"140":1,"142":2,"144":2,"154":4,"159":1,"160":2,"163":1,"164":3,"165":2,"167":2,"168":2,"172":2,"173":2,"177":2,"182":1,"183":1,"207":1,"213":2,"233":2}}],["utc",{"2":{"58":3,"182":1}}],["utils",{"2":{"51":1,"107":1,"230":1}}],["utilization",{"2":{"9":1}}],["utility",{"0":{"179":1,"186":1},"1":{"187":1,"188":1,"189":1,"190":1,"191":1,"192":1,"193":1,"194":1,"195":1,"196":1,"197":1,"198":1,"199":1,"200":1,"201":1,"202":1,"203":1,"204":1,"205":1,"206":1,"207":1,"208":1,"209":1},"2":{"9":1,"98":1,"100":1}}],["url>",{"2":{"165":1}}],["url=http",{"2":{"142":1}}],["url=https",{"2":{"6":1}}],["url=",{"2":{"141":1}}],["url",{"2":{"56":1,"113":1,"141":1,"142":1,"146":1,"162":9,"164":3,"165":1,"167":1,"168":1,"169":2,"209":3}}],["unformatted",{"2":{"197":1}}],["undefined",{"2":{"196":1,"201":2}}],["underlying",{"2":{"191":2,"199":2}}],["understanding",{"2":{"127":1}}],["underutilizing",{"2":{"9":1}}],["under",{"2":{"8":1,"9":1,"207":1,"229":1}}],["unselected",{"2":{"194":1,"199":1}}],["unselecting",{"2":{"194":1,"199":1}}],["unchanged",{"2":{"175":1}}],["unplugin",{"2":{"171":1}}],["unexpired",{"2":{"162":1}}],["unexpected",{"2":{"14":1}}],["universally",{"2":{"160":1}}],["uniquely",{"2":{"199":1}}],["unique",{"2":{"26":1,"69":1,"160":1,"217":2}}],["unauthorized",{"2":{"157":1,"158":1,"160":1,"161":1,"162":2,"211":1,"212":1}}],["unnecessary",{"2":{"98":1}}],["unless",{"2":{"87":1}}],["until",{"2":{"81":1}}],["unused",{"2":{"75":1}}],["unwanted",{"2":{"59":1}}],["unknown",{"2":{"23":1,"32":1}}],["unhandled",{"2":{"18":1}}],["upon",{"2":{"223":1}}],["upto",{"2":{"216":1}}],["uptime",{"2":{"71":1}}],["uploads",{"2":{"212":1,"213":1,"217":1,"223":3}}],["uploaded",{"2":{"212":1,"213":2,"215":1,"216":5,"217":4,"218":2,"220":4,"221":3,"222":2,"223":1}}],["uploading",{"2":{"211":1,"212":3,"213":1,"220":1}}],["upload",{"0":{"210":1,"214":1},"1":{"211":1,"212":1,"213":1,"214":1,"215":2,"216":2,"217":2,"218":2,"219":2,"220":2,"221":1,"222":1,"223":1},"2":{"175":1,"212":4,"213":5,"215":8,"216":8,"217":9,"218":10,"219":3,"220":9,"221":2,"223":1}}],["updates",{"2":{"84":1,"153":1,"154":1,"165":1}}],["updated",{"2":{"81":1,"84":1,"105":1,"154":4,"230":1}}],["updatedat",{"2":{"58":3}}],["update",{"2":{"65":4,"137":1,"138":1,"143":2,"144":1,"154":2,"165":1,"192":1,"209":2,"231":1,"233":1}}],["updating",{"0":{"40":1}}],["up",{"0":{"143":1},"2":{"12":1,"13":1,"103":1,"108":1,"110":2,"117":2,"126":1,"138":1,"139":1,"141":2,"144":2,"168":1,"170":1,"198":1,"232":1,"233":2}}],["us",{"2":{"212":1}}],["usage",{"0":{"8":1,"12":1,"17":1,"36":1,"49":1,"66":1,"89":1,"138":1,"188":1,"195":1},"2":{"69":1,"72":2,"73":1,"75":2,"76":2,"77":3,"171":3,"182":1,"209":1}}],["usequerypersistence",{"0":{"209":1},"2":{"209":3}}],["usenavstore",{"2":{"184":4}}],["usecase",{"2":{"182":1}}],["usecolors",{"2":{"172":2}}],["users`",{"2":{"190":1}}],["users",{"2":{"36":1,"60":2,"65":5,"87":2,"115":1,"133":1,"135":1,"146":3,"157":1,"158":3,"159":2,"160":6,"161":2,"162":2,"163":2,"167":2,"177":1,"178":1,"184":2,"189":5,"190":1,"212":2,"213":2}}],["userservice",{"2":{"17":2,"25":2,"27":2,"29":2,"63":1,"65":1,"66":1,"189":1,"190":1}}],["username",{"2":{"17":4,"25":4,"29":4,"51":5,"52":3,"53":3,"54":2,"63":2,"65":5,"66":3,"91":2,"189":2,"190":3}}],["user",{"0":{"152":1},"2":{"17":6,"27":1,"31":4,"51":4,"52":4,"53":4,"59":5,"60":2,"63":3,"64":1,"65":18,"66":5,"74":1,"95":1,"105":3,"123":1,"126":1,"146":2,"153":2,"154":6,"160":2,"162":4,"163":2,"164":2,"165":1,"178":2,"189":4,"190":2,"194":1,"213":2,"218":2}}],["useful",{"2":{"9":1,"34":1,"126":1,"205":1}}],["used",{"2":{"3":1,"7":1,"15":1,"22":1,"46":2,"56":2,"64":1,"72":1,"76":2,"77":2,"85":1,"107":1,"132":1,"135":2,"143":1,"161":1,"162":1,"174":1,"176":1,"182":1,"191":3,"196":1,"199":4,"200":2,"213":1,"217":1,"219":1,"233":4}}],["use",{"0":{"65":1,"83":1},"2":{"2":1,"8":1,"21":1,"23":1,"25":1,"27":1,"29":1,"31":1,"36":1,"37":1,"42":1,"43":1,"44":1,"45":1,"49":1,"53":1,"58":1,"70":1,"75":1,"89":1,"93":1,"98":1,"102":1,"122":1,"126":1,"133":1,"143":1,"154":1,"171":1,"172":3,"180":1,"182":3,"191":2,"199":1,"232":1}}],["uses",{"2":{"0":1,"5":1,"35":1,"47":1,"63":1,"111":1,"123":1,"127":1,"135":1,"154":2}}],["using",{"0":{"52":1,"53":1,"141":1,"142":1,"168":1},"2":{"1":2,"8":1,"13":1,"36":1,"54":1,"58":3,"81":1,"82":1,"95":1,"103":1,"107":1,"108":1,"121":1,"138":1,"143":1,"144":2,"154":1,"171":1,"172":2,"177":1,"233":2}}],["rgba",{"2":{"172":1}}],["r",{"2":{"123":1,"144":3,"231":1,"233":3}}],["rhythm",{"0":{"233":1},"2":{"123":1,"141":8,"143":2,"144":3,"160":2,"213":1,"216":1,"232":1,"233":3}}],["rsa",{"2":{"110":1,"142":1}}],["range",{"2":{"195":1,"196":1,"197":1}}],["ranging",{"2":{"87":1}}],["randomized",{"2":{"160":1}}],["randomization",{"2":{"160":1}}],["random",{"2":{"160":1}}],["rabbitmq",{"2":{"143":1}}],["rather",{"2":{"100":1,"101":1}}],["ram",{"2":{"75":1}}],["raw",{"2":{"55":1,"58":1,"197":5,"216":1}}],["ruleset",{"2":{"96":1}}],["rules",{"2":{"49":2,"52":1,"53":1,"54":2,"96":1}}],["runs",{"2":{"127":1,"131":1}}],["run",{"0":{"141":1,"142":1},"2":{"9":1,"10":1,"36":4,"42":1,"95":2,"107":1,"115":1,"141":3,"142":1,"143":1,"144":4,"168":1,"170":2,"212":1,"213":1,"223":1,"229":1,"230":1,"232":1,"233":2}}],["running",{"0":{"168":1,"169":1,"232":1,"233":1},"2":{"8":1,"12":2,"36":1,"37":1,"83":1,"138":1,"139":1,"141":2,"143":2,"144":2,"159":1,"167":2,"207":1,"232":1,"233":2}}],["runtime",{"2":{"5":1,"107":1,"176":1}}],["rights",{"2":{"162":1}}],["right",{"2":{"38":1}}],["rowdata",{"2":{"201":1}}],["rollup",{"2":{"170":2}}],["role",{"2":{"59":2,"64":3,"65":4,"133":1,"154":1,"162":1,"178":2,"218":1}}],["roles",{"2":{"59":2,"64":1,"65":6,"162":2}}],["root",{"2":{"168":1}}],["routing",{"2":{"105":2,"106":1,"107":1,"170":1}}],["route>",{"2":{"178":3,"184":1}}],["routes",{"2":{"23":1,"32":1,"33":1,"48":1,"63":3,"64":1,"66":1,"95":4,"104":2,"105":1,"106":1,"107":2,"232":1}}],["routers",{"2":{"63":1,"105":1}}],["router",{"0":{"178":1},"2":{"17":1,"25":1,"27":1,"29":2,"31":2,"63":2,"65":1,"66":1,"95":3,"105":4,"107":1,"170":1}}],["route",{"0":{"149":1},"2":{"15":1,"16":1,"17":1,"47":2,"48":1,"49":2,"51":2,"53":1,"54":2,"63":1,"65":1,"95":1,"105":3,"107":2,"178":3,"184":2}}],["robust",{"2":{"9":1,"14":1,"98":1,"138":1}}],["rejects",{"2":{"218":1}}],["rendered",{"2":{"193":2}}],["render",{"2":{"193":1}}],["reused",{"2":{"179":1}}],["reusable",{"2":{"47":1,"107":1}}],["reusability",{"2":{"35":1}}],["retries",{"2":{"216":1,"223":2}}],["retrieved",{"2":{"191":1,"194":1,"199":2}}],["retrievedusers",{"2":{"190":3}}],["retrieve",{"2":{"154":2}}],["retryable",{"2":{"223":1}}],["retry",{"0":{"223":1},"2":{"154":1}}],["return=https",{"2":{"177":2}}],["returned",{"2":{"59":1,"154":1,"182":1,"198":1}}],["returning",{"2":{"54":1}}],["returns",{"2":{"53":1}}],["return",{"2":{"25":2,"27":2,"29":2,"31":2,"51":4,"52":1,"59":1,"61":3,"65":3,"66":2,"81":1,"189":1,"195":9,"196":10,"197":9,"198":1}}],["remain",{"2":{"175":1,"176":1}}],["remains",{"2":{"146":1}}],["removals",{"2":{"195":2,"196":2,"197":2}}],["remove=",{"2":{"195":1,"196":1,"197":1}}],["remove",{"2":{"59":1,"119":2,"126":1}}],["remote",{"2":{"144":4,"165":2,"233":4}}],["revisiting",{"0":{"152":1}}],["revisit",{"2":{"146":1}}],["reverse",{"2":{"135":4,"144":1,"233":1}}],["redeployed",{"2":{"183":1}}],["redirect",{"2":{"142":2,"146":1,"164":2,"167":1,"168":1,"169":1}}],["reduce",{"2":{"103":1,"154":1}}],["reduces",{"2":{"33":1,"48":1}}],["reducing",{"2":{"2":1,"52":1,"53":1}}],["reopen",{"2":{"121":1}}],["relative",{"2":{"162":1,"203":1,"216":1}}],["relational",{"2":{"56":1,"215":1}}],["reload",{"2":{"111":1}}],["reloads",{"2":{"103":1}}],["reloading",{"2":{"98":1}}],["rely",{"2":{"1":1}}],["regardless",{"2":{"45":1}}],["registers",{"2":{"87":1}}],["register",{"2":{"36":1}}],["registering",{"2":{"35":1,"42":1}}],["registercronjobs",{"2":{"35":1,"36":2}}],["registered",{"2":{"32":1,"36":1,"90":1,"106":1,"160":1,"233":1}}],["reactive",{"2":{"209":1}}],["reach",{"2":{"22":1,"154":1}}],["reason",{"2":{"90":1,"91":1,"154":1}}],["real",{"2":{"75":1,"81":1,"84":1,"130":1,"137":2}}],["reads",{"2":{"183":1,"226":1}}],["reading",{"2":{"183":2}}],["readme",{"2":{"165":2}}],["ready",{"2":{"98":1}}],["readdirsync",{"2":{"72":2}}],["readableduration",{"2":{"182":1}}],["readability",{"2":{"40":1,"48":1,"52":1}}],["readany",{"2":{"65":2}}],["readown",{"2":{"65":2}}],["read",{"2":{"65":4,"66":1,"160":1,"174":1,"203":1}}],["recreated",{"2":{"217":1,"221":2,"222":1}}],["recalculation",{"2":{"195":1,"196":1,"197":1}}],["record",{"2":{"217":1}}],["recorded",{"2":{"81":1}}],["recognized",{"2":{"175":1}}],["recommended",{"2":{"143":1,"171":1}}],["recurring",{"2":{"34":1}}],["receive",{"2":{"135":1}}],["receives",{"2":{"20":1,"105":1,"163":1,"213":1,"218":1}}],["received",{"2":{"12":1,"216":1}}],["receiving",{"2":{"10":1}}],["refobject",{"2":{"209":3}}],["ref",{"2":{"189":2,"190":2,"195":5,"196":7,"197":5,"209":3}}],["refs",{"2":{"153":1}}],["reflects",{"2":{"81":1,"84":1}}],["refined",{"2":{"77":1}}],["refreshtoken",{"2":{"153":3}}],["refresh",{"0":{"153":1},"2":{"63":1,"209":1}}],["refactoring",{"2":{"25":2,"27":2,"29":2,"31":2}}],["references",{"2":{"165":1}}],["reference",{"2":{"81":1}}],["refers",{"2":{"77":1}}],["refer",{"2":{"13":1,"59":1,"147":1}}],["repo",{"2":{"165":3,"171":1}}],["repository",{"0":{"165":1},"2":{"109":1,"110":1,"165":1}}],["reported",{"2":{"84":2}}],["reporting",{"2":{"81":1}}],["represent",{"2":{"84":1}}],["represents",{"2":{"73":1,"105":1}}],["replaced",{"2":{"218":1}}],["replace",{"2":{"95":1,"154":1,"165":2,"232":2}}],["replacement",{"0":{"230":1},"2":{"70":1}}],["replaces",{"2":{"17":1}}],["repetitive",{"2":{"16":1,"33":1,"47":1,"48":1,"51":1}}],["req",{"2":{"17":4,"25":4,"27":4,"29":6,"31":2,"51":8,"52":5,"53":4,"63":3,"65":6,"66":2,"105":3,"110":1,"142":1}}],["requested",{"2":{"81":1,"146":1,"164":3}}],["requester",{"2":{"64":1,"65":6,"105":1}}],["requesting",{"2":{"65":1}}],["request",{"0":{"47":1,"105":1},"1":{"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"106":1},"2":{"28":1,"47":2,"49":2,"52":1,"53":1,"54":1,"63":1,"64":1,"65":1,"86":2,"87":1,"88":14,"100":1,"105":12,"154":5,"160":4,"164":1,"216":2,"218":3}}],["requests",{"0":{"70":1},"1":{"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"9":1,"23":1,"30":1,"47":1,"66":1,"84":1,"88":1,"89":1,"105":1,"153":1,"154":1,"163":2,"164":3,"185":1,"213":2,"218":1}}],["requirements",{"0":{"158":1,"212":1},"1":{"159":1},"2":{"156":1,"157":1,"163":1,"213":1,"231":2}}],["requiresauth",{"2":{"178":1}}],["requiresroles",{"2":{"178":1,"184":1}}],["requires",{"2":{"98":1,"113":1,"178":1}}],["require",{"2":{"8":1,"12":1,"17":1,"29":1,"31":1,"36":3,"53":2,"55":2,"59":1,"63":3,"65":1,"66":1,"89":1,"91":1,"93":1,"178":1}}],["required",{"2":{"5":1,"7":1,"8":1,"11":1,"29":2,"51":1,"101":1,"110":1,"113":1,"115":1,"141":1,"146":1,"174":1}}],["result",{"2":{"193":2,"195":4,"196":4,"197":4,"198":1,"199":3}}],["results=",{"2":{"195":2,"196":2,"197":2}}],["results",{"2":{"184":3,"191":3,"192":1,"194":4,"195":2,"196":2,"197":3,"199":11,"200":1}}],["resetsearchstate",{"2":{"195":2,"196":2,"197":2}}],["resetting",{"2":{"195":1,"196":1,"197":1}}],["reset=",{"2":{"195":1,"196":1,"197":1}}],["reset",{"2":{"184":1,"194":1,"195":2,"196":5,"197":2}}],["resident",{"2":{"75":1}}],["responds",{"2":{"153":1,"154":1,"164":2}}],["responsible",{"2":{"35":1,"86":1}}],["responses",{"2":{"20":1,"24":1,"26":1,"33":1,"54":1,"88":1,"106":1}}],["response",{"2":{"18":1,"28":1,"30":1,"31":7,"53":1,"54":1,"86":1,"87":1,"105":8,"106":4,"154":2,"185":1}}],["respective",{"2":{"40":1}}],["res",{"2":{"17":4,"19":1,"25":4,"27":4,"29":4,"31":4,"51":6,"52":3,"53":2,"63":1,"65":2,"66":2,"185":2,"190":2,"195":3,"196":3,"197":3}}],["resolve",{"2":{"195":2,"196":2,"197":2}}],["resolves",{"2":{"4":1}}],["resolution",{"2":{"81":1,"82":1,"84":2}}],["resourceowner",{"2":{"65":2}}],["resource",{"2":{"24":1,"64":1,"65":2,"66":3,"105":1,"162":1,"199":1}}],["resources",{"0":{"69":1},"1":{"70":1},"2":{"9":1,"64":2,"69":7,"70":2,"104":1,"220":1,"221":1,"223":1}}],["restarts",{"2":{"11":3,"12":3}}],["restart",{"2":{"9":1,"10":1,"11":1,"138":3}}],["n",{"2":{"143":1}}],["nginx",{"2":{"135":2,"159":2,"160":1,"163":2,"164":2,"213":1,"218":1}}],["nav",{"2":{"184":12}}],["navigating",{"2":{"158":1}}],["navigational",{"0":{"184":1}}],["navigation",{"0":{"149":1}}],["navigate",{"2":{"110":1,"138":1,"160":1,"163":1,"213":1}}],["natively",{"0":{"141":1,"142":1}}],["name=",{"2":{"171":1}}],["name>",{"2":{"138":1,"144":3,"233":3}}],["names",{"2":{"37":1,"180":1,"216":1,"232":1}}],["name",{"2":{"36":1,"55":2,"59":1,"60":1,"90":1,"95":1,"125":1,"160":8,"161":1,"165":6,"174":1,"184":2,"188":4,"189":3,"197":1,"199":1,"200":1,"218":2,"232":7,"233":5}}],["named",{"2":{"8":1,"193":3,"200":1,"217":1,"226":1}}],["npm",{"2":{"95":1,"121":2,"141":4,"142":3,"144":2,"170":1,"181":1}}],["npx",{"2":{"56":3,"115":2,"141":2}}],["num",{"2":{"201":1}}],["number",{"2":{"9":1,"11":2,"51":1,"69":3,"71":1,"72":1,"73":1,"90":2,"199":5,"229":2,"232":1}}],["null",{"2":{"201":2,"209":1}}],["nullable",{"2":{"61":5}}],["nullslast",{"2":{"61":2}}],["nulls",{"0":{"61":1},"2":{"61":3}}],["no",{"2":{"98":1,"130":1,"144":1,"178":1,"185":1,"233":1}}],["normalized",{"2":{"88":1}}],["normalizes",{"2":{"87":1}}],["normalize",{"2":{"87":1}}],["now",{"2":{"58":5,"143":1}}],["non",{"2":{"20":1,"60":1,"61":1}}],["notifications",{"0":{"185":1}}],["notice",{"2":{"70":1,"197":1}}],["notable",{"0":{"173":1}}],["notes",{"0":{"198":1}}],["note",{"2":{"58":1,"122":1}}],["notempty",{"2":{"55":2}}],["notfound",{"2":{"23":2,"25":1,"32":1,"65":1,"66":1}}],["not",{"0":{"24":1,"141":1,"142":1},"1":{"25":1},"2":{"15":1,"24":1,"25":1,"40":1,"58":1,"59":1,"63":1,"66":1,"72":1,"81":2,"84":3,"95":1,"106":1,"107":2,"143":2,"146":1,"154":3,"159":1,"162":1,"163":1,"174":1,"184":2,"185":1,"191":1,"199":1,"201":1,"213":1,"218":1,"220":1,"229":1}}],["nodes",{"0":{"144":1},"2":{"110":1,"142":1,"212":3,"213":1}}],["nodemon",{"2":{"103":1}}],["nodejs",{"0":{"68":1,"69":1,"70":2,"81":1},"1":{"70":1,"79":2,"80":2,"81":2,"82":2,"83":2,"84":2},"2":{"69":2,"70":2,"75":1,"76":3,"77":3,"79":8,"81":1,"84":1}}],["node",{"0":{"67":1,"233":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"3":1,"4":1,"8":1,"9":3,"35":1,"36":1,"68":1,"70":1,"71":1,"72":1,"77":2,"80":1,"85":1,"88":1,"95":1,"109":1,"111":1,"128":1,"131":1,"133":1,"138":2,"141":4,"143":1,"144":3,"159":2,"160":1,"163":4,"173":1,"213":4,"233":3}}],["neither",{"2":{"201":1}}],["nearing",{"2":{"72":1,"73":1}}],["network",{"2":{"60":1,"131":1,"136":1,"185":1,"212":1,"220":1}}],["negatively",{"2":{"60":1}}],["newkey",{"2":{"110":1,"142":1}}],["newuser",{"2":{"27":4,"29":4}}],["new",{"2":{"20":1,"36":2,"40":1,"48":1,"56":1,"59":1,"62":1,"65":1,"76":4,"90":1,"91":1,"100":1,"138":1,"153":2,"154":2,"165":2,"174":2,"195":1,"196":1,"197":1,"226":2}}],["next",{"2":{"17":3,"25":4,"27":4,"29":4,"31":4,"63":1,"65":3,"66":2,"81":2}}],["necessary",{"2":{"4":1,"49":1,"158":1,"232":1}}],["needs",{"2":{"154":1,"162":1,"218":1}}],["needing",{"2":{"154":1}}],["needed",{"2":{"37":1,"161":1}}],["need",{"2":{"1":1,"9":1,"16":1,"17":1,"42":1,"47":1,"53":1,"107":1,"144":1,"171":1,"183":1,"233":1}}],["vs",{"0":{"176":1}}],["vscode",{"2":{"121":4}}],["vuetify",{"2":{"170":1}}],["vueuse",{"2":{"170":1}}],["vuestic",{"0":{"173":1},"2":{"170":1,"171":1,"172":3,"173":1,"208":1}}],["vue3",{"2":{"170":1}}],["vue",{"0":{"148":1},"2":{"165":1,"170":1,"171":1,"179":1}}],["vpn",{"2":{"143":1}}],["v",{"2":{"119":1,"190":1,"193":1,"195":1,"196":2,"197":1,"199":2}}],["volumes",{"2":{"111":1,"119":1}}],["v8",{"2":{"76":1,"77":3}}],["vmdata",{"2":{"75":1}}],["vmsize",{"2":{"75":1}}],["vmrss",{"2":{"75":1}}],["v23",{"2":{"70":1}}],["verifying",{"2":{"218":1}}],["verify",{"2":{"218":1}}],["verification",{"2":{"164":1}}],["verified",{"2":{"65":1}}],["verifies",{"2":{"63":1,"218":1}}],["very",{"2":{"162":1}}],["version",{"2":{"77":1,"95":1,"143":2,"174":1}}],["versions",{"2":{"15":1}}],["vite",{"2":{"135":2,"142":2,"167":3,"168":1,"169":1,"170":1,"174":1,"177":2,"183":2}}],["visualizer",{"2":{"170":1}}],["visualizes",{"2":{"137":1}}],["visualize",{"2":{"132":1,"170":1}}],["visualizing",{"2":{"127":1}}],["visited",{"2":{"172":1}}],["visits",{"2":{"146":1}}],["visit",{"2":{"95":1}}],["view",{"2":{"88":1,"118":1,"138":1,"163":1,"178":1,"212":1}}],["virtual",{"2":{"75":2,"143":2}}],["violations",{"2":{"26":1}}],["violation",{"0":{"26":1},"1":{"27":1}}],["via",{"2":{"2":1,"128":2,"163":1,"192":1,"194":1,"196":1,"197":6,"199":2,"213":1}}],["vaselect",{"2":{"196":1}}],["var",{"2":{"172":7}}],["various",{"2":{"128":1,"175":1,"182":1}}],["variety",{"2":{"101":1}}],["variations",{"2":{"84":1}}],["variable",{"2":{"56":1,"174":2,"226":2}}],["variables",{"0":{"5":1,"7":1,"113":1},"1":{"6":1,"7":1},"2":{"1":1,"2":1,"3":2,"4":1,"5":2,"7":2,"8":4,"113":2,"172":1,"173":1,"174":6,"176":1,"177":1}}],["va",{"2":{"171":1,"172":33,"189":2,"191":2,"192":1,"193":2,"197":4,"199":6,"207":1,"208":1}}],["val",{"2":{"195":1,"196":1,"197":1}}],["value",{"2":{"55":1,"59":1,"81":1,"84":2,"141":2,"164":1,"174":3,"189":2,"190":1,"191":2,"192":1,"195":18,"196":21,"197":29,"198":1,"199":4}}],["values",{"2":{"2":1,"8":1,"55":1,"81":1,"165":1,"167":1,"174":5,"175":2,"176":1,"191":1,"197":4,"220":1,"227":2,"231":1}}],["validity",{"2":{"162":2}}],["valid",{"2":{"54":1,"63":1,"65":1,"95":1,"162":4,"164":1}}],["validating",{"2":{"216":1,"222":2}}],["validationresult",{"2":{"52":1}}],["validation",{"0":{"47":1,"51":1},"1":{"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1},"2":{"47":3,"48":4,"49":4,"51":1,"52":2,"53":2,"54":5,"98":1,"105":2,"162":1,"164":2,"216":1,"218":1,"221":1,"222":1}}],["validates",{"2":{"105":1,"163":1,"213":1}}],["validated",{"2":{"49":1}}],["validate",{"0":{"53":1},"2":{"47":1,"49":2,"53":3,"54":2,"55":3,"105":2,"160":1}}],["validators",{"2":{"47":1,"53":1,"55":1}}],["validator",{"0":{"52":1},"2":{"47":3,"49":1,"52":1,"53":1,"55":1}}],["guarantees",{"2":{"232":1}}],["guard",{"0":{"149":1}}],["guess",{"2":{"161":1}}],["guide",{"0":{"108":1,"124":1,"139":1},"1":{"109":1,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"118":1,"119":1,"120":1,"121":1,"122":1,"123":1,"124":1,"125":2,"126":2,"140":1,"141":1,"142":1,"143":1,"144":1},"2":{"108":1,"139":1,"140":1,"177":2}}],["guidelines",{"0":{"225":1},"1":{"226":1,"227":1,"228":1,"229":1,"230":1},"2":{"46":1}}],["global",{"2":{"105":2,"106":1,"185":1}}],["gzip",{"2":{"105":3,"106":1}}],["gt",{"2":{"95":3,"141":3,"144":4,"160":20,"165":3,"171":3,"180":1,"191":2,"197":3,"199":4,"207":1,"208":1,"232":5,"233":4}}],["gauarnteed",{"2":{"154":1}}],["gauge",{"2":{"69":1,"71":1,"72":1,"73":1,"75":1,"76":1,"77":1,"78":1,"81":1}}],["garbage",{"2":{"68":1,"76":2}}],["gc",{"0":{"68":1},"2":{"68":1}}],["google",{"2":{"177":8}}],["good",{"2":{"60":1}}],["go",{"2":{"141":1}}],["goes",{"0":{"106":1},"2":{"220":1,"222":1,"226":2}}],["goal",{"2":{"64":1,"157":1}}],["gotchas",{"0":{"57":1},"1":{"58":1,"59":1,"60":1,"61":1,"62":1}}],["gives",{"2":{"212":1}}],["given",{"2":{"12":1,"160":2}}],["git",{"2":{"109":1,"110":1,"144":2,"165":4,"233":2}}],["github",{"2":{"59":1,"110":1,"141":1,"165":1,"171":1}}],["grow",{"2":{"232":1}}],["grows",{"2":{"138":1}}],["group",{"2":{"143":1,"160":1}}],["grep",{"2":{"125":1}}],["greater",{"2":{"51":1}}],["granular",{"2":{"212":1}}],["granting",{"2":{"160":1}}],["granted",{"2":{"65":1,"162":1,"218":2}}],["grafana",{"0":{"132":1},"1":{"133":1,"134":1,"135":1,"136":1},"2":{"127":1,"132":1,"133":1,"134":6,"135":2,"136":1,"137":1,"138":1}}],["grace",{"2":{"11":2,"12":1}}],["gracefully",{"2":{"10":1,"11":1,"12":1}}],["graceful",{"2":{"9":1}}],["genome",{"2":{"183":2,"201":1}}],["genomebrowser",{"2":{"183":1}}],["genkeys",{"2":{"107":1,"110":1,"141":1}}],["generation",{"0":{"161":1}}],["generating",{"2":{"42":1,"107":1,"109":1}}],["generates",{"2":{"95":1}}],["generate",{"2":{"95":2,"110":1,"141":3,"143":1}}],["generated",{"2":{"62":1,"95":2,"160":1,"161":1}}],["generic",{"2":{"19":1,"20":1,"66":1,"185":1}}],["getbymatchingusername",{"2":{"190":1}}],["getall",{"2":{"189":1}}],["getactiveresourcesinfo",{"2":{"69":1}}],["getrecords",{"2":{"185":1}}],["getp2",{"2":{"184":1}}],["getp1",{"2":{"184":1}}],["getting",{"0":{"167":1},"1":{"168":1,"169":1}}],["getheapspacestatistics",{"2":{"76":1}}],["get",{"2":{"8":1,"17":2,"31":4,"65":2,"66":1,"87":1,"88":10,"125":1,"167":2}}],["g",{"2":{"1":1,"3":1,"4":1,"8":1,"20":2,"26":1,"36":1,"60":1,"82":1,"84":2,"87":2,"88":1}}],["fn",{"2":{"191":1}}],["fn=",{"2":{"189":1}}],["f6f6f6",{"2":{"172":1}}],["ffffff",{"2":{"172":2}}],["ffd43a",{"2":{"172":1}}],["ffc5274e",{"2":{"172":1}}],["f",{"2":{"118":1,"125":1,"141":1,"144":1,"233":2}}],["flag",{"2":{"183":1}}],["flags",{"0":{"183":1},"2":{"170":1}}],["flask",{"2":{"171":2}}],["flow",{"0":{"105":1,"147":1,"150":1,"151":1,"152":1,"153":1},"1":{"106":1},"2":{"163":1,"213":1}}],["flexibility",{"2":{"51":1,"103":1}}],["fp",{"2":{"96":1}}],["framework",{"2":{"98":1,"100":1,"102":1,"103":1}}],["fragmented",{"2":{"93":1}}],["fromnow",{"2":{"182":1}}],["from",{"2":{"1":1,"2":1,"5":1,"19":1,"30":1,"40":1,"47":1,"49":1,"64":1,"68":1,"69":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"81":2,"82":1,"84":2,"87":1,"92":2,"96":1,"107":1,"128":2,"131":1,"137":1,"157":2,"158":3,"160":5,"163":1,"164":1,"165":1,"167":2,"168":1,"172":1,"174":2,"176":1,"182":2,"183":3,"184":2,"185":1,"195":1,"196":1,"197":1,"199":1,"209":1,"212":3,"213":2,"218":1,"221":1,"222":1,"226":2,"227":1,"233":2}}],["fd",{"2":{"72":2}}],["fds",{"0":{"72":1,"73":1},"2":{"72":1,"73":2}}],["fs",{"2":{"72":1}}],["full",{"2":{"100":1}}],["fully",{"2":{"45":1,"98":1}}],["future",{"2":{"58":1,"143":1}}],["further",{"2":{"53":1,"162":1}}],["functionality",{"2":{"157":1,"160":1,"161":1}}],["functions",{"2":{"40":1,"52":1,"98":1,"100":1,"107":2}}],["function",{"2":{"9":1,"10":1,"11":4,"35":1,"36":4,"40":3,"49":2,"53":1,"54":1,"59":1,"66":1,"86":1,"91":1,"153":1,"161":1,"182":1,"188":1,"191":1,"198":2,"199":3,"209":2}}],["fair",{"2":{"144":1,"232":2,"233":1}}],["failing",{"2":{"216":1,"220":1,"223":4}}],["failed",{"2":{"90":2,"172":1,"220":7,"223":2}}],["failures",{"2":{"90":1,"105":1,"106":1}}],["fails",{"2":{"53":1,"54":2,"154":1,"216":1}}],["fail",{"2":{"42":1,"43":1,"141":1}}],["fastqc",{"2":{"143":1}}],["faster",{"2":{"72":1,"137":1}}],["familiar",{"2":{"98":1}}],["family",{"2":{"95":1,"181":1}}],["facilitate",{"2":{"98":1,"160":1}}],["false",{"2":{"55":2,"58":1,"177":4,"178":1,"184":1,"191":2,"199":1,"209":1}}],["fallback",{"2":{"32":1}}],["falling",{"2":{"4":1}}],["fetched",{"2":{"199":1}}],["fetches",{"2":{"164":1}}],["fetchfn",{"2":{"195":2,"196":2,"197":2}}],["fetchquery",{"2":{"195":2,"196":2,"197":2}}],["fetch",{"2":{"165":1,"185":1}}],["fetching",{"2":{"60":1,"194":1}}],["feedback",{"2":{"14":1}}],["feature",{"0":{"101":1,"183":1},"2":{"9":1,"14":1,"34":2,"47":2,"49":1,"170":1,"183":3}}],["features",{"0":{"19":1,"133":1,"170":1},"2":{"9":1,"98":1,"101":1,"111":1,"183":2,"194":1}}],["font",{"2":{"181":1,"189":1}}],["fontsource",{"2":{"181":3}}],["fonts",{"0":{"181":1},"2":{"181":1}}],["folders",{"2":{"121":1}}],["follows",{"2":{"81":1,"160":2}}],["follow",{"2":{"33":1,"49":1,"118":1}}],["following",{"2":{"3":1,"4":1,"7":1,"11":1,"32":1,"46":1,"88":1,"128":1,"162":1,"177":2,"215":1,"216":1,"217":1,"220":1}}],["focus",{"2":{"54":1,"76":1,"101":1,"172":1}}],["found",{"0":{"24":1},"1":{"25":1},"2":{"24":1,"25":1,"229":1}}],["foobar",{"2":{"174":2}}],["footprint",{"2":{"75":1}}],["foo",{"2":{"8":1,"20":1}}],["foreach",{"2":{"195":2,"196":2,"197":2}}],["fork",{"2":{"165":1,"232":1}}],["forking",{"2":{"11":1,"38":1,"42":1,"46":1}}],["form",{"2":{"164":1}}],["format=requirements",{"2":{"231":1}}],["formatfn",{"2":{"197":2,"199":2}}],["formatted",{"2":{"197":7}}],["formatting",{"0":{"197":1},"2":{"199":2,"200":1}}],["formatduration",{"2":{"182":1}}],["formats",{"2":{"182":2,"197":1}}],["format",{"2":{"7":1,"47":1,"55":1,"86":1,"88":1,"105":2,"199":2}}],["forward",{"2":{"144":1,"233":1}}],["forwarded",{"2":{"144":1,"208":1,"233":1}}],["forwards",{"2":{"23":1,"135":1,"164":1}}],["forbidden",{"2":{"65":1}}],["for",{"0":{"8":1,"40":1,"146":1,"152":1,"153":1},"2":{"1":1,"2":2,"3":1,"8":1,"9":2,"10":1,"11":3,"13":1,"17":1,"20":2,"32":1,"34":1,"35":1,"36":2,"37":1,"40":1,"44":1,"46":2,"49":1,"53":1,"54":2,"56":1,"59":1,"60":1,"61":2,"63":1,"66":1,"76":1,"77":3,"85":2,"86":1,"87":1,"90":1,"93":1,"95":1,"100":1,"101":1,"102":1,"105":2,"106":1,"107":4,"108":1,"109":3,"110":2,"111":3,"113":1,"121":1,"122":1,"123":1,"127":2,"135":1,"138":3,"139":1,"142":1,"143":3,"153":1,"154":2,"160":1,"161":1,"162":1,"164":2,"167":1,"174":1,"175":1,"181":1,"183":2,"184":2,"191":3,"193":2,"194":2,"195":3,"196":4,"197":6,"199":9,"200":4,"207":1,"213":2,"215":1,"216":2,"217":5,"220":3,"223":3,"226":2,"230":1,"232":2}}],["filtering",{"2":{"200":1}}],["filtervalue",{"2":{"196":5}}],["filterquery",{"2":{"196":3}}],["filters>",{"2":{"196":1}}],["filtersuffix",{"2":{"195":3,"196":3,"197":3}}],["filters",{"0":{"196":1},"2":{"194":1,"196":2,"200":1}}],["filtered",{"2":{"193":1}}],["filtered=",{"2":{"189":1,"193":1}}],["filterfn",{"2":{"189":2}}],["filter",{"2":{"188":1,"189":1,"191":3,"196":2,"233":1}}],["filename",{"2":{"36":1}}],["filepath",{"2":{"36":1}}],["file",{"0":{"6":1,"164":1},"2":{"5":1,"7":1,"8":3,"13":2,"35":1,"36":3,"39":1,"40":1,"56":2,"72":4,"73":2,"87":1,"90":1,"95":1,"107":1,"129":1,"138":1,"141":2,"142":2,"143":1,"156":1,"158":1,"159":3,"160":3,"162":7,"163":6,"164":12,"167":1,"170":1,"174":4,"212":1,"213":5,"215":3,"216":6,"217":13,"218":5,"219":5,"220":4,"221":6,"222":2,"223":1,"226":3,"227":1,"233":1}}],["filesystem",{"2":{"216":1,"220":1,"222":1,"223":1}}],["files",{"0":{"3":1},"1":{"4":1},"2":{"0":1,"2":1,"3":1,"40":1,"95":1,"107":1,"110":1,"113":1,"134":1,"137":1,"143":1,"157":1,"158":4,"159":3,"160":5,"162":3,"163":4,"165":1,"174":1,"201":1,"211":2,"212":7,"213":4,"216":2,"220":2,"221":1,"223":1,"226":1}}],["fixed",{"2":{"176":2}}],["five",{"2":{"153":1}}],["field",{"2":{"61":5,"193":2,"195":1,"196":1,"197":5}}],["fields",{"2":{"54":1,"58":1,"61":4}}],["first",{"2":{"61":2,"115":1,"154":1,"216":1}}],["fine",{"2":{"174":1}}],["findmany",{"2":{"60":2}}],["finds",{"2":{"52":1}}],["findactiveuserby",{"2":{"17":2,"63":1,"65":1,"66":1}}],["final",{"2":{"32":1,"45":1}}],["fit",{"2":{"14":1}}],["fits",{"0":{"2":1}}],["b3d4fc",{"2":{"172":1}}],["bfpq",{"2":{"147":1}}],["breadcrumb",{"2":{"184":1}}],["breadcrumbs",{"0":{"184":1},"2":{"184":1}}],["browse",{"2":{"160":1}}],["browser=true",{"2":{"183":1}}],["browsers",{"2":{"158":1,"163":1,"212":1,"213":1}}],["browser",{"2":{"154":1,"157":1,"163":1,"164":1,"168":1,"182":1,"183":1,"211":1,"220":1}}],["branch",{"2":{"144":1,"233":1}}],["brittle",{"2":{"1":1}}],["blobs",{"2":{"60":1}}],["blocking",{"2":{"83":1}}],["blocked",{"2":{"80":1,"83":1}}],["blocks",{"2":{"17":1}}],["block",{"2":{"16":1,"72":1,"77":1,"172":1,"184":1}}],["binarystatuschip",{"0":{"205":1},"1":{"206":1},"2":{"205":1}}],["bit",{"2":{"160":1}}],["bigint",{"2":{"55":2}}],["bioloopuser",{"2":{"144":1,"233":1}}],["bioloop",{"2":{"36":1,"108":1,"110":2,"138":1,"139":1,"140":1,"144":2,"163":1,"165":8,"173":1,"211":1,"213":1,"232":3,"233":2}}],["bold",{"2":{"189":1}}],["border",{"2":{"172":1}}],["bound",{"2":{"77":1,"162":1}}],["bool",{"2":{"191":1}}],["boolean",{"2":{"55":2,"58":1,"191":3,"199":2,"206":1}}],["bootstrap",{"2":{"43":1}}],["boilerplate",{"2":{"52":1}}],["body",{"2":{"27":2,"29":4,"40":1,"47":1,"49":1,"51":7,"52":6,"53":6,"54":3,"55":5,"105":5,"106":1,"181":1}}],["both",{"2":{"10":1,"58":1,"154":1,"157":1,"174":1,"197":2,"226":1}}],["bottlenecks",{"2":{"69":1,"76":1,"80":1,"85":1,"127":1}}],["bottleneck",{"2":{"9":1,"83":1}}],["builtin",{"2":{"172":1}}],["built",{"2":{"100":1}}],["building",{"2":{"126":1}}],["buildorderbyobject",{"2":{"61":1}}],["build",{"2":{"9":1,"170":1}}],["bucket",{"2":{"88":8}}],["buckets",{"2":{"87":2}}],["bundled",{"2":{"160":1}}],["bundle",{"2":{"86":1,"93":1,"160":13,"161":3,"170":1}}],["bursts",{"2":{"84":1}}],["button>",{"2":{"172":1}}],["button",{"2":{"172":1,"192":1,"203":1}}],["but",{"2":{"44":1,"45":1,"72":1,"82":1,"107":1,"141":1,"163":1,"213":1,"232":1}}],["business",{"2":{"20":1,"54":1,"105":4,"107":3}}],["batch",{"2":{"199":2}}],["batchingquery",{"2":{"195":2,"196":2,"197":2}}],["batches",{"2":{"194":1}}],["balancing",{"2":{"107":1}}],["basic",{"0":{"188":1,"195":1}}],["bashpython",{"2":{"232":2}}],["bashpoetry",{"2":{"231":1}}],["bashssh",{"2":{"144":1,"233":1}}],["bashsudo",{"2":{"123":1}}],["bashvite",{"2":{"142":1}}],["bashnode",{"2":{"141":1}}],["bashnetstat",{"2":{"125":1}}],["bashcolo23>",{"2":{"144":2,"233":2}}],["bashcd",{"2":{"141":1,"142":1,"143":3,"144":4,"231":1,"232":1,"233":4}}],["bashcp",{"2":{"110":1,"167":1}}],["bashrc",{"2":{"126":1}}],["bashbin",{"2":{"126":1}}],["bashdocker",{"2":{"110":1,"115":1,"117":1,"118":1,"119":1,"125":2,"138":2,"141":1}}],["bash",{"2":{"110":1,"115":2,"121":1,"122":2,"125":1,"126":1}}],["bashgit",{"2":{"110":1,"165":2}}],["bashapi>",{"2":{"88":1}}],["based",{"2":{"19":1,"43":1,"64":2,"84":1,"103":1,"154":1,"162":1,"170":1,"174":1,"185":1,"191":1,"217":1,"218":1,"226":1}}],["base",{"2":{"6":1,"96":1,"165":1,"169":1,"181":1}}],["badge",{"2":{"205":1}}],["badrequest",{"2":{"29":1}}],["bad",{"2":{"28":1,"53":1,"54":1,"60":1}}],["bar",{"2":{"8":1,"20":1}}],["backed",{"2":{"174":1}}],["backend",{"2":{"98":1}}],["background",{"2":{"34":1,"172":4}}],["back",{"2":{"4":1,"54":1}}],["began",{"2":{"217":1}}],["begins",{"2":{"216":2}}],["beneath",{"2":{"191":1,"199":1}}],["benefits",{"0":{"48":1,"94":1}}],["because",{"2":{"159":1}}],["becoming",{"2":{"9":1}}],["becomes",{"2":{"1":1,"154":1}}],["been",{"2":{"98":1,"101":1,"103":1,"162":1,"183":1,"217":2,"221":2,"222":1,"223":2}}],["bearer",{"2":{"63":1,"218":2,"219":1}}],["behavior",{"2":{"61":1,"114":1,"127":1,"143":1,"174":1,"176":1}}],["behaves",{"2":{"0":1}}],["between",{"2":{"55":1,"80":1,"143":1,"146":1,"174":1,"176":1,"199":1}}],["best",{"0":{"37":1,"102":1},"2":{"98":1,"103":1}}],["being",{"2":{"19":1,"83":1,"194":2,"197":1,"199":1,"215":1,"216":2,"218":1,"220":2}}],["below",{"2":{"15":1,"197":1,"199":1}}],["beforeapplicationshutdown",{"0":{"44":1},"2":{"46":1}}],["beforeapplicationfork",{"0":{"42":1},"2":{"11":1,"36":1,"46":1}}],["before",{"0":{"150":1},"2":{"11":1,"22":1,"25":1,"27":1,"29":1,"31":1,"38":1,"42":1,"44":1,"46":1,"63":1,"106":1,"153":1,"158":1,"168":1,"216":2,"217":1,"218":2,"221":1,"222":1,"223":1}}],["be",{"2":{"1":1,"51":2,"65":1,"66":1,"82":1,"85":1,"90":1,"93":1,"98":1,"103":1,"107":1,"113":1,"114":1,"130":1,"134":1,"144":1,"146":1,"154":1,"158":4,"160":2,"162":6,"175":1,"179":1,"183":3,"184":1,"185":1,"191":3,"193":2,"196":2,"197":5,"198":2,"199":13,"207":2,"208":1,"212":4,"217":1,"218":2,"219":1,"220":3,"229":1,"232":1,"233":1}}],["by=",{"2":{"188":1,"190":1,"195":1,"196":1,"197":1}}],["bytes",{"2":{"75":3,"76":3,"77":3}}],["by",{"0":{"8":1},"2":{"1":1,"2":1,"5":1,"33":1,"46":1,"47":2,"52":1,"53":1,"54":1,"56":2,"61":2,"62":1,"65":1,"66":1,"67":1,"68":1,"72":1,"73":1,"74":1,"77":2,"80":1,"83":1,"84":1,"86":1,"98":1,"103":1,"114":1,"132":1,"139":1,"159":1,"161":1,"162":2,"164":2,"167":1,"171":1,"174":3,"175":1,"178":1,"183":1,"189":1,"191":3,"196":1,"198":1,"209":1,"212":1,"218":1,"221":1,"222":1,"229":1}}],["lt",{"2":{"58":2,"95":3,"141":3,"144":4,"160":20,"165":3,"171":3,"180":1,"197":3,"199":4,"207":1,"208":1,"232":5,"233":4}}],["lang=",{"2":{"178":3,"184":1}}],["layouts",{"2":{"170":1}}],["layers",{"2":{"100":1}}],["layer",{"2":{"47":1,"107":1}}],["layered",{"2":{"32":1,"174":1}}],["later",{"2":{"162":1}}],["latest",{"2":{"144":1,"233":1}}],["label=",{"2":{"190":1,"196":1}}],["label",{"2":{"184":3,"191":2,"195":2,"196":2,"197":2,"199":1,"206":2}}],["labels",{"2":{"90":1,"206":1}}],["labelnames",{"2":{"90":1}}],["labeled",{"2":{"88":1}}],["lag",{"0":{"81":1},"2":{"79":8,"81":1,"82":1,"83":2,"84":5}}],["last",{"0":{"61":1},"2":{"18":1,"61":2}}],["large",{"2":{"9":1,"76":3,"77":1,"143":1,"163":1,"212":1}}],["let",{"2":{"144":1,"185":1,"195":1,"196":1,"197":1,"233":1}}],["le=",{"2":{"88":8}}],["leaving",{"2":{"184":1}}],["learning",{"2":{"98":1}}],["leaks",{"2":{"72":1,"75":1,"76":1,"77":1}}],["least",{"2":{"51":1}}],["leads",{"2":{"51":1}}],["leading",{"2":{"1":1,"16":1,"47":1}}],["length",{"2":{"51":1,"55":1,"72":1}}],["less",{"2":{"47":1,"223":1}}],["leveraging",{"2":{"46":1,"52":1}}],["leverages",{"2":{"9":1}}],["level",{"2":{"32":1,"183":1}}],["lighten",{"2":{"172":1}}],["lightweight",{"2":{"163":1,"213":1}}],["lived",{"2":{"76":1,"162":1}}],["likely",{"2":{"174":1}}],["like",{"2":{"58":1,"59":1,"175":1,"216":1}}],["lifecycle",{"0":{"38":1,"39":1,"40":1},"1":{"39":1,"40":2,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"2":{"36":1,"38":2,"39":2,"40":4,"46":2}}],["linting",{"0":{"96":1,"121":1},"2":{"98":1,"103":1}}],["linked",{"2":{"215":2}}],["links",{"2":{"184":2}}],["link",{"2":{"36":1,"158":1,"162":1,"172":5}}],["line",{"2":{"2":1,"4":1,"165":1}}],["library",{"2":{"13":1,"35":1,"64":1,"85":1,"86":1,"128":1,"171":1}}],["listen",{"2":{"11":1,"13":1,"125":1}}],["list",{"2":{"11":1,"55":1,"118":1,"160":1}}],["limited",{"2":{"160":1}}],["limitations",{"0":{"84":1,"158":1,"159":1,"212":1},"1":{"159":1,"161":1},"2":{"143":1}}],["limit",{"0":{"229":1},"2":{"11":1,"55":1,"69":1,"72":1,"73":1,"160":1,"195":3,"196":3,"197":3}}],["limits",{"2":{"9":1,"10":1,"55":1,"73":1}}],["looks",{"2":{"223":1}}],["looked",{"2":{"198":1}}],["loop",{"2":{"69":2,"72":1,"77":1,"80":1,"81":2,"82":1,"83":3,"84":2}}],["lot",{"2":{"171":1}}],["lodash",{"2":{"59":1,"96":1,"195":1,"196":1,"197":1}}],["long",{"2":{"37":1,"51":1,"76":1,"80":1,"83":1,"146":1}}],["logo",{"2":{"165":1}}],["login",{"0":{"150":2,"151":2},"2":{"153":1,"177":1}}],["logic",{"0":{"149":1},"2":{"1":2,"10":1,"20":1,"36":2,"40":3,"47":1,"48":1,"51":1,"54":2,"101":1,"105":5,"107":3}}],["loglevel",{"2":{"144":1,"232":1,"233":1}}],["logging",{"0":{"62":1,"215":1},"2":{"45":1,"62":1,"98":1,"101":1,"102":1,"146":1}}],["loggers",{"2":{"37":1}}],["logger",{"2":{"36":5}}],["logged",{"0":{"152":1},"2":{"14":1,"42":1,"43":1,"44":1,"45":1,"215":1,"216":1}}],["logs",{"2":{"12":2,"20":1,"30":1,"36":1,"118":4,"125":2,"126":1,"163":1,"164":1,"213":1}}],["log",{"2":{"8":1,"12":2,"36":2,"43":1,"62":2,"138":1,"146":1,"188":1,"189":1,"215":5,"217":2}}],["locahost",{"2":{"88":1}}],["locally",{"0":{"143":1},"2":{"93":1,"121":1,"141":1,"143":1,"144":3,"233":3}}],["local",{"0":{"139":1,"232":1},"1":{"140":1,"141":1,"142":1,"143":1,"144":1},"2":{"58":1,"108":1,"109":1,"121":1,"139":1,"140":1,"141":1,"143":1,"144":5,"153":1,"154":4,"182":1,"233":5}}],["localhost",{"2":{"8":1,"122":1,"133":1,"141":2,"142":3,"144":3,"167":4,"168":1,"169":1,"177":2,"233":3}}],["location",{"0":{"39":1},"1":{"40":1},"2":{"160":1}}],["located",{"2":{"2":1,"39":1,"129":1}}],["loaded",{"2":{"227":1}}],["loadresults",{"2":{"195":4,"196":4,"197":4}}],["loadnextpage",{"2":{"195":2,"196":2,"197":2}}],["loading",{"0":{"7":1},"2":{"2":1,"191":2,"199":3}}],["load",{"2":{"0":1,"5":1,"7":1,"8":1,"9":1,"107":1,"194":2,"195":1,"196":1,"197":1,"199":2,"226":1,"231":1}}],["hyphens",{"2":{"218":1}}],["html",{"2":{"170":1,"172":1,"178":3,"184":3,"188":1,"189":1,"190":1,"195":1,"196":1,"197":1,"201":1,"203":1,"205":1,"207":1}}],["https",{"2":{"31":2,"95":1,"110":2,"122":2,"133":1,"135":1,"141":1,"142":1,"147":1,"167":2,"168":1,"170":1,"171":2,"172":1,"177":2,"181":1}}],["http",{"0":{"185":1},"2":{"20":2,"24":1,"26":1,"30":1,"47":1,"86":1,"87":1,"88":15,"95":1,"105":3,"122":1,"125":1,"167":2,"168":1,"169":1,"216":1,"218":1}}],["h",{"2":{"144":1,"232":2,"233":1}}],["hsi",{"2":{"143":1}}],["history",{"2":{"209":1}}],["histogram",{"2":{"68":1,"87":1,"88":3}}],["hint",{"2":{"199":1}}],["hierarchical",{"0":{"226":1},"2":{"107":1,"174":1}}],["highlighted",{"2":{"172":1}}],["high",{"2":{"9":1,"81":1,"83":2,"84":1,"87":1}}],["height",{"2":{"199":1}}],["health",{"2":{"85":1,"106":1,"122":2}}],["heaps",{"2":{"77":1}}],["heapsizeandused",{"0":{"77":1}}],["heapspacessizeandused",{"0":{"76":1},"2":{"77":1}}],["heap",{"2":{"75":2,"76":8,"77":6}}],["headers",{"2":{"105":2,"106":1,"154":1,"164":1}}],["header",{"2":{"63":1,"135":1,"154":1,"164":1,"218":1}}],["heavy",{"2":{"9":1,"37":1}}],["here",{"2":{"36":1,"49":1,"77":1,"217":1}}],["helper",{"2":{"228":1}}],["helps",{"0":{"137":1},"2":{"47":1,"73":1,"83":1,"209":1}}],["help",{"2":{"14":1,"66":1,"88":1,"90":1,"108":1}}],["happen",{"2":{"216":1}}],["having",{"2":{"146":1}}],["have",{"2":{"81":1,"84":1,"98":1,"101":1,"103":1,"143":1,"158":1,"159":1,"162":2,"163":1,"174":1,"182":1,"184":1,"212":1,"213":1,"217":1,"219":1,"221":1,"223":2,"226":1}}],["handy",{"2":{"52":1}}],["handling",{"0":{"14":1,"185":1},"1":{"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":1},"2":{"14":4,"32":1,"33":2,"42":1,"43":1,"44":1,"45":1,"48":2,"53":2,"54":1,"58":1,"100":2,"105":1}}],["handleuserselect",{"2":{"189":1}}],["handleselect",{"2":{"195":2,"196":2,"197":2}}],["handles",{"0":{"70":1},"1":{"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"26":1,"30":1,"54":1,"126":1}}],["handleremove",{"2":{"195":2,"196":2,"197":2}}],["handlers",{"0":{"22":1},"1":{"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1},"2":{"15":1,"17":1,"22":1,"32":2,"48":1,"51":1,"53":1,"54":1,"105":2,"106":1}}],["handler",{"0":{"15":1,"18":1,"20":1,"23":1,"24":1,"26":1,"28":1,"30":1},"1":{"16":1,"17":1,"19":1,"20":1,"21":1,"25":1,"27":1,"29":1,"31":1},"2":{"15":1,"16":1,"17":1,"18":1,"20":1,"22":1,"47":1,"49":1,"51":1,"54":2,"95":1,"105":5,"106":2,"185":1}}],["handledatasetselect",{"2":{"188":2}}],["handled",{"2":{"14":1,"32":1,"56":1}}],["handle",{"2":{"9":1,"22":1,"46":1,"49":1,"66":1,"105":4,"106":3,"185":1}}],["harder",{"2":{"51":1}}],["hardcoded",{"2":{"176":1}}],["hardcode",{"2":{"1":1}}],["hashes",{"2":{"231":1}}],["has",{"2":{"43":1,"45":2,"64":1,"65":1,"105":2,"143":2,"159":1,"162":1,"164":1,"171":1,"183":1,"194":1,"217":1,"218":1,"221":1,"222":1}}],["hours",{"2":{"220":1,"223":3}}],["house",{"2":{"179":1}}],["houses",{"2":{"107":1}}],["hold",{"2":{"209":1}}],["hover",{"2":{"172":1}}],["hoppscotch",{"2":{"122":2}}],["hot",{"0":{"230":1},"2":{"98":1,"111":1}}],["hooks",{"0":{"38":1,"39":1,"40":1,"41":1},"1":{"39":1,"40":2,"41":1,"42":2,"43":2,"44":2,"45":2,"46":1},"2":{"38":2,"39":1,"46":2,"68":1,"82":1,"84":1}}],["hook",{"2":{"36":1,"40":2,"42":1,"43":1}}],["hosts",{"2":{"163":1}}],["hosted",{"2":{"159":1,"213":1}}],["hostname",{"2":{"141":1,"142":1,"144":1,"165":1,"232":2,"233":1}}],["host",{"0":{"169":1},"2":{"8":1,"95":1,"111":1,"172":1}}],["however",{"2":{"81":1,"154":1,"159":2}}],["how",{"0":{"2":1,"35":1,"137":1},"1":{"36":1,"37":1},"2":{"14":2,"80":1,"85":1,"197":1}}],["hoc",{"2":{"1":1}}],["our",{"2":{"212":2,"216":1,"218":1}}],["outline",{"2":{"205":2}}],["outlined",{"2":{"163":1,"213":1}}],["output",{"2":{"95":2}}],["out",{"2":{"75":1,"110":1,"142":1,"233":1}}],["oidc",{"2":{"177":1}}],["obtains",{"2":{"162":1}}],["obfuscated",{"2":{"160":1}}],["observability",{"2":{"101":1}}],["obj",{"2":{"59":2}}],["objectives",{"0":{"146":1}}],["objective",{"2":{"65":1}}],["objects",{"2":{"55":1,"59":1,"76":3,"77":2,"105":1,"191":1,"215":1}}],["object",{"2":{"11":1,"52":1,"56":1,"76":3,"105":3,"172":1,"191":3,"197":1,"209":1,"215":2,"226":1}}],["occurs",{"2":{"81":2,"163":1,"213":1}}],["old",{"2":{"76":2,"154":1}}],["o",{"2":{"72":1,"144":1,"232":2,"233":1}}],["osmemoryheaplinux",{"0":{"75":1}}],["os",{"2":{"72":1,"73":1,"143":1}}],["owners",{"2":{"165":2}}],["ownership",{"2":{"66":2}}],["owner",{"2":{"65":1}}],["own",{"2":{"64":1,"65":3,"143":1}}],["omit",{"2":{"59":1}}],["other=",{"2":{"197":1}}],["others",{"2":{"143":1,"160":1}}],["otherwise",{"2":{"65":1,"141":1,"154":1,"174":1}}],["other",{"0":{"82":1},"2":{"32":1,"34":1,"65":1,"80":1,"82":1,"95":1,"105":1,"106":1,"161":1,"162":1,"174":1,"182":1,"195":5,"196":8,"197":6,"212":1,"228":1}}],["operation",{"2":{"64":1,"72":1,"105":1}}],["operations",{"0":{"116":1},"1":{"117":1,"118":1,"119":1},"2":{"34":1,"38":1,"54":1,"80":1,"83":2}}],["operator",{"2":{"64":1,"162":1,"178":2,"184":1}}],["opened",{"2":{"192":1}}],["open=",{"2":{"190":1}}],["openid",{"2":{"177":4}}],["openssl",{"2":{"109":1,"110":1,"142":1}}],["openapi",{"0":{"95":1},"2":{"95":1,"102":1}}],["opentelemetry",{"2":{"74":1}}],["open",{"0":{"72":1},"2":{"36":1,"72":1,"73":2,"121":1,"122":1,"168":1,"170":1,"190":1,"192":1}}],["opaque",{"2":{"24":1}}],["optimization",{"2":{"137":1,"232":1}}],["optimized",{"0":{"103":1}}],["optimal",{"2":{"9":1}}],["option",{"2":{"61":1,"82":1,"143":1,"171":2,"196":1}}],["optionally",{"2":{"191":1}}],["optional",{"2":{"11":2,"55":3,"66":1,"231":1}}],["options=",{"2":{"196":1}}],["options",{"0":{"11":1},"2":{"11":1,"121":1}}],["overriding",{"2":{"226":1}}],["overrides",{"2":{"2":1,"3":1,"8":1,"226":1}}],["override",{"2":{"1":1,"8":1}}],["overflows",{"2":{"203":1}}],["over",{"2":{"77":1,"100":1}}],["overhead",{"2":{"60":1,"72":1,"103":1}}],["overall",{"2":{"14":1,"77":1}}],["overview",{"0":{"10":1,"41":1,"140":1,"147":1,"163":1,"166":1,"213":1,"224":1},"1":{"42":1,"43":1,"44":1,"45":1,"164":1,"167":1,"168":1,"169":1,"170":1,"171":1,"172":1,"173":1,"174":1,"175":1,"176":1,"177":1,"178":1,"179":1,"180":1,"181":1,"182":1,"183":1,"184":1,"185":1,"225":1,"226":1,"227":1,"228":1,"229":1,"230":1,"231":1,"232":1,"233":1},"2":{"67":1,"156":1,"157":1}}],["oauth",{"2":{"6":2,"163":1,"164":2,"177":4,"213":1}}],["onmounted",{"2":{"195":1,"196":1,"197":1}}],["onselect",{"2":{"190":2}}],["onlogin",{"2":{"153":1}}],["only",{"2":{"65":2,"81":2,"84":1,"133":1,"135":1,"141":1,"154":1,"158":1,"160":2,"162":2,"165":2,"178":1,"199":1,"203":1,"218":1,"232":1}}],["once",{"2":{"153":1,"183":1,"194":2,"199":1,"216":1,"217":1,"222":1}}],["onapplicationshutdown",{"0":{"45":1},"2":{"46":1}}],["onapplicationbootstrap",{"0":{"43":1},"2":{"40":1,"46":1}}],["ones",{"2":{"223":1}}],["one",{"2":{"42":1,"84":1,"154":1,"162":1,"163":1,"182":1,"192":1,"199":2,"211":1,"215":3}}],["on",{"0":{"141":1,"142":1,"169":1,"178":1,"232":1,"233":1},"2":{"1":1,"8":1,"10":1,"13":1,"19":1,"43":1,"54":1,"61":1,"64":2,"76":1,"82":1,"90":1,"93":1,"101":1,"105":2,"111":1,"138":1,"139":1,"143":3,"144":2,"146":1,"153":1,"154":1,"159":2,"160":1,"163":2,"165":1,"167":2,"174":1,"177":1,"181":1,"185":1,"191":1,"195":1,"196":1,"197":1,"205":2,"206":3,"209":1,"212":1,"213":4,"217":2,"218":2,"226":1,"232":1,"233":2}}],["organization",{"0":{"228":1}}],["organized",{"2":{"137":1}}],["org",{"2":{"165":1,"177":1,"181":1}}],["orphans",{"2":{"126":1}}],["orchestration",{"2":{"126":1}}],["originally",{"2":{"146":1}}],["origin",{"2":{"105":1}}],["orm",{"0":{"56":1},"1":{"57":1,"58":1,"59":1,"60":1,"61":1,"62":1},"2":{"56":1,"107":1}}],["order",{"0":{"148":1},"2":{"4":1,"32":1,"55":1,"61":2,"217":1}}],["or",{"2":{"1":1,"2":1,"14":1,"19":2,"38":1,"40":1,"42":1,"43":1,"44":1,"45":1,"47":2,"49":1,"51":1,"59":1,"61":1,"66":1,"68":1,"72":1,"75":1,"76":1,"77":1,"105":4,"108":1,"109":1,"130":1,"137":1,"141":1,"143":1,"154":2,"158":1,"160":4,"161":1,"174":2,"176":1,"178":1,"183":1,"185":1,"189":1,"191":1,"192":1,"194":2,"197":2,"198":1,"201":2,"211":1,"215":1,"226":1,"232":1}}],["off",{"2":{"205":2,"206":3}}],["offset",{"2":{"195":5,"196":5,"197":5}}],["offers",{"2":{"194":1}}],["offering",{"2":{"98":1,"100":1}}],["offered",{"2":{"47":1}}],["often",{"2":{"51":1,"176":1}}],["of",{"0":{"1":1,"4":1,"39":1,"54":1,"81":1,"82":1,"144":1,"147":1,"156":1},"1":{"40":1},"2":{"0":1,"2":2,"4":1,"9":3,"11":3,"13":1,"14":2,"22":1,"34":1,"36":1,"40":1,"47":1,"53":1,"55":1,"64":1,"65":1,"66":1,"67":1,"68":1,"69":5,"71":1,"72":1,"73":1,"76":3,"77":3,"80":1,"81":2,"85":3,"87":1,"88":1,"90":2,"95":2,"98":2,"100":1,"101":1,"102":1,"103":1,"107":1,"140":2,"141":1,"143":2,"144":1,"146":1,"154":3,"157":2,"160":6,"161":4,"162":7,"163":1,"164":3,"171":1,"174":3,"175":1,"176":1,"191":3,"192":1,"194":2,"195":2,"196":2,"197":6,"198":1,"199":10,"200":1,"206":2,"212":2,"213":1,"216":2,"217":3,"218":2,"220":2,"221":3,"222":3,"227":1,"229":2,"231":1,"232":2,"233":2}}],["wq",{"2":{"209":1}}],["w",{"2":{"196":1}}],["www",{"2":{"177":1}}],["w1",{"2":{"144":1,"232":2,"233":1}}],["we",{"2":{"143":1,"174":1,"218":2}}],["well",{"2":{"137":1,"197":1,"216":1,"220":1}}],["web",{"2":{"91":1,"101":1,"125":1,"140":1,"158":1,"162":1,"163":1,"212":1,"213":1}}],["weakcb",{"2":{"68":1}}],["width",{"2":{"195":2,"196":2,"197":2,"203":1}}],["widget",{"2":{"194":2,"199":3}}],["wiki",{"2":{"141":1}}],["windows",{"2":{"121":1}}],["will",{"2":{"61":1,"62":1,"65":1,"90":1,"108":1,"141":2,"143":2,"146":2,"153":1,"160":2,"178":1,"183":1,"185":1,"191":1,"193":2,"198":1,"199":1,"209":2,"217":1,"219":1,"232":2}}],["within",{"2":{"10":1,"11":1,"12":1,"146":1,"191":1,"217":1}}],["with",{"0":{"50":1,"61":1,"189":1,"232":1,"233":1},"1":{"51":1,"52":1,"53":1,"54":1},"2":{"2":1,"11":1,"13":1,"17":1,"36":1,"37":1,"48":1,"49":1,"52":1,"55":1,"56":2,"60":1,"64":1,"65":2,"66":2,"72":1,"73":1,"77":1,"81":1,"85":1,"86":2,"88":1,"95":1,"96":1,"98":2,"111":3,"133":1,"138":1,"141":2,"143":3,"144":1,"146":1,"153":1,"154":2,"158":2,"160":1,"162":2,"163":4,"164":5,"165":2,"174":1,"177":3,"178":1,"185":1,"191":2,"193":1,"194":1,"198":1,"199":3,"203":1,"205":1,"209":1,"213":2,"216":1,"218":1,"221":2,"226":1,"230":1,"232":3,"233":1}}],["without",{"0":{"130":1},"2":{"1":1,"9":1,"14":1,"16":1,"47":1,"93":1,"98":2,"109":1,"130":1,"146":1,"154":2,"174":1,"183":1,"231":1}}],["wrong",{"0":{"106":1}}],["written",{"2":{"66":1}}],["writing",{"2":{"51":1,"222":1}}],["writes",{"2":{"216":1}}],["write",{"2":{"47":1,"154":1}}],["wrapping",{"2":{"53":1}}],["wraps",{"2":{"47":1,"52":1,"54":1,"105":1}}],["wrap",{"2":{"15":1,"16":1,"17":1,"49":1,"105":1}}],["was",{"2":{"197":1,"220":2,"221":1}}],["watching",{"2":{"209":1}}],["watch",{"2":{"195":1,"196":1,"197":1,"233":1}}],["watcher",{"2":{"195":1,"196":1,"197":1}}],["want",{"2":{"160":2}}],["warning",{"2":{"122":1,"141":2,"172":2,"207":1}}],["warnings",{"2":{"43":1,"62":1}}],["warn",{"2":{"62":1}}],["ways",{"2":{"171":1}}],["way",{"2":{"1":1,"14":1,"85":1,"199":2}}],["won",{"2":{"143":1}}],["work",{"2":{"143":1,"185":1}}],["working",{"2":{"141":1,"143":1}}],["works",{"0":{"35":1},"1":{"36":1,"37":1}}],["workloads",{"2":{"9":1}}],["workers",{"0":{"143":1,"144":1,"232":1,"233":1},"2":{"10":2,"11":2,"12":2,"46":1,"110":2,"113":1,"143":6,"144":4,"163":1,"165":8,"212":2,"213":1,"226":4,"227":1,"228":4,"229":2,"230":4,"231":1,"232":3,"233":5}}],["worker",{"0":{"224":1},"1":{"225":1,"226":1,"227":1,"228":1,"229":1,"230":1,"231":1,"232":1,"233":1},"2":{"9":2,"10":2,"11":4,"12":3,"37":1,"38":1,"42":1,"44":1,"45":1,"92":1,"93":2,"140":1,"144":2,"165":1,"211":1,"213":2,"216":1,"221":1,"222":1,"223":1,"229":2,"230":1,"232":7,"233":2}}],["workflows",{"2":{"141":1,"213":1}}],["workflow",{"2":{"6":1,"141":2,"160":3}}],["would",{"2":{"1":1,"9":1,"16":1,"47":1,"93":1,"130":1,"159":1}}],["who",{"2":{"158":1,"160":3,"162":1,"212":1}}],["whose",{"2":{"143":1,"200":1}}],["which",{"2":{"65":1,"77":1,"82":1,"83":1,"128":1,"143":1,"146":1,"153":1,"154":2,"159":3,"163":1,"167":1,"170":1,"183":1,"197":1,"207":1,"213":1,"216":2,"223":2,"226":2,"229":1}}],["while",{"2":{"4":1,"14":1,"51":1,"54":1,"84":1,"100":1,"103":1,"137":1,"154":1,"157":1,"160":1,"161":1,"176":3,"211":1,"220":1,"233":1}}],["why",{"0":{"16":1}}],["whether",{"2":{"61":1,"64":1,"105":1,"183":1,"199":1}}],["whereas",{"2":{"65":1}}],["where",{"2":{"13":1,"76":5,"159":1,"160":1,"217":1}}],["whenever",{"2":{"182":1}}],["when",{"0":{"106":1},"2":{"4":1,"12":2,"24":1,"54":1,"58":3,"69":1,"81":1,"121":1,"154":2,"161":1,"191":1,"192":5,"199":1,"209":1,"217":1,"218":1,"232":1}}],["evaluated",{"2":{"216":1}}],["evaluation",{"2":{"216":1}}],["even",{"2":{"162":2}}],["eventloopmonitoringprecision",{"2":{"82":1}}],["eventloop",{"0":{"81":1},"2":{"79":8,"81":1,"84":1}}],["eventlooplag",{"0":{"78":1},"1":{"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"80":1,"81":1}}],["event",{"2":{"69":2,"72":1,"77":1,"80":1,"81":2,"82":1,"83":3,"84":2,"196":1}}],["events",{"0":{"192":1},"2":{"68":1,"194":1}}],["every",{"2":{"12":1,"16":1,"47":1,"84":1,"90":1,"154":2,"223":1}}],["effect",{"2":{"183":1}}],["efficiency",{"2":{"103":1}}],["efficient",{"2":{"85":1,"160":1}}],["efficiently",{"2":{"9":1}}],["ebf1f4",{"2":{"172":1}}],["elements",{"2":{"206":1}}],["element",{"2":{"172":1,"191":3,"199":1,"206":1}}],["else",{"2":{"65":1,"185":2,"201":1}}],["e42222",{"2":{"172":1}}],["ecosystem",{"2":{"165":1,"229":1,"231":1}}],["ephemeral",{"2":{"162":1}}],["edu",{"2":{"143":2,"144":1,"147":1,"232":2,"233":1}}],["edit",{"2":{"115":1}}],["editorconfig",{"2":{"96":1}}],["editing",{"0":{"40":1},"2":{"46":1,"114":1}}],["either",{"2":{"143":1,"160":1,"178":1,"198":1,"232":1}}],["embeds",{"2":{"197":1}}],["embed",{"2":{"197":1,"199":2}}],["emitting",{"2":{"194":1}}],["emitted",{"2":{"192":5}}],["emit",{"2":{"190":3}}],["employed",{"2":{"163":1,"213":1}}],["emphasizing",{"2":{"103":1}}],["email",{"2":{"51":1,"60":1,"189":3}}],["essential",{"2":{"98":1,"101":1,"107":1,"127":1}}],["eslint",{"2":{"96":2,"121":3,"170":1}}],["etc",{"2":{"95":1,"175":1,"185":1,"207":1,"216":1}}],["etiquette",{"0":{"40":1}}],["equal",{"2":{"51":1}}],["err",{"2":{"17":2,"19":1,"185":2}}],["errorhandler",{"2":{"20":1,"21":1,"32":1}}],["errors",{"2":{"5":1,"14":1,"15":2,"17":1,"18":1,"20":3,"22":1,"24":2,"30":1,"32":1,"42":1,"43":1,"44":1,"45":1,"52":5,"53":1,"54":1,"62":1,"105":3,"106":2,"185":1,"220":2}}],["error",{"0":{"14":1,"15":1,"18":1,"20":1,"22":1,"24":1,"28":1,"30":1,"185":1},"1":{"15":1,"16":2,"17":2,"18":1,"19":2,"20":2,"21":2,"22":1,"23":2,"24":2,"25":3,"26":2,"27":2,"28":2,"29":3,"30":2,"31":3,"32":1,"33":1},"2":{"1":1,"14":4,"15":1,"16":1,"17":1,"18":1,"19":1,"20":3,"22":2,"23":1,"30":1,"32":1,"33":3,"42":1,"43":1,"44":1,"45":1,"47":1,"48":2,"51":5,"53":1,"54":2,"61":1,"62":1,"91":1,"100":1,"105":9,"106":5,"154":3,"185":5,"191":2,"199":3}}],["easy",{"2":{"86":1,"132":1,"137":1,"138":1}}],["easier",{"2":{"1":1,"9":1,"40":1,"48":1}}],["easily",{"2":{"1":1}}],["early",{"2":{"76":1}}],["each",{"2":{"11":1,"12":1,"35":1,"36":2,"64":1,"76":3,"113":2,"193":1,"215":1,"216":3,"220":1}}],["extracts",{"2":{"160":1}}],["extension",{"2":{"121":3}}],["extensive",{"2":{"47":1}}],["extends",{"2":{"98":1,"199":2}}],["extend",{"2":{"1":1,"137":1,"138":1}}],["external",{"2":{"2":1,"77":1,"143":1}}],["exec",{"2":{"115":1,"125":1,"126":1,"141":1,"144":1,"233":1}}],["execution",{"0":{"148":1},"2":{"80":1}}],["executed",{"2":{"107":1}}],["executes",{"2":{"42":1,"43":1,"44":1,"45":1,"81":1,"105":1}}],["execute",{"2":{"11":3,"34":1,"105":1,"160":1}}],["ex",{"2":{"95":1,"144":2,"167":1,"169":1,"171":1,"185":1,"232":2,"233":2}}],["except",{"2":{"230":1}}],["exceptions",{"2":{"223":1}}],["excessive",{"2":{"72":1,"75":1,"77":1}}],["exceeds",{"2":{"69":1}}],["excluding",{"0":{"59":1},"2":{"59":1}}],["examined",{"2":{"65":1}}],["exampletask",{"2":{"36":3}}],["examples",{"0":{"55":1},"2":{"33":1}}],["example",{"0":{"6":1,"12":1,"21":1,"25":1,"27":1,"29":1,"31":1,"104":1,"219":1},"2":{"6":1,"7":1,"8":1,"12":1,"13":1,"31":2,"36":1,"40":1,"46":1,"49":1,"59":1,"60":2,"61":1,"90":1,"93":1,"110":3,"113":1,"141":2,"142":1,"143":1,"160":1,"167":3,"174":1,"197":2,"200":1,"219":1,"226":1,"231":1}}],["exits",{"2":{"42":1,"43":1,"45":1}}],["exists",{"0":{"16":1},"2":{"1":1,"7":1,"27":1}}],["expired",{"2":{"162":1}}],["expire",{"2":{"146":1}}],["expires",{"2":{"146":1,"153":1}}],["experience",{"0":{"103":1},"2":{"98":1}}],["expected",{"2":{"7":1,"47":1,"105":1,"218":1,"219":1,"221":1}}],["explicitly",{"2":{"61":1,"162":1,"184":1}}],["explained",{"0":{"145":1},"1":{"146":1,"147":1,"148":1,"149":1,"150":1,"151":1,"152":1,"153":1,"154":1}}],["explains",{"2":{"14":1}}],["explanation",{"0":{"54":1}}],["exporter",{"2":{"128":1,"138":1}}],["exports",{"2":{"36":2}}],["export",{"2":{"36":1,"226":1,"231":1}}],["exposes",{"2":{"93":1}}],["exposed",{"2":{"19":1,"86":1,"128":2}}],["expose",{"2":{"13":1,"14":1,"85":1}}],["express",{"0":{"52":1,"105":1},"1":{"106":1},"2":{"8":5,"14":1,"15":1,"47":2,"49":1,"52":1,"53":1,"55":1,"84":1,"86":1,"89":1,"93":1,"98":2,"100":1,"103":1,"105":5,"106":1,"107":2,"128":1,"144":1,"233":1}}],["e",{"2":{"1":1,"3":1,"4":1,"8":1,"20":2,"25":4,"26":1,"27":4,"29":4,"31":5,"36":1,"60":1,"82":1,"84":2,"87":2,"88":1,"195":2,"196":2,"197":2,"213":1,"220":1,"229":1}}],["end",{"2":{"195":2,"196":2,"197":2}}],["end=",{"2":{"195":1,"196":1,"197":1}}],["endpoints",{"2":{"107":1,"113":1,"131":1,"163":1,"213":1}}],["endpoint",{"2":{"92":1,"93":1,"163":1,"218":1}}],["enetered",{"2":{"191":1}}],["en",{"2":{"172":1}}],["enforcing",{"2":{"162":1}}],["enforces",{"2":{"105":1}}],["enforce",{"2":{"47":1}}],["enhance",{"2":{"160":1,"162":1}}],["enhances",{"2":{"100":1}}],["enumeration",{"2":{"160":2}}],["encoded",{"2":{"154":1}}],["encountering",{"2":{"223":1}}],["encountered",{"2":{"220":1}}],["encounters",{"2":{"154":1}}],["encounter",{"2":{"143":1}}],["encourages",{"2":{"48":1}}],["entity",{"2":{"199":1}}],["entities",{"2":{"194":4}}],["entire",{"2":{"93":1,"160":1}}],["entry",{"2":{"107":1}}],["enough",{"2":{"64":1}}],["enables",{"2":{"103":1}}],["enabledfeatures",{"2":{"183":1}}],["enabled",{"2":{"87":1,"111":1,"177":4,"183":4,"206":1,"207":1}}],["enable",{"2":{"62":1,"146":1,"177":1,"191":1}}],["enabling",{"2":{"0":1,"86":1,"137":1}}],["engine",{"2":{"24":1,"109":1}}],["env=production",{"2":{"226":2}}],["env=default",{"2":{"141":1}}],["envalert",{"0":{"207":1},"1":{"208":1},"2":{"207":1}}],["envexpress",{"2":{"8":1}}],["envdatabase",{"2":{"6":1}}],["env",{"0":{"6":1},"2":{"3":1,"4":1,"5":1,"7":1,"8":3,"56":1,"107":1,"110":6,"113":1,"141":6,"142":4,"143":4,"144":1,"167":4,"174":6,"177":2,"183":5,"226":2,"227":2,"228":1,"231":2,"233":1}}],["environments",{"2":{"0":1,"1":1,"56":1,"94":1,"174":1,"175":1,"176":1,"207":1}}],["environment",{"0":{"5":1,"7":1,"113":1},"1":{"6":1,"7":1},"2":{"0":1,"1":2,"2":1,"3":3,"4":1,"5":1,"7":1,"8":5,"56":1,"92":1,"98":1,"103":1,"107":1,"110":1,"113":1,"140":1,"143":2,"167":1,"174":8,"176":1,"177":1,"226":2}}],["ensure",{"2":{"7":1,"37":1,"38":1,"46":1,"58":1,"72":1,"138":1,"146":1,"162":1}}],["ensures",{"2":{"2":1,"4":1,"5":1,"14":1,"15":1,"18":1,"32":1,"33":1,"40":1,"46":1,"47":1,"54":2,"89":1,"92":1,"94":1,"102":1,"105":1,"138":1,"160":2,"161":1,"162":2}}],["ensuring",{"2":{"0":1,"9":1,"35":1,"36":1,"54":1,"56":1,"98":1,"127":1,"136":1,"157":1,"161":1,"211":1}}],["pm2",{"2":{"231":2}}],["px",{"2":{"189":1}}],["p>",{"2":{"172":1}}],["p",{"2":{"172":1}}],["pnpm",{"2":{"169":2,"171":1,"233":2}}],["py",{"2":{"165":2,"223":1,"226":3,"227":1,"228":4,"232":1,"233":1}}],["python=3",{"2":{"143":1}}],["python",{"2":{"141":2,"143":2,"144":1,"227":1,"231":1,"233":1}}],["pinia",{"2":{"170":1}}],["pid",{"2":{"144":1,"232":1,"233":1}}],["pidfile",{"2":{"144":1,"232":1,"233":1}}],["pip",{"2":{"143":1,"231":1}}],["placeholder",{"2":{"191":2,"199":2}}],["placeholder=",{"2":{"188":1,"189":1,"190":1,"196":1}}],["plan",{"2":{"143":1}}],["platforms",{"2":{"143":1}}],["plaintext",{"2":{"88":1}}],["ps",{"2":{"118":1,"126":1}}],["pending",{"2":{"223":1,"229":1}}],["pem",{"2":{"110":2,"142":2}}],["persisted",{"2":{"221":1}}],["persistent",{"2":{"216":1}}],["percentiles",{"2":{"82":1}}],["per",{"2":{"76":1,"88":1,"197":1,"216":1}}],["perf",{"2":{"68":1,"82":1,"84":1}}],["performs",{"2":{"164":1}}],["performing",{"2":{"58":1,"199":1}}],["perform",{"2":{"42":1,"43":1,"44":1,"64":1,"105":1,"154":1,"164":1,"218":1}}],["performed",{"2":{"38":1,"105":1,"218":1}}],["performanceobserver",{"2":{"68":1}}],["performance",{"2":{"9":1,"37":1,"60":1,"67":1,"80":1,"83":1,"85":3,"127":1,"130":1,"132":1,"137":1}}],["permission",{"2":{"65":5,"105":2,"160":2}}],["permissions",{"2":{"64":2,"105":2,"123":1,"126":1,"158":1}}],["permitted",{"2":{"65":1}}],["period",{"2":{"11":1,"146":1}}],["p95",{"2":{"79":1}}],["p90",{"2":{"79":1}}],["p50",{"2":{"79":1}}],["philosophy",{"0":{"99":1},"1":{"100":1,"101":1,"102":1,"103":1}}],["physical",{"2":{"75":1}}],["phase",{"2":{"43":1,"46":1}}],["purged",{"2":{"223":1}}],["purpose",{"0":{"1":1},"2":{"42":1,"43":1,"44":1,"45":1,"176":1}}],["put",{"2":{"197":1}}],["public",{"2":{"164":1}}],["published",{"2":{"58":1}}],["pull",{"2":{"144":2,"233":2}}],["push",{"2":{"141":1,"195":1,"196":1,"197":1,"209":1}}],["p2002",{"2":{"27":1}}],["possible",{"2":{"182":1}}],["possess",{"2":{"160":1}}],["postgres",{"2":{"128":2,"131":1,"133":1,"138":3,"141":1,"144":4,"233":4}}],["postgresql",{"2":{"111":1,"140":1,"141":1,"159":1,"163":1,"213":1,"215":1}}],["postman",{"2":{"122":1}}],["post",{"2":{"27":2,"29":2,"51":1,"52":1,"53":1,"58":1,"63":1,"87":1}}],["poetry",{"2":{"143":4,"144":4,"233":4}}],["populate",{"2":{"141":1,"167":1}}],["popular",{"2":{"85":1}}],["powerful",{"2":{"128":1}}],["points",{"2":{"160":2}}],["point",{"2":{"107":1}}],["policies",{"2":{"105":2}}],["portions",{"2":{"75":1}}],["port=3030",{"2":{"8":1}}],["port",{"2":{"8":8,"13":1,"93":1,"95":1,"125":1,"144":8,"233":8}}],["potentially",{"2":{"9":1}}],["potential",{"2":{"2":1,"69":1,"72":1,"73":1,"75":1,"80":1,"83":1}}],["past",{"2":{"194":1}}],["pass",{"2":{"17":1}}],["passed",{"2":{"15":1,"36":1}}],["password",{"2":{"6":1,"51":6,"52":3,"53":3,"54":2,"59":1,"91":3}}],["password=your",{"2":{"6":1}}],["page2promise",{"2":{"184":2}}],["page2",{"2":{"184":3}}],["page1promise",{"2":{"184":2}}],["page1",{"2":{"184":4}}],["page",{"2":{"178":3,"184":6,"195":9,"196":9,"197":9,"209":1,"213":1}}],["pagesizesearch",{"2":{"199":1}}],["pages",{"2":{"77":1,"165":1}}],["packs",{"2":{"171":1}}],["packages",{"2":{"101":1}}],["pattern",{"2":{"160":2}}],["path=",{"2":{"88":10}}],["path",{"2":{"87":3,"88":2,"159":1,"160":14,"161":1,"162":7,"164":3,"198":1,"203":1,"233":6}}],["paths",{"2":{"86":1,"87":1,"88":1,"143":1,"160":1,"161":1,"216":1,"217":1,"233":1}}],["pair",{"2":{"141":1}}],["panl",{"2":{"125":1}}],["payload",{"2":{"63":1,"162":2}}],["parent",{"2":{"233":1}}],["parallel",{"0":{"229":1}}],["parameter",{"2":{"164":1}}],["parameters",{"2":{"47":2,"105":3,"113":1,"209":4}}],["param",{"2":{"55":1}}],["params",{"2":{"25":2,"49":1,"65":2,"66":1,"209":5}}],["parsed",{"2":{"105":1}}],["parses",{"2":{"63":1,"105":1}}],["parseint",{"2":{"51":1}}],["party",{"2":{"101":1,"171":1}}],["particular",{"2":{"64":1,"105":1}}],["particularly",{"2":{"9":1}}],["parts",{"2":{"2":1}}],["part",{"2":{"0":1,"14":1,"162":1}}],["practice",{"2":{"98":1}}],["practices",{"0":{"37":1,"102":1},"2":{"103":1}}],["privacy",{"2":{"161":1}}],["primary",{"2":{"34":1,"36":1,"157":1,"172":11}}],["prismamodel",{"2":{"58":1}}],["prismaclient",{"2":{"62":1}}],["prismaclientknownrequesterror",{"2":{"25":1,"27":1}}],["prismaconstraintfailedhandler",{"2":{"27":1,"32":1}}],["prismanotfoundhandler",{"2":{"24":1,"25":1,"32":1}}],["prisma",{"0":{"24":1,"26":1},"1":{"25":1,"27":1},"2":{"24":1,"25":1,"27":1,"32":1,"56":8,"58":2,"59":2,"60":2,"61":1,"62":3,"102":1,"106":1,"107":4,"115":3,"141":2}}],["prefetching",{"2":{"232":1}}],["prefix",{"2":{"218":1}}],["prefixed",{"2":{"218":1}}],["prepend",{"2":{"193":1}}],["prependinner",{"2":{"193":3}}],["prepare",{"2":{"44":1}}],["present",{"2":{"160":1}}],["pre",{"2":{"133":1,"138":1,"194":1,"199":2,"232":1}}],["prerequisites",{"0":{"109":1}}],["preventing",{"2":{"159":1,"160":1,"161":1}}],["prevent",{"2":{"157":1,"162":1,"211":1}}],["prevents",{"2":{"5":1,"19":1,"47":1}}],["previous",{"2":{"81":1,"84":1}}],["predictable",{"2":{"46":1}}],["precedence",{"0":{"4":1},"2":{"4":2}}],["proxies",{"2":{"167":1}}],["proxied",{"2":{"167":1}}],["proxy",{"2":{"135":4}}],["programming",{"2":{"160":1}}],["provisioning",{"2":{"134":2}}],["providers",{"2":{"177":1}}],["provided",{"2":{"86":1,"171":1,"191":2,"193":2,"197":2,"199":4,"201":1}}],["provide",{"2":{"64":1,"90":1}}],["provides",{"2":{"9":1,"14":1,"51":1,"67":1,"80":1,"84":1,"85":1,"132":1,"139":1,"157":1,"199":1}}],["providing",{"2":{"2":1,"56":1,"98":1,"200":1}}],["prod",{"2":{"114":1,"141":1,"144":1,"165":1,"233":1}}],["products",{"2":{"217":1}}],["product",{"2":{"55":1,"217":1}}],["production",{"2":{"1":1,"3":1,"4":1,"8":1,"19":1,"56":1,"98":1,"108":1,"114":1,"127":1,"135":1,"143":4,"165":2,"226":2}}],["professional",{"2":{"77":1}}],["profile",{"2":{"65":4,"105":1,"153":1,"232":1}}],["procfs",{"2":{"72":1}}],["proc",{"2":{"72":2,"73":1,"75":1}}],["processing",{"0":{"221":1},"2":{"37":1,"113":1,"154":1,"213":1,"220":5,"223":1}}],["processed",{"2":{"18":1,"212":1,"217":3,"220":2,"221":1,"223":1,"232":1}}],["processes",{"2":{"9":2,"10":2,"11":1,"38":1,"42":1,"44":1,"45":1,"92":1,"93":2,"94":1,"140":1,"232":2}}],["process",{"0":{"71":1,"72":1,"73":1,"74":1},"2":{"9":1,"10":1,"11":2,"12":4,"13":1,"34":1,"36":1,"42":2,"43":1,"44":1,"45":1,"46":3,"69":1,"71":2,"72":3,"73":4,"74":1,"75":2,"77":4,"81":1,"85":1,"88":2,"92":1,"93":2,"103":1,"160":1,"216":1,"221":1,"223":2}}],["protected",{"2":{"65":1,"158":1,"212":1}}],["protects",{"2":{"33":1}}],["problems",{"2":{"212":1}}],["problem",{"2":{"58":1}}],["prop",{"2":{"197":1,"198":1,"199":2}}],["props",{"0":{"191":1,"199":1,"202":1,"204":1,"206":1,"208":1},"2":{"172":1,"193":1,"197":2,"198":1,"208":1}}],["propogated",{"2":{"154":1}}],["propagate",{"2":{"105":1}}],["propagating",{"2":{"47":1}}],["property",{"2":{"76":1,"191":2,"198":1,"207":1}}],["properties",{"2":{"3":1,"11":1,"105":1}}],["proper",{"2":{"14":1,"18":1,"46":1,"54":1}}],["properly",{"2":{"14":1}}],["project>",{"2":{"165":1}}],["projects",{"2":{"102":1,"162":1}}],["project",{"0":{"107":1},"2":{"38":1,"56":1,"110":1,"111":1,"127":1,"162":1,"165":1,"168":1,"174":2,"226":1}}],["prompted",{"2":{"121":1}}],["promote",{"2":{"102":1}}],["promotes",{"2":{"47":1}}],["prombundle",{"2":{"93":2}}],["prometheus",{"0":{"128":1,"130":1},"1":{"129":1,"130":1,"131":1},"2":{"85":1,"86":1,"90":1,"93":1,"127":1,"128":1,"129":3,"130":1,"131":1,"132":1,"133":1,"134":1,"136":1,"137":1}}],["promise",{"2":{"81":1,"184":1,"195":1,"196":1,"197":1}}],["prom",{"2":{"13":1,"85":1,"86":1,"88":1,"93":1,"128":1}}],["prone",{"2":{"1":1,"16":1,"47":1,"51":1}}],["dropdownval",{"2":{"196":3}}],["dropdown",{"2":{"191":1,"196":2}}],["dyn",{"2":{"184":2}}],["dynamically",{"2":{"200":1}}],["dynamic",{"2":{"184":1}}],["danger",{"2":{"172":2}}],["dashboard",{"2":{"171":1,"178":3}}],["dashboards",{"2":{"132":1,"133":1,"134":2,"138":3}}],["darken",{"2":{"172":1}}],["dark",{"2":{"170":1}}],["dates",{"0":{"182":1},"2":{"182":1}}],["datetime",{"2":{"58":2,"182":9}}],["date",{"2":{"55":1,"182":3}}],["data=",{"2":{"188":1,"189":1,"190":1,"201":1}}],["datatset",{"2":{"185":1}}],["datasources",{"2":{"134":1}}],["datasource",{"2":{"133":2,"134":1}}],["dataset",{"0":{"160":1,"164":1},"1":{"161":1},"2":{"95":1,"156":2,"157":1,"158":4,"160":30,"161":5,"162":5,"163":4,"164":2,"175":1,"188":3,"203":1,"213":1,"215":3,"216":1,"217":4,"221":1,"223":2,"233":6}}],["datasets",{"2":{"55":2,"88":10,"159":1,"160":2,"161":2,"162":1,"175":1,"185":1,"188":3,"218":1,"233":1}}],["data",{"0":{"222":1},"2":{"31":2,"47":3,"49":1,"51":1,"54":1,"55":2,"56":2,"85":2,"107":1,"115":1,"141":1,"153":1,"154":8,"159":1,"160":1,"161":1,"163":1,"189":2,"190":1,"191":5,"199":4,"201":1,"202":1,"212":1,"216":1,"217":2,"218":1,"222":1}}],["database",{"0":{"115":1},"2":{"1":1,"6":1,"26":1,"56":5,"58":1,"102":1,"107":2,"111":1,"113":1,"115":1,"128":1,"131":1,"140":1,"141":3,"154":2,"159":1,"163":1,"164":1,"213":1,"221":1}}],["dcl=",{"2":{"126":1}}],["dce=",{"2":{"126":1}}],["dcp=",{"2":{"126":1}}],["dcd=",{"2":{"126":1}}],["dcu=",{"2":{"126":1}}],["d",{"2":{"110":1,"117":1,"126":1,"138":1,"141":1,"144":2,"147":1,"168":1,"171":1,"232":1,"233":2}}],["dd1ab3c78284",{"2":{"95":1}}],["dduck",{"2":{"65":1}}],["dbaeumer",{"2":{"121":1}}],["dbeaver",{"2":{"58":1}}],["db",{"2":{"56":2,"58":3,"115":1,"123":1,"141":2,"144":1,"233":1}}],["dummy",{"2":{"141":1}}],["duration",{"0":{"68":1},"2":{"68":1,"88":14}}],["during",{"2":{"38":2,"43":1,"46":2,"56":1,"84":2,"160":1,"176":1}}],["due",{"2":{"60":1}}],["du",{"2":{"55":1,"61":1}}],["duplication",{"2":{"2":1}}],["dotmap",{"2":{"226":1}}],["dotenv",{"2":{"5":1,"7":1,"8":1,"226":1}}],["doc",{"2":{"141":1}}],["docker",{"0":{"114":1,"141":1,"142":1,"168":1},"2":{"103":1,"108":1,"109":3,"111":1,"114":2,"117":1,"118":2,"119":1,"123":1,"126":6,"128":1,"129":1,"131":1,"134":1,"136":1,"138":1,"141":3,"144":6,"165":2,"168":1,"170":1,"232":1,"233":6}}],["docs",{"2":{"95":1,"171":1}}],["documentation",{"0":{"67":1,"95":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"42":1,"59":1,"68":1,"77":1,"95":4,"102":1,"179":1}}],["documents",{"2":{"36":1}}],["document",{"2":{"7":1,"14":1,"67":1,"157":1}}],["do",{"2":{"40":1,"63":1,"165":2,"218":1}}],["don",{"2":{"37":1}}],["does",{"2":{"15":1,"59":1,"81":1,"146":1,"163":1,"213":1}}],["downloaded",{"2":{"162":1}}],["downloads",{"2":{"160":2,"163":1}}],["downloading",{"0":{"164":1},"2":{"156":1,"158":1,"233":1}}],["download",{"0":{"155":1},"1":{"156":1,"157":1,"158":1,"159":1,"160":1,"161":1,"162":1,"163":1,"164":1},"2":{"157":2,"158":3,"159":1,"160":9,"162":2,"163":5,"164":9}}],["down",{"2":{"10":1,"11":1,"12":1,"44":1,"45":1,"119":2,"126":1}}],["dict",{"2":{"226":1,"227":1}}],["dir",{"2":{"217":3,"233":1}}],["dirty",{"2":{"185":1}}],["directories",{"2":{"217":1}}],["directory",{"0":{"217":1},"2":{"2":1,"8":1,"36":2,"56":1,"110":1,"113":1,"160":11,"162":1,"217":2,"233":1}}],["direct",{"2":{"159":1,"163":1,"213":1}}],["directly",{"2":{"51":1,"59":1,"131":1,"157":1,"158":1,"160":2,"174":1,"212":1,"226":1}}],["div>",{"2":{"172":1,"196":1}}],["div",{"2":{"172":1,"196":1}}],["difficult",{"2":{"93":1,"130":1,"143":1}}],["different",{"0":{"50":1},"1":{"51":1,"52":1,"53":1,"54":1},"2":{"0":1,"1":1,"58":1,"174":1,"175":1,"232":1}}],["disbaled",{"2":{"206":1}}],["disables",{"2":{"199":1,"232":1}}],["disabled",{"2":{"183":1,"184":1,"191":2}}],["disable",{"2":{"177":1}}],["distinguish",{"2":{"232":1}}],["dist",{"2":{"173":1}}],["distributed",{"2":{"163":1,"213":1}}],["distribute",{"2":{"9":1}}],["displays",{"2":{"207":1}}],["displayed",{"2":{"197":1}}],["display",{"2":{"154":1,"182":1,"185":1,"190":1,"191":2,"199":2}}],["disk",{"2":{"72":1}}],["discussion",{"2":{"59":1}}],["dee5f2",{"2":{"172":1}}],["deemed",{"2":{"63":1}}],["delayed",{"2":{"84":1}}],["delay",{"2":{"80":1,"81":1,"82":1}}],["delegate",{"2":{"37":1}}],["deleted",{"2":{"55":1,"220":1,"221":1}}],["deleteduser",{"2":{"25":4}}],["delete",{"2":{"25":2,"65":2}}],["derived",{"2":{"68":1,"69":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"82":1,"84":1}}],["denied",{"2":{"65":1}}],["detailing",{"2":{"157":1}}],["detailed",{"2":{"77":1,"105":1}}],["details",{"0":{"111":1},"2":{"59":1,"74":1,"199":2}}],["deterministic",{"2":{"161":1}}],["deterministically",{"2":{"161":1}}],["determined",{"2":{"229":1}}],["determines",{"2":{"191":1,"199":2}}],["determine",{"2":{"64":1,"105":1,"159":1,"174":1,"183":1}}],["detection",{"2":{"76":1}}],["detect",{"2":{"73":1,"75":1,"77":1,"83":1,"130":1,"143":1}}],["debug",{"2":{"62":1}}],["debugging",{"2":{"37":1,"137":1}}],["depending",{"2":{"205":1}}],["dependencies",{"2":{"121":1,"141":1,"142":1,"143":1,"144":1,"168":1,"170":1,"231":1,"233":1}}],["dependency",{"2":{"111":1}}],["depends",{"2":{"61":1,"82":1}}],["deprecation",{"2":{"70":1}}],["deprecated",{"0":{"70":1},"1":{"79":1,"80":1,"81":1,"82":1,"83":1,"84":1},"2":{"70":1}}],["deployed",{"2":{"141":1}}],["deployment",{"0":{"231":1},"2":{"108":1}}],["deploy",{"2":{"56":1}}],["dev>",{"2":{"144":1,"233":1}}],["dev1",{"2":{"143":1}}],["dev",{"2":{"56":1,"115":1,"121":5,"126":1,"141":1,"142":2,"143":3,"144":7,"169":2,"170":1,"172":1,"226":1,"232":2,"233":7}}],["developed",{"2":{"179":1}}],["developement",{"2":{"167":1}}],["developer",{"0":{"103":1}}],["developers",{"2":{"0":1,"1":2,"16":1,"47":1,"98":1,"137":1,"177":1}}],["developing",{"2":{"98":1}}],["development",{"0":{"111":1,"120":1,"126":1,"141":1,"142":1},"1":{"121":1,"122":1},"2":{"1":1,"19":1,"56":1,"98":3,"103":3,"108":1,"109":1,"111":1,"114":1,"135":1,"140":1,"143":2}}],["desktop",{"2":{"109":1}}],["design",{"2":{"65":1,"171":1}}],["designed",{"2":{"9":1,"38":1}}],["desired",{"2":{"64":1,"105":1}}],["deserialization",{"2":{"60":1}}],["described",{"2":{"164":1}}],["describes",{"2":{"95":1}}],["descriptor",{"2":{"72":1,"73":1}}],["descriptors",{"2":{"72":2,"73":1}}],["description",{"0":{"80":1},"2":{"68":1,"69":1,"71":1,"72":1,"73":1,"74":1,"105":2,"220":1}}],["desc",{"2":{"55":2}}],["declaration",{"2":{"230":1}}],["declaratively",{"2":{"52":1}}],["declarative",{"2":{"47":1,"48":2}}],["decisions",{"2":{"85":1}}],["decoded",{"2":{"63":1}}],["decoupled",{"2":{"1":1}}],["deduggi",{"2":{"36":1}}],["demonstrated",{"2":{"13":1}}],["definition",{"2":{"49":1,"129":1,"134":1}}],["defines",{"2":{"64":1,"87":1,"107":1,"134":1}}],["define",{"2":{"8":2,"10":1,"34":1,"35":1,"36":2,"40":2,"49":1,"52":1,"54":1,"90":1,"176":1}}],["defined",{"2":{"5":1,"56":1,"69":1,"128":1,"129":1,"134":1,"174":1}}],["defaultvalue",{"2":{"209":1}}],["defaults",{"2":{"4":1,"19":1,"82":1,"98":1,"191":2,"199":8}}],["default",{"0":{"18":1,"20":1},"1":{"19":1,"20":1,"21":1},"2":{"3":2,"4":1,"8":3,"11":5,"15":1,"18":1,"22":1,"55":3,"58":4,"66":1,"84":1,"88":1,"105":1,"106":1,"141":2,"143":1,"165":1,"174":1,"177":1,"178":1,"191":1,"199":2,"201":1,"202":1,"206":1,"209":3,"226":1,"232":1}}],["my",{"2":{"219":2}}],["mb",{"2":{"212":1}}],["md5",{"2":{"216":1,"217":1,"221":2,"222":2}}],["mdi",{"2":{"171":4,"205":2}}],["md",{"2":{"165":2}}],["m",{"2":{"141":2,"144":1,"229":1,"232":2,"233":1}}],["mkdir",{"2":{"110":1,"142":1}}],["mutability",{"2":{"176":1}}],["muted",{"2":{"172":1}}],["must",{"2":{"51":2,"61":1,"154":1,"158":1,"162":4,"212":1}}],["multiqc",{"2":{"143":1}}],["multi",{"2":{"9":1}}],["multiple",{"2":{"9":1,"107":1,"137":1,"171":1,"174":1,"194":1,"199":3,"232":1}}],["merging",{"2":{"217":1}}],["merges",{"2":{"216":1}}],["merged",{"2":{"174":1,"211":1,"217":1,"221":1,"226":1}}],["merge",{"2":{"165":2}}],["meet",{"2":{"163":1,"213":1}}],["member",{"2":{"162":1}}],["membership",{"2":{"162":1}}],["memoryusage",{"2":{"77":2}}],["memory",{"2":{"72":1,"75":8,"76":3,"77":6}}],["medium",{"2":{"95":1}}],["measurable",{"2":{"82":1}}],["measurement",{"2":{"81":1}}],["measures",{"2":{"68":1,"80":1,"81":1,"88":1,"161":1}}],["meaning",{"2":{"162":1}}],["meaningful",{"2":{"14":1,"37":1}}],["means",{"2":{"81":1,"160":1}}],["mean",{"2":{"79":1,"82":1}}],["mechanisms",{"2":{"14":1}}],["met",{"2":{"162":1}}],["metric",{"0":{"91":1},"2":{"81":2,"90":4}}],["metricsapp",{"2":{"93":1}}],["metricsmiddleware",{"2":{"86":1,"87":1,"89":2}}],["metrics",{"0":{"13":1,"67":1,"70":1,"79":1,"82":1,"86":1,"88":1,"90":1,"92":1,"127":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":2,"80":2,"81":2,"82":2,"83":2,"84":2,"87":1,"88":1,"89":1,"91":1,"93":1,"94":1,"128":1,"129":1,"130":1,"131":1,"132":1,"133":1,"134":1,"135":1,"136":1,"137":1,"138":1},"2":{"13":3,"67":1,"75":1,"76":1,"77":1,"81":5,"82":1,"84":3,"85":2,"86":2,"87":2,"88":6,"89":1,"90":2,"91":3,"92":2,"93":5,"94":2,"98":1,"127":1,"128":4,"129":1,"130":1,"131":1,"132":1,"133":2,"134":3,"136":1,"137":1,"138":5}}],["method=",{"2":{"88":10}}],["method",{"2":{"36":1,"81":4,"87":2,"88":1,"90":1,"91":1,"154":1}}],["methods",{"2":{"1":1,"49":1}}],["metadata",{"2":{"163":1,"201":1,"213":1,"215":3,"216":1}}],["meta",{"2":{"25":1,"174":1,"178":3,"183":1,"184":2}}],["messages",{"2":{"36":1,"175":1,"199":1}}],["message",{"2":{"12":2,"19":1,"20":2,"29":1,"154":2,"191":1,"199":1}}],["millisecond",{"2":{"84":1}}],["milliseconds",{"2":{"11":2}}],["migrate",{"2":{"56":2,"115":1}}],["migrations",{"2":{"56":2,"111":1,"115":1}}],["migration",{"2":{"56":3}}],["minutes",{"2":{"153":1,"182":1}}],["miniconda",{"2":{"143":1}}],["minimizing",{"2":{"103":1}}],["minimal",{"0":{"100":1},"2":{"154":1}}],["minimum",{"2":{"82":1}}],["minor",{"2":{"68":1}}],["min",{"2":{"52":2,"53":2,"54":2,"55":3,"79":1,"82":1,"232":1}}],["middleware",{"0":{"66":1,"86":1},"1":{"87":1,"88":1,"89":1},"2":{"15":2,"16":1,"17":1,"18":1,"23":1,"24":1,"32":1,"33":1,"49":1,"53":2,"54":2,"55":1,"63":3,"64":1,"65":2,"66":5,"86":1,"88":1,"89":1,"100":1,"105":5,"106":2,"107":2}}],["missing",{"2":{"5":1}}],["mockrow",{"2":{"195":2,"196":2,"197":2}}],["mockresults",{"2":{"195":2,"196":2,"197":2}}],["months",{"2":{"182":1}}],["mongo",{"2":{"144":4,"232":2,"233":4}}],["mongodb",{"2":{"143":1}}],["monitoring",{"0":{"127":1},"1":{"128":1,"129":1,"130":1,"131":1,"132":1,"133":1,"134":1,"135":1,"136":1,"137":1,"138":1},"2":{"83":1,"84":1,"85":1,"86":1,"94":1,"127":1,"128":1,"137":1,"138":1}}],["monitoreventloopdelay",{"2":{"82":1,"84":1}}],["monitors",{"2":{"72":1}}],["monitor",{"2":{"62":1,"69":1,"75":1,"76":1,"77":1,"85":1,"93":1,"130":1,"137":1}}],["mounted",{"2":{"143":1,"159":1}}],["modified",{"2":{"154":1}}],["mode",{"2":{"141":1,"170":1,"174":1,"207":1}}],["modern",{"2":{"101":1}}],["model=",{"2":{"196":1}}],["model",{"2":{"63":1,"190":1,"195":1,"196":1,"197":1,"199":2}}],["modular",{"2":{"102":1,"137":1}}],["modularity",{"2":{"35":1,"40":1}}],["modules",{"2":{"40":1,"107":2,"111":1,"169":1,"170":1,"173":1,"231":1}}],["module",{"0":{"146":1,"230":1},"2":{"0":1,"5":1,"8":1,"9":1,"35":1,"36":1,"154":1,"177":1,"182":1,"231":1}}],["moreover",{"2":{"199":2}}],["more",{"0":{"55":1},"2":{"59":1,"60":1,"177":1,"182":1,"194":1,"197":1,"211":1,"212":1,"215":1,"220":1,"223":1}}],["most",{"2":{"4":1}}],["margin",{"2":{"199":1}}],["marked",{"2":{"220":1,"223":1}}],["markup",{"2":{"193":4,"197":3,"199":2}}],["mark",{"2":{"76":1}}],["made",{"2":{"185":1}}],["material",{"2":{"171":1}}],["matched",{"2":{"221":1}}],["matching",{"2":{"198":1}}],["match",{"2":{"162":2,"218":1}}],["machine",{"0":{"141":1,"142":1,"169":1,"232":1},"2":{"139":1,"143":4,"144":2,"174":1,"232":1,"233":2}}],["main",{"2":{"105":1,"107":1,"165":1,"181":1,"212":1}}],["maintenance",{"2":{"34":1}}],["maintains",{"2":{"161":1}}],["maintained",{"2":{"100":1,"159":1,"174":1}}],["maintainability",{"2":{"33":1,"102":1,"137":1}}],["maintainable",{"2":{"0":1,"14":1,"32":1,"47":1,"98":1,"102":1,"103":1}}],["maintaining",{"2":{"14":1}}],["maintain",{"2":{"0":1,"1":1,"14":1,"48":1,"51":1,"138":1,"218":1}}],["major",{"2":{"68":1}}],["makes",{"2":{"162":1,"216":1}}],["make",{"2":{"51":1,"85":1,"132":1,"141":1,"142":1,"167":1,"194":1,"231":1}}],["making",{"2":{"9":1,"48":1,"93":1,"130":1,"137":1,"138":1,"154":1,"161":1}}],["malicious",{"2":{"47":1}}],["maybe",{"0":{"201":1},"1":{"202":1},"2":{"201":1}}],["may",{"2":{"24":1,"58":1,"72":1,"77":1,"83":1,"84":2,"143":1,"154":1,"176":1}}],["many",{"2":{"72":1}}],["manual",{"0":{"51":1},"2":{"17":1,"51":1,"53":1}}],["manually",{"2":{"16":1,"107":1}}],["managing",{"2":{"9":1,"107":1}}],["manager",{"2":{"12":1,"13":1}}],["management",{"0":{"9":1},"1":{"10":1,"11":1,"12":1,"13":1},"2":{"46":1,"73":1,"100":1,"102":1,"105":1}}],["managed",{"2":{"1":2,"77":1,"162":1,"174":1}}],["manage",{"2":{"0":2,"1":1,"9":2,"10":2,"12":2,"38":1,"209":1,"223":1}}],["maximum",{"2":{"11":1,"73":1,"229":1}}],["max",{"0":{"73":1},"2":{"11":2,"12":2,"55":2,"72":1,"73":1,"79":1,"82":1,"196":1,"232":1}}],["master",{"2":{"10":2,"11":2,"12":3,"13":1,"42":1,"46":1,"92":1,"93":2}}],["mapping",{"2":{"56":1}}],["map",{"2":{"8":2,"59":1,"76":2,"195":1,"196":1,"197":1}}],["maps",{"2":{"3":1,"76":1}}],["icon=",{"2":{"171":1,"207":1}}],["icon",{"2":{"171":5,"205":1,"206":2,"208":2}}],["iconify",{"2":{"171":5}}],["icons=",{"2":{"205":1}}],["icons",{"0":{"171":1},"2":{"170":1,"171":5,"206":1}}],["image",{"2":{"126":1}}],["impact",{"2":{"60":1}}],["implements",{"2":{"107":1}}],["implementation",{"0":{"93":1},"2":{"74":1}}],["implementing",{"2":{"54":1,"107":1,"161":1}}],["implemented",{"2":{"14":1,"35":1,"39":1,"93":1,"177":2}}],["imported",{"2":{"134":1,"171":1,"174":1,"226":2}}],["imports",{"2":{"107":1}}],["important",{"2":{"58":1,"154":1}}],["import",{"2":{"49":1,"66":1,"170":2,"172":1,"174":1,"181":1,"183":1,"184":2,"195":1,"196":1,"197":1,"226":2}}],["improving",{"2":{"48":1,"52":1}}],["improve",{"2":{"85":1,"103":1,"154":1}}],["improved",{"2":{"48":1}}],["improves",{"2":{"33":1}}],["io",{"2":{"122":1}}],["i",{"2":{"72":1,"171":2,"195":9,"196":9,"197":9,"213":1,"217":2,"220":1,"229":1}}],["iusca",{"2":{"110":1,"165":2}}],["iu",{"0":{"150":1,"151":1},"2":{"63":1,"143":2,"144":1,"146":1,"147":1,"177":1,"232":2,"233":1}}],["id=",{"2":{"177":2}}],["identity",{"2":{"177":1,"199":1}}],["identifier",{"2":{"160":1}}],["identification",{"2":{"83":1}}],["identifying",{"2":{"127":1,"217":1}}],["identify",{"2":{"69":1,"72":1,"73":1,"76":1,"85":1,"132":1}}],["id",{"2":{"58":2,"59":1,"60":1,"87":1,"90":1,"91":1,"141":2,"165":3,"199":1,"217":7}}],["if",{"2":{"25":1,"27":1,"29":1,"31":1,"37":1,"42":1,"43":1,"51":4,"52":1,"53":1,"54":1,"58":1,"61":2,"63":1,"65":6,"66":2,"72":1,"73":1,"84":1,"91":1,"105":1,"141":3,"143":1,"146":1,"154":3,"162":2,"164":2,"174":2,"182":1,"184":1,"185":1,"191":1,"195":1,"196":1,"197":1,"198":2,"199":1,"201":1,"203":1,"216":1,"218":2,"223":1,"232":1}}],["ingested",{"2":{"233":1}}],["input",{"2":{"191":3,"192":2,"193":4,"199":3,"203":2}}],["infinite",{"2":{"194":1}}],["infeasible",{"2":{"161":1}}],["informed",{"2":{"85":1}}],["information",{"2":{"1":1,"14":1,"19":1,"33":1,"60":1,"154":3,"162":1,"215":1}}],["info",{"2":{"36":1,"62":1,"144":1,"172":2,"185":1,"207":1,"232":1,"233":1}}],["initiated",{"2":{"220":1}}],["initiate",{"2":{"160":1,"163":1,"213":1,"218":2}}],["initialize",{"2":{"141":1,"153":1,"174":1}}],["initializes",{"2":{"107":1}}],["initialized",{"2":{"98":1}}],["initialization",{"2":{"46":1,"56":1}}],["initial",{"2":{"56":1,"105":1,"107":1,"115":1,"195":1,"196":1,"197":1}}],["ini",{"2":{"134":1}}],["inherited",{"2":{"96":1}}],["insensitive",{"2":{"191":1}}],["inside",{"2":{"162":1,"193":2,"197":5,"226":1}}],["insights",{"2":{"137":1}}],["insight",{"2":{"80":1}}],["insomnia",{"2":{"122":1}}],["installed",{"2":{"143":1,"171":2}}],["install",{"2":{"121":6,"141":3,"142":3,"143":6,"144":2,"169":2,"171":1,"181":2,"231":2,"233":2}}],["installation",{"0":{"108":1,"139":1},"1":{"109":1,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"118":1,"119":1,"120":1,"121":1,"122":1,"123":1,"124":1,"125":1,"126":1,"140":1,"141":1,"142":1,"143":1,"144":1},"2":{"111":1,"121":1,"140":1,"171":2}}],["instance",{"0":{"144":1},"2":{"36":1,"141":1,"143":2}}],["instances",{"2":{"28":1,"144":1,"174":1,"233":1}}],["instanceof",{"2":{"25":1,"27":1,"29":1}}],["instrumented",{"2":{"89":1}}],["instrumenting",{"2":{"85":1}}],["instrumentation",{"0":{"85":1},"1":{"86":1,"87":1,"88":1,"89":1,"90":1,"91":1,"92":1,"93":1,"94":1},"2":{"85":1,"138":1}}],["instructions",{"0":{"8":1,"36":1,"49":1,"138":1},"2":{"139":1}}],["instead",{"2":{"40":1,"58":1,"59":1,"87":1,"160":1,"212":1,"227":1}}],["indicate",{"2":{"83":1}}],["indicator",{"2":{"80":1,"191":1,"199":1}}],["individual",{"2":{"40":1,"107":1,"160":1,"194":1,"217":2}}],["indexof",{"2":{"195":1,"196":1,"197":1}}],["index",{"2":{"63":1,"104":1,"105":1,"107":3,"217":2}}],["inline",{"2":{"60":1}}],["inefficiencies",{"2":{"60":1}}],["inverted",{"2":{"172":1}}],["invoking",{"2":{"164":1}}],["invokes",{"2":{"153":1,"154":1}}],["invoke",{"2":{"153":1}}],["invoked",{"2":{"46":1,"81":1,"106":1}}],["involved",{"2":{"160":1}}],["involves",{"2":{"51":1,"160":1}}],["invalidation",{"0":{"154":1}}],["invalid",{"2":{"47":1,"51":1,"91":2}}],["inc",{"2":{"91":1}}],["incurs",{"2":{"72":1}}],["increase",{"2":{"195":1,"196":1,"197":1}}],["increases",{"2":{"84":1}}],["increased",{"2":{"60":1}}],["incremental",{"2":{"68":1}}],["including",{"2":{"61":1,"101":1,"164":1}}],["included",{"2":{"95":1,"103":1,"154":1,"162":2,"208":1,"218":1}}],["include",{"0":{"91":1},"2":{"49":1,"87":3,"154":1,"171":1}}],["includes",{"2":{"25":1,"61":1,"75":1,"162":1,"189":3,"195":1,"196":1,"197":1}}],["incorporating",{"2":{"98":1}}],["incoming",{"2":{"47":1,"51":1,"54":1,"64":1,"89":1,"154":1,"163":1,"213":1}}],["inconsistencies",{"2":{"2":1}}],["in",{"0":{"91":1,"144":1,"152":1},"2":{"2":1,"4":1,"8":4,"9":2,"11":4,"12":1,"13":1,"14":2,"15":1,"16":1,"18":1,"19":2,"32":2,"34":2,"35":1,"36":3,"37":1,"38":1,"39":1,"40":4,"42":2,"43":1,"44":1,"45":1,"46":2,"47":2,"48":1,"49":2,"51":1,"52":2,"53":1,"54":2,"55":2,"56":5,"58":2,"59":1,"63":1,"64":2,"70":1,"72":1,"75":2,"76":1,"80":1,"81":4,"84":4,"86":1,"87":1,"88":1,"89":1,"92":2,"93":2,"94":1,"95":2,"100":1,"105":1,"113":1,"121":2,"127":1,"128":1,"129":1,"130":1,"131":1,"134":1,"135":2,"137":2,"138":2,"140":1,"141":2,"142":1,"143":5,"144":2,"146":2,"153":1,"154":8,"159":1,"160":2,"161":1,"162":2,"163":1,"164":3,"165":3,"167":1,"168":1,"171":1,"174":4,"175":1,"176":1,"179":2,"181":3,"182":4,"183":2,"191":2,"193":1,"194":1,"196":1,"197":3,"198":2,"199":6,"203":1,"207":2,"208":1,"209":3,"212":2,"213":2,"217":2,"218":4,"220":3,"226":1,"227":2,"228":2,"229":1,"230":1,"232":1,"233":3}}],["introduce",{"2":{"174":1}}],["introduced",{"2":{"98":1}}],["introducing",{"2":{"100":1}}],["introduction",{"0":{"98":1,"157":1,"211":1},"1":{"99":1,"100":1,"101":1,"102":1,"103":1,"104":1,"105":1,"106":1,"107":1},"2":{"156":1}}],["int",{"2":{"58":1}}],["intended",{"2":{"200":1}}],["integer",{"2":{"55":1}}],["integrity",{"0":{"222":1},"2":{"14":1,"47":1,"161":1}}],["integrated",{"2":{"98":1,"101":1,"136":1}}],["integrate",{"2":{"33":1}}],["integrates",{"2":{"2":1}}],["integrating",{"2":{"13":1}}],["integration",{"0":{"13":1,"32":1,"46":1,"131":1,"136":1},"2":{"86":1,"121":1}}],["intermediate",{"2":{"160":1}}],["interface",{"2":{"137":1,"143":1,"160":2,"163":1,"213":1}}],["interfere",{"2":{"37":1}}],["interactions",{"2":{"56":1}}],["internal",{"2":{"30":1,"164":1}}],["intercepts",{"2":{"24":1,"167":1}}],["intervals",{"2":{"34":1,"107":1}}],["interval",{"2":{"11":3,"12":2}}],["into",{"0":{"2":1,"32":1},"2":{"14":1,"24":1,"33":1,"80":1,"88":1,"107":1,"136":1,"138":1,"163":1,"174":1,"183":1,"211":1,"212":1,"213":1,"216":2,"217":2,"221":1,"226":2}}],["items",{"2":{"199":2}}],["item",{"2":{"188":2,"189":3,"191":1,"193":2}}],["iteration",{"2":{"81":2}}],["iterates",{"2":{"77":1}}],["itself",{"2":{"154":1,"215":1,"223":1}}],["its",{"2":{"80":1,"143":1,"162":3,"168":1,"196":1,"216":1,"220":1,"221":1,"222":1}}],["it",{"0":{"2":1,"16":1,"35":1},"1":{"36":1,"37":1},"2":{"0":1,"1":1,"9":3,"12":1,"18":1,"35":1,"40":1,"47":1,"48":1,"49":1,"51":1,"53":1,"56":1,"72":1,"76":2,"81":2,"84":1,"85":1,"86":1,"93":1,"98":1,"128":1,"130":1,"131":1,"132":2,"133":1,"137":1,"143":5,"154":3,"160":1,"161":1,"162":4,"164":2,"167":1,"174":2,"182":1,"183":1,"198":2,"201":2,"209":2,"212":1,"217":1,"218":3,"222":2,"226":1}}],["iso",{"2":{"182":1}}],["iso8601",{"2":{"55":1}}],["iss",{"2":{"165":1}}],["issue",{"2":{"141":2,"143":1,"144":3,"233":3}}],["issues",{"0":{"125":1},"2":{"14":1,"73":1,"76":1,"83":1,"130":1,"165":1}}],["isvalidcredentials",{"2":{"91":1}}],["ispermittedto",{"2":{"66":2}}],["isarray",{"2":{"55":1}}],["isiso8601",{"2":{"55":1}}],["isin",{"2":{"55":2}}],["isint",{"2":{"52":1,"53":1,"54":1,"55":1}}],["isempty",{"2":{"52":1}}],["isemail",{"2":{"51":1,"52":1,"53":1,"54":1}}],["islength",{"2":{"52":1,"53":1,"54":1,"55":1}}],["isnan",{"2":{"51":1}}],["is",{"2":{"0":1,"1":1,"7":1,"9":3,"12":1,"13":2,"14":1,"15":1,"18":2,"20":1,"24":1,"29":2,"32":1,"34":1,"35":2,"36":2,"46":2,"49":1,"54":1,"56":3,"58":2,"61":1,"63":2,"64":2,"65":6,"66":2,"72":4,"73":1,"76":8,"80":1,"81":5,"83":1,"84":1,"85":2,"86":2,"87":2,"89":1,"93":1,"100":1,"102":1,"105":4,"106":1,"128":3,"129":1,"132":1,"134":1,"135":2,"136":1,"138":2,"141":4,"143":3,"154":10,"157":1,"159":3,"160":4,"162":6,"163":1,"164":1,"171":2,"174":6,"176":1,"177":2,"182":1,"183":1,"192":5,"193":1,"197":3,"198":3,"200":2,"201":2,"203":2,"207":1,"212":1,"213":2,"215":3,"216":1,"217":3,"218":3,"219":1,"221":2,"223":1,"226":2,"227":2,"228":1,"229":2}}],["skip",{"2":{"195":2,"196":2,"197":2}}],["sm",{"2":{"189":1}}],["smoothly",{"2":{"127":1}}],["slotprops",{"2":{"197":7}}],["slotted",{"2":{"197":4,"199":2}}],["slot",{"2":{"193":6,"200":1}}],["slots",{"0":{"189":1,"193":1,"197":1,"200":1},"2":{"196":1,"197":1,"199":2,"200":2}}],["slate",{"2":{"143":1,"157":1,"158":1,"159":1,"160":4,"212":1}}],["svg",{"2":{"165":1}}],["svc",{"2":{"143":1,"144":2,"233":2}}],["ssh",{"2":{"144":1,"233":1}}],["sda",{"2":{"143":1,"160":2,"233":1}}],["shadow",{"2":{"172":1}}],["shared",{"2":{"111":1}}],["showerror",{"2":{"199":1}}],["shows",{"2":{"199":1,"205":1,"207":1}}],["showing",{"2":{"197":1}}],["shown",{"2":{"191":1,"196":1,"199":3}}],["show",{"2":{"182":1,"184":1,"191":5,"199":1,"201":2,"203":1}}],["should",{"2":{"154":1,"158":3,"162":1,"184":1,"199":1,"207":1,"212":2}}],["short",{"2":{"84":1,"162":1}}],["shell",{"2":{"144":1,"233":1}}],["sh",{"2":{"107":1,"110":1,"126":1,"141":1,"143":1,"165":1}}],["shuts",{"2":{"44":1}}],["shutdown",{"2":{"11":1,"38":1,"44":2,"46":1}}],["shutdowns",{"2":{"9":1}}],["shut",{"2":{"10":1,"11":1,"12":1,"45":1}}],["swlh",{"2":{"95":1}}],["sweep",{"2":{"76":1}}],["swapped",{"2":{"75":1}}],["swagger",{"2":{"42":1,"95":5,"141":1}}],["sql",{"2":{"58":1,"59":1}}],["symlinks",{"2":{"160":1}}],["symlink",{"2":{"160":1}}],["sync",{"2":{"209":1}}],["synchronous",{"2":{"72":1,"83":1}}],["syntax",{"2":{"48":1}}],["systems",{"2":{"86":1,"143":1}}],["system",{"0":{"1":1,"2":1,"32":1,"123":1},"2":{"0":1,"1":2,"2":1,"4":1,"9":3,"14":1,"33":1,"47":1,"72":1,"74":1,"93":1,"127":1,"128":1,"130":1,"132":1,"138":1,"157":1,"162":1,"174":2,"175":1}}],["src",{"2":{"35":1,"36":1,"39":1,"105":3,"107":10,"143":1,"144":3,"165":2,"177":2,"233":3}}],["scoped>",{"2":{"172":1}}],["scope",{"2":{"163":1,"213":2,"218":5,"219":1}}],["scopes",{"2":{"64":1,"218":1}}],["scrollable",{"2":{"203":1}}],["scroll",{"2":{"195":1,"196":1,"197":1}}],["scrolled",{"2":{"194":1}}],["scrolling",{"2":{"194":1}}],["scratch",{"2":{"143":1,"157":1,"158":1,"159":1,"160":4,"212":1,"233":1}}],["scrape",{"2":{"90":1,"93":1,"128":1,"131":1}}],["screen",{"2":{"138":1}}],["script>",{"2":{"172":1,"184":2,"188":1,"189":1,"190":1,"195":1,"196":1,"197":1}}],["script",{"2":{"95":1,"107":2,"126":1,"172":1,"184":2,"188":1,"189":1,"190":1,"195":1,"196":1,"197":1,"223":1}}],["scripts",{"2":{"56":1,"107":2,"141":2,"143":2,"144":3,"165":1,"228":2,"233":3}}],["schema=public",{"2":{"141":1}}],["schema",{"2":{"56":4,"107":2,"141":1}}],["scheduled",{"2":{"107":1,"223":1,"228":1}}],["schedules",{"2":{"81":1}}],["schedule",{"2":{"35":1,"36":3}}],["scheduling",{"0":{"34":1},"1":{"35":1,"36":1,"37":1},"2":{"34":1,"80":1}}],["sca",{"2":{"36":1,"143":2,"232":2}}],["scalability",{"2":{"48":1}}],["scalable",{"2":{"9":1,"103":1}}],["scaling",{"2":{"9":1,"232":1}}],["sure",{"2":{"162":1}}],["successfully",{"2":{"216":1,"220":2,"221":1}}],["successful",{"2":{"216":1}}],["success",{"2":{"154":1,"172":1}}],["such",{"2":{"1":1,"9":1,"38":1,"42":1,"63":1,"83":1,"86":1,"98":1,"106":1,"198":1}}],["sudo",{"2":{"141":1,"144":1,"233":1}}],["sudden",{"2":{"84":1}}],["sufficient",{"2":{"105":1}}],["sum",{"2":{"88":1}}],["summary",{"0":{"33":1}}],["suggests",{"2":{"83":1}}],["superadmin",{"2":{"64":1}}],["supports",{"2":{"226":1}}],["supporting",{"2":{"163":1,"213":1}}],["support",{"2":{"59":1}}],["subscribe",{"2":{"232":1}}],["subsequent",{"2":{"81":1}}],["subj",{"2":{"110":1,"142":1}}],["sub",{"2":{"63":1,"84":1,"95":2,"105":2,"141":2}}],["someone",{"2":{"185":1}}],["sometime",{"2":{"162":1}}],["something",{"0":{"106":1}}],["somehow",{"2":{"162":1}}],["some",{"2":{"111":1,"198":1,"199":2,"223":1}}],["so",{"2":{"72":1,"105":1,"184":1,"212":1}}],["sortorder",{"2":{"61":4}}],["sorting",{"0":{"61":1},"2":{"61":1}}],["sort",{"2":{"55":1,"61":1}}],["softdeleteuser",{"2":{"25":2}}],["source",{"2":{"2":1,"14":1,"60":1,"68":1,"69":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"95":1,"160":1,"176":1,"205":1,"216":1}}],["sources",{"2":{"2":1,"128":1,"137":1}}],["s",{"2":{"24":1,"38":1,"47":1,"49":1,"59":1,"61":1,"65":1,"77":1,"154":4,"160":2,"174":1,"182":1,"183":1,"191":1,"192":1,"193":4,"197":5,"199":9,"208":2,"215":1,"216":1,"217":4,"218":1,"220":2}}],["splice",{"2":{"195":1,"196":1,"197":1}}],["special",{"2":{"164":1}}],["specifying",{"2":{"159":1}}],["specify",{"2":{"61":1}}],["specified",{"2":{"34":1,"162":1,"174":1,"198":1}}],["specific",{"2":{"0":1,"1":1,"3":1,"4":1,"8":2,"22":1,"32":1,"36":1,"38":1,"59":1,"105":2,"107":4,"113":1,"117":1,"171":1,"174":1,"215":1,"227":2}}],["speed",{"2":{"154":1}}],["spending",{"2":{"101":1}}],["spikes",{"2":{"84":1}}],["span>",{"2":{"172":1,"189":4}}],["span",{"2":{"172":1,"189":2}}],["spaces",{"2":{"76":1,"218":2}}],["space",{"2":{"76":19,"77":1,"170":1}}],["spawns",{"2":{"12":1}}],["spawn",{"2":{"11":1}}],["side",{"2":{"216":2}}],["sidebar",{"2":{"138":1}}],["since",{"2":{"71":1,"81":1,"212":1,"218":1}}],["single",{"2":{"9":1,"63":1,"92":1,"98":1,"103":1,"137":1,"154":1,"199":3}}],["simplicity",{"2":{"103":1}}],["simplifies",{"2":{"48":1,"53":1,"56":1,"94":1}}],["simplify",{"2":{"37":1}}],["simple",{"0":{"65":1},"2":{"85":1,"159":1}}],["size",{"2":{"55":2,"61":2,"75":2,"76":6,"77":3,"195":3,"196":3,"197":3}}],["signet",{"2":{"163":1,"164":2,"213":1,"218":1}}],["signed",{"2":{"122":1,"142":1}}],["signature",{"2":{"162":2}}],["signal",{"2":{"12":1}}],["signals",{"2":{"10":1,"11":2,"12":1}}],["signing",{"2":{"141":1}}],["sighup",{"2":{"12":1}}],["sigterm",{"2":{"11":1,"12":1}}],["sigint",{"2":{"11":1,"12":1}}],["style>",{"2":{"172":1}}],["style",{"2":{"172":2}}],["style=",{"2":{"172":2}}],["styles",{"2":{"96":1,"172":1,"173":1,"181":1}}],["str",{"2":{"233":1}}],["strategy",{"2":{"154":1}}],["strict",{"2":{"141":1,"157":1,"211":1}}],["string",{"2":{"55":1,"58":2,"105":1,"161":1,"191":7,"198":3,"199":9,"204":1,"208":2}}],["strings",{"2":{"55":1,"182":1}}],["structure",{"0":{"107":1,"217":1}}],["structured",{"2":{"98":1,"100":1,"102":2}}],["streamlined",{"2":{"98":1}}],["stddev",{"2":{"79":1}}],["still",{"2":{"72":1,"141":1}}],["stop",{"2":{"119":1}}],["stopping",{"0":{"119":1}}],["stopped",{"2":{"45":1}}],["storage",{"2":{"153":1,"154":4,"216":1}}],["store",{"2":{"130":1,"153":1,"154":2}}],["stores",{"2":{"128":1,"153":1,"184":2}}],["stored",{"2":{"36":1,"76":2,"154":2,"163":1,"175":1,"213":1,"217":2}}],["storing",{"2":{"85":1}}],["stages",{"2":{"222":1}}],["stage",{"2":{"160":17,"216":1,"221":1,"226":2}}],["staged",{"2":{"158":2,"159":1,"160":2,"161":2,"163":1}}],["staging",{"0":{"160":1},"1":{"161":1},"2":{"156":1,"160":6,"161":2,"163":1,"233":3}}],["stale",{"2":{"154":1}}],["state",{"2":{"191":1,"195":1,"196":1,"197":1,"212":1}}],["states",{"2":{"175":2}}],["static",{"2":{"174":3,"184":1}}],["stats",{"2":{"170":1}}],["stat",{"2":{"143":1}}],["status=",{"2":{"205":1}}],["statustext",{"2":{"31":1}}],["status",{"0":{"118":1,"220":1},"2":{"19":1,"20":1,"31":1,"51":4,"52":1,"70":1,"75":1,"86":1,"88":13,"118":1,"172":1,"183":1,"185":1,"205":2,"206":1,"209":1,"220":4,"223":1}}],["statuscode",{"2":{"19":1}}],["standalone",{"2":{"107":1}}],["standardize",{"2":{"1":1}}],["starting",{"0":{"117":1},"2":{"168":1}}],["start",{"0":{"71":1,"110":1},"2":{"55":1,"81":2,"110":1,"117":2,"126":1,"138":2,"141":2,"142":1,"143":1,"144":5,"165":1,"168":1,"169":1,"195":2,"196":2,"197":2,"231":1,"232":2,"233":5}}],["started",{"0":{"167":1},"1":{"168":1,"169":1},"2":{"43":1,"71":1}}],["startup",{"2":{"38":1,"46":1,"111":1}}],["starts",{"2":{"12":1,"107":1}}],["stack",{"2":{"18":1,"19":1}}],["steps",{"0":{"141":1,"142":1,"216":1},"2":{"49":1,"105":2,"143":1,"164":1,"216":1}}],["step",{"0":{"8":2},"2":{"105":2,"139":2,"141":1,"160":1,"164":8}}],["salt",{"2":{"161":1}}],["save",{"2":{"121":2,"141":1,"142":1,"231":1}}],["sanitization",{"2":{"54":1}}],["sanitize",{"2":{"51":1}}],["sanitizers",{"2":{"47":1}}],["safe",{"2":{"5":1,"7":1,"8":1,"56":1}}],["same",{"2":{"2":1,"65":1,"131":1,"136":1,"197":1,"218":1,"232":2}}],["sequentially",{"2":{"216":2,"217":2}}],["searched",{"2":{"199":1}}],["searchresultcount",{"2":{"199":1}}],["searchresultcolumns",{"2":{"197":2,"199":1}}],["searchresults",{"2":{"195":5,"196":5,"197":5,"199":1}}],["searchcolumnsconfig",{"2":{"195":3,"196":3,"197":3}}],["searching",{"2":{"194":1}}],["searchandselect>",{"2":{"196":1,"197":1}}],["searchandselect",{"0":{"194":1},"1":{"195":1,"196":1,"197":1,"198":1,"199":1,"200":1},"2":{"194":1,"195":1,"196":1,"197":1}}],["searchterm=",{"2":{"195":1,"196":1,"197":1}}],["searchterm",{"2":{"190":2,"195":15,"196":16,"197":15,"199":2}}],["searchtext",{"2":{"190":2}}],["search",{"2":{"181":1,"188":1,"189":1,"190":2,"191":6,"192":4,"193":2,"194":2,"195":6,"196":7,"197":6,"199":8}}],["seamless",{"2":{"136":1}}],["seamlessly",{"2":{"2":1}}],["session",{"2":{"146":3}}],["several",{"2":{"140":1}}],["semi",{"2":{"76":1}}],["seldom",{"2":{"154":1}}],["self",{"2":{"72":2,"73":1,"75":1,"122":1,"142":1}}],["selectmode",{"2":{"199":1}}],["selectoptions",{"2":{"196":2}}],["selectvalue",{"2":{"196":5}}],["selection",{"2":{"195":3,"196":3,"197":3}}],["selections",{"2":{"195":2,"196":2,"197":2}}],["selecting",{"2":{"194":1,"199":1}}],["select=",{"2":{"188":1,"189":1,"190":1,"195":1,"196":1,"197":1}}],["selectedlabel",{"2":{"199":1}}],["selectedresultcolumns",{"2":{"197":2,"199":1}}],["selectedresults",{"2":{"195":6,"196":6,"197":6,"199":2}}],["selectedcolumnsconfig",{"2":{"195":2,"196":2,"197":2}}],["selecteduser",{"2":{"189":1,"190":2}}],["selected",{"2":{"172":1,"188":1,"191":2,"192":1,"194":2,"195":2,"196":2,"197":2,"199":8}}],["select",{"0":{"60":1},"2":{"60":1,"105":1,"192":1,"196":1}}],["serves",{"2":{"163":1,"213":1,"217":1}}],["serve",{"2":{"160":1}}],["servers",{"2":{"84":1,"143":2}}],["server",{"0":{"105":1},"1":{"106":1},"2":{"13":1,"14":2,"30":1,"44":1,"45":1,"90":1,"105":4,"106":1,"134":1,"140":1,"141":1,"142":1,"144":2,"159":3,"160":1,"163":5,"164":5,"167":3,"169":1,"212":1,"213":5,"216":1,"217":1,"218":2,"233":2}}],["service",{"2":{"125":1,"129":2,"134":2,"138":1,"144":1,"165":1,"182":1,"218":1,"233":1}}],["services",{"0":{"117":1,"119":1},"2":{"64":1,"91":1,"98":1,"107":1,"110":1,"117":2,"138":4,"182":1,"185":1}}],["serving",{"2":{"80":1}}],["seeds",{"2":{"56":1}}],["seed",{"2":{"56":3,"107":1,"115":2,"141":1}}],["seeding",{"2":{"56":2,"107":1}}],["see",{"2":{"49":1,"58":1,"60":1,"113":1,"138":1,"141":1,"170":1,"199":2,"200":1}}],["separated",{"2":{"232":1}}],["separate",{"2":{"35":1,"40":2,"93":1,"107":1,"121":1}}],["sensible",{"2":{"174":1}}],["sensitive",{"2":{"1":1,"8":1,"14":1,"19":1,"33":1,"162":1}}],["sending",{"2":{"218":1}}],["send",{"2":{"93":1,"105":4,"106":3}}],["sends",{"2":{"19":1,"20":1,"26":1,"28":1,"30":1,"105":3,"106":1,"164":1,"216":1}}],["sent",{"2":{"18":1,"20":1,"54":1,"90":1}}],["secret=",{"2":{"177":2}}],["secrets",{"2":{"107":1,"113":1,"227":1}}],["section",{"2":{"162":1,"164":1,"199":2,"200":1}}],["security",{"2":{"160":1,"161":1,"162":2}}],["securedownloadapi",{"2":{"164":2}}],["secure",{"0":{"155":1},"1":{"156":1,"157":1,"158":1,"159":1,"160":1,"161":1,"162":1,"163":1,"164":1},"2":{"54":1,"135":1,"157":1,"160":1,"162":2,"163":2,"213":2,"218":1}}],["securely",{"2":{"1":1}}],["secondary",{"2":{"172":4,"189":2}}],["seconds",{"0":{"68":1,"71":1,"74":1,"81":1},"2":{"12":1,"71":1,"79":8,"81":1,"84":2,"88":12}}],["second",{"2":{"12":2,"154":1,"221":1}}],["setnavitems",{"2":{"184":2}}],["setting",{"2":{"139":1}}],["settings",{"2":{"0":1,"1":3,"2":1,"3":1,"4":2,"8":4,"43":1,"107":1,"113":1,"134":1,"228":1}}],["setimmediate",{"2":{"80":1,"81":1}}],["setinterval",{"2":{"12":1}}],["setup>",{"2":{"172":1,"184":2,"188":1,"189":1,"190":1,"195":1,"196":1,"197":1}}],["setup",{"0":{"111":1,"115":1,"137":1,"141":1,"142":1,"144":1},"2":{"42":1,"56":1,"93":1,"114":1,"123":1,"126":1,"137":1,"138":1,"143":1,"160":2}}],["sets",{"2":{"19":1,"58":1,"105":1,"106":1}}],["set",{"0":{"101":1,"143":1},"2":{"13":1,"58":1,"59":2,"69":1,"98":1,"103":1,"108":1,"110":1,"113":1,"141":3,"168":1,"169":1,"184":2,"195":1,"196":1,"197":2,"207":1,"226":1,"229":1}}],["txt",{"2":{"231":3}}],["tune",{"2":{"174":1}}],["turn",{"2":{"165":1,"183":1}}],["tsconfig",{"2":{"170":1}}],["takes",{"2":{"195":1,"196":1,"197":1}}],["taken",{"2":{"88":1,"233":1}}],["taking",{"2":{"170":1}}],["tailwind",{"2":{"170":2}}],["tables",{"2":{"199":1,"215":1}}],["table",{"0":{"156":1},"2":{"199":5}}],["talk",{"2":{"144":1,"233":1}}],["target",{"2":{"198":2}}],["targets",{"2":{"128":1}}],["tar",{"2":{"143":1,"233":3}}],["tags",{"2":{"95":1}}],["tasklogger",{"2":{"36":2}}],["task",{"2":{"34":1,"35":1,"36":13,"123":1,"160":1,"163":1,"213":1,"230":1,"232":2}}],["tasks",{"0":{"34":1,"229":1},"1":{"35":1,"36":1,"37":1},"2":{"34":1,"35":1,"36":1,"37":3,"38":1,"42":3,"43":2,"44":2,"45":1,"46":1,"107":1,"143":1,"144":2,"160":2,"228":2,"229":1,"232":3,"233":2}}],["two",{"2":{"64":1,"121":1,"154":1,"160":1,"222":1}}],["tips",{"0":{"126":1}}],["tied",{"2":{"107":1}}],["title",{"2":{"58":1,"172":3,"178":3,"184":1}}],["timeouts",{"2":{"212":1}}],["times",{"0":{"182":1},"2":{"86":1,"87":1,"216":1,"223":1}}],["timestamps",{"2":{"58":2,"182":1}}],["timestamp",{"0":{"58":1},"2":{"58":3,"81":1}}],["timer",{"2":{"80":1,"82":1,"153":1}}],["timezone",{"2":{"58":4}}],["time",{"0":{"71":1},"2":{"11":1,"38":1,"42":1,"58":2,"74":1,"81":1,"84":1,"88":1,"101":1,"130":1,"137":2,"146":1,"182":4,"199":1,"229":1}}],["typical",{"0":{"105":1},"1":{"106":1}}],["typically",{"2":{"43":1,"84":1}}],["type",{"2":{"55":1,"56":1,"68":2,"69":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"88":1,"161":1,"199":1,"216":1,"233":1}}],["types",{"2":{"22":1,"175":1}}],["t",{"2":{"37":1,"143":1}}],["term",{"2":{"192":1,"199":1}}],["termination",{"2":{"10":1,"12":1}}],["text=",{"2":{"190":1,"203":1}}],["texts",{"2":{"175":1}}],["text",{"2":{"171":1,"172":4,"189":9,"191":4,"192":1,"195":9,"196":9,"197":12,"203":2,"204":1,"205":1}}],["templatename",{"2":{"197":1}}],["template>",{"2":{"172":2,"188":2,"189":3,"190":2,"195":2,"196":3,"197":3,"201":1,"203":2,"205":2,"207":2}}],["template",{"2":{"167":1,"189":1,"196":1,"197":5,"201":1}}],["temporary",{"2":{"160":1}}],["teams",{"2":{"101":1}}],["tests",{"2":{"228":1,"232":2}}],["testing",{"0":{"122":1,"232":1,"233":1},"2":{"40":1,"122":3}}],["test",{"0":{"144":1},"2":{"37":1,"143":1,"144":1,"207":1,"228":1,"232":2,"233":1}}],["trends",{"2":{"132":1}}],["troubleshooting",{"0":{"124":1},"1":{"125":1,"126":1}}],["trackby",{"2":{"199":1}}],["tracked",{"2":{"174":1}}],["track",{"2":{"90":1,"174":1,"195":1,"196":1,"197":1}}],["tracks",{"2":{"69":1,"71":1,"74":1,"88":1}}],["trace",{"2":{"19":1}}],["transferring",{"2":{"212":1}}],["transfer",{"2":{"160":1}}],["transformuser",{"2":{"59":1}}],["transform",{"2":{"59":2}}],["transaction",{"2":{"154":1}}],["transmission",{"2":{"60":1}}],["true",{"2":{"55":1,"60":3,"61":1,"66":1,"177":4,"183":1,"190":1,"197":2,"199":2}}],["truth",{"2":{"2":1}}],["try",{"2":{"16":1,"17":2,"25":1,"27":1,"29":1,"31":1}}],["triggers",{"2":{"160":1}}],["trigger",{"2":{"11":1,"213":1,"216":1}}],["those",{"2":{"162":1,"183":1}}],["thoroughly",{"2":{"37":1}}],["thresholds",{"2":{"69":1}}],["throughput",{"2":{"84":1}}],["through",{"0":{"105":1},"1":{"106":1},"2":{"47":1,"100":1,"103":1,"144":1,"154":2,"160":6,"162":1,"163":1,"164":1,"174":1,"211":2,"213":1,"220":2,"222":1,"233":1}}],["throughout",{"2":{"2":1}}],["throws",{"2":{"66":1}}],["throw",{"2":{"24":1,"61":1,"91":1}}],["thrown",{"2":{"15":1}}],["things",{"2":{"216":1}}],["third",{"2":{"101":1,"171":1}}],["thier",{"2":{"65":1}}],["this",{"0":{"137":1},"2":{"1":1,"2":1,"4":1,"5":1,"9":3,"12":1,"13":1,"14":3,"15":2,"16":1,"17":1,"32":1,"33":1,"34":1,"38":1,"40":1,"42":1,"43":1,"47":3,"51":1,"52":1,"56":2,"58":1,"60":1,"61":1,"62":1,"63":1,"64":1,"65":1,"67":1,"80":1,"81":3,"84":1,"85":1,"89":1,"90":2,"92":1,"93":1,"98":1,"101":1,"103":1,"106":1,"108":1,"126":1,"127":1,"135":1,"138":1,"139":1,"140":1,"141":1,"143":1,"154":3,"157":1,"160":8,"161":1,"162":2,"163":2,"165":3,"171":1,"174":3,"177":2,"178":1,"183":1,"193":1,"194":1,"199":1,"207":1,"209":2,"212":1,"213":4,"216":1,"217":5,"218":1,"220":1,"221":1,"229":1}}],["than",{"2":{"51":1,"72":1,"100":1,"101":1,"182":2,"220":1,"223":2}}],["that",{"2":{"0":1,"2":1,"4":1,"5":1,"7":1,"9":1,"14":2,"15":1,"18":1,"32":1,"36":2,"38":1,"42":1,"47":1,"51":1,"54":2,"63":1,"83":1,"89":1,"92":1,"95":2,"98":2,"105":1,"107":3,"128":1,"132":1,"138":1,"146":1,"153":1,"154":2,"160":1,"162":5,"163":1,"175":1,"197":3,"198":1,"200":1,"207":2,"212":1,"213":1,"217":1,"218":4,"219":1,"221":1,"223":1,"226":1,"229":1}}],["therefore",{"2":{"220":1}}],["there",{"2":{"72":1,"130":1,"144":1,"154":1,"171":1,"233":1}}],["then",{"2":{"51":1,"52":1,"81":1,"154":1,"184":1,"185":2,"189":1,"190":1,"195":1,"196":1,"197":1,"211":1,"212":1}}],["these",{"2":{"24":1,"38":1,"46":2,"49":1,"64":1,"86":1,"127":1,"140":1,"141":1,"142":1,"144":1,"161":1,"162":1,"165":1,"174":2,"175":2,"194":1,"211":1,"213":1,"218":1,"232":1,"233":1}}],["them",{"2":{"17":1,"24":1,"30":1,"40":1,"46":1,"51":1,"52":1,"85":1,"93":1,"105":1,"137":1,"153":1,"209":1,"212":1,"221":1}}],["they",{"2":{"14":2,"22":1,"36":1,"37":1,"146":1,"161":1,"162":1,"184":1,"197":1,"212":1,"223":1,"232":1}}],["their",{"2":{"7":1,"131":1,"158":1,"160":1,"163":1,"212":1,"213":1,"223":1}}],["the",{"0":{"1":1,"2":1,"18":1,"32":1,"54":1,"91":1,"105":1,"160":1,"214":1},"1":{"19":1,"20":1,"21":1,"106":1,"161":1,"215":1,"216":1,"217":1,"218":1,"219":1,"220":1},"2":{"0":4,"1":4,"2":6,"3":2,"4":3,"5":2,"7":1,"8":3,"9":3,"10":1,"11":6,"12":2,"13":5,"14":6,"15":1,"17":2,"18":4,"19":1,"20":5,"22":1,"23":1,"24":1,"32":4,"33":1,"34":3,"35":3,"36":17,"38":3,"39":2,"40":2,"42":2,"43":3,"44":3,"45":3,"46":6,"47":5,"49":11,"51":1,"52":1,"53":3,"54":8,"56":9,"58":6,"59":1,"62":1,"63":11,"64":4,"65":8,"66":3,"67":2,"68":1,"69":4,"71":2,"72":5,"73":5,"74":2,"76":5,"77":6,"80":2,"81":14,"82":3,"83":2,"84":5,"85":3,"86":2,"87":4,"88":3,"89":2,"90":4,"92":1,"93":4,"95":9,"98":1,"100":1,"102":2,"103":1,"105":18,"106":4,"107":6,"109":1,"110":3,"111":1,"113":1,"114":1,"115":1,"122":2,"123":1,"126":1,"127":1,"128":2,"129":1,"131":2,"132":1,"133":2,"134":3,"135":4,"136":1,"137":2,"138":9,"139":1,"140":2,"141":5,"142":2,"143":6,"144":3,"146":7,"153":5,"154":40,"156":1,"157":5,"158":4,"159":7,"160":43,"161":10,"162":22,"163":10,"164":19,"165":5,"167":10,"168":3,"169":1,"174":17,"175":2,"176":2,"179":1,"182":2,"183":4,"185":2,"191":5,"192":6,"193":2,"194":6,"195":1,"196":4,"197":22,"198":8,"199":37,"200":4,"203":1,"207":3,"208":1,"209":6,"211":1,"212":3,"213":12,"215":2,"216":17,"217":14,"218":23,"219":2,"220":9,"221":11,"222":4,"223":3,"226":5,"229":2,"230":1,"232":2,"233":9}}],["tolowercase",{"2":{"189":4}}],["toast",{"2":{"185":5}}],["toggled",{"2":{"183":1}}],["too",{"2":{"143":1}}],["tools",{"0":{"120":1},"1":{"121":1,"122":1},"2":{"100":1,"127":1}}],["tool",{"2":{"56":1,"58":1,"196":1}}],["todo",{"2":{"74":1,"141":1,"143":1,"170":1}}],["totalresultcount",{"2":{"195":5,"196":5,"197":5}}],["total",{"0":{"74":1},"2":{"69":2,"70":1,"74":1,"75":2,"76":2,"77":2,"90":3,"199":1}}],["toboolean",{"2":{"55":1}}],["toint",{"2":{"52":1,"53":1,"54":1,"55":1}}],["token>",{"2":{"141":1}}],["token=",{"2":{"141":1}}],["token=your",{"2":{"6":1}}],["tokens",{"2":{"135":1,"163":1,"213":1}}],["token",{"0":{"153":1},"2":{"6":1,"63":2,"65":1,"135":2,"141":5,"143":3,"144":4,"153":4,"154":6,"162":2,"164":3,"218":4,"219":1,"233":4}}],["to",{"0":{"102":1,"141":1,"142":1},"2":{"0":2,"1":5,"3":1,"4":1,"5":1,"7":2,"8":2,"9":4,"10":2,"11":7,"12":2,"13":2,"14":2,"15":3,"16":2,"17":2,"18":1,"19":2,"20":3,"22":1,"23":1,"33":1,"34":1,"35":1,"36":3,"37":3,"38":1,"40":1,"42":1,"46":2,"47":3,"48":2,"49":1,"51":4,"54":1,"55":2,"56":2,"58":4,"59":1,"60":1,"62":1,"63":3,"64":4,"65":5,"66":3,"69":2,"72":1,"73":1,"75":1,"76":1,"77":3,"82":1,"85":3,"87":2,"88":2,"89":1,"90":4,"93":3,"95":3,"101":1,"102":1,"103":2,"105":16,"106":2,"107":3,"110":1,"111":1,"113":1,"115":1,"126":1,"128":1,"130":2,"131":1,"132":2,"133":1,"134":1,"135":1,"136":1,"137":1,"138":5,"141":8,"142":2,"143":6,"144":4,"146":5,"147":1,"154":12,"157":3,"158":5,"159":3,"160":18,"161":2,"162":9,"163":9,"164":7,"165":2,"167":2,"168":1,"169":1,"170":1,"171":3,"174":9,"176":1,"179":1,"181":1,"182":5,"183":3,"184":5,"185":1,"191":11,"193":5,"194":2,"196":1,"199":27,"203":1,"205":1,"208":2,"209":1,"211":1,"212":7,"213":9,"215":4,"216":5,"217":1,"218":8,"219":3,"220":2,"221":1,"222":1,"223":2,"226":3,"227":1,"229":1,"231":1,"232":4,"233":8}}],["ci",{"2":{"207":1}}],["cil",{"2":{"177":1}}],["cilogon",{"2":{"177":7}}],["css",{"2":{"172":2,"173":3,"181":1}}],["cn=localhost",{"2":{"110":1,"142":1}}],["cp",{"2":{"110":2,"141":1,"142":1,"143":1}}],["cpus",{"2":{"9":1}}],["cpu",{"0":{"74":1},"2":{"9":1,"74":1,"107":1}}],["cd",{"2":{"110":5,"121":2,"141":4,"144":3,"165":1,"233":3}}],["cell",{"2":{"197":2}}],["celeryconfig",{"2":{"227":1,"232":1}}],["celery",{"0":{"227":1},"2":{"143":2,"144":5,"160":3,"163":1,"165":1,"213":1,"227":1,"228":1,"229":4,"230":1,"232":5,"233":5}}],["centerdot",{"2":{"189":1}}],["centrally",{"2":{"174":1}}],["centralizing",{"2":{"33":1,"47":1}}],["centralized",{"2":{"94":1,"130":1,"137":1}}],["centralizes",{"2":{"48":1}}],["centralize",{"2":{"1":1}}],["certain",{"2":{"146":1,"194":1,"199":2}}],["certificate",{"2":{"122":1,"142":1}}],["certificates",{"2":{"109":1,"110":1}}],["cert",{"2":{"110":4,"142":4}}],["cycle",{"2":{"81":2,"84":1}}],["c++",{"2":{"77":1}}],["curve",{"2":{"98":1}}],["curl",{"2":{"88":1,"122":1,"125":1}}],["currentresults",{"2":{"195":2,"196":2,"197":2}}],["currently",{"2":{"69":1,"194":1,"199":2,"220":1,"229":1}}],["current",{"2":{"58":1,"84":1,"153":1,"192":1,"199":3}}],["customcomponent",{"2":{"180":1}}],["customized",{"2":{"114":1,"197":2}}],["customsanitizer",{"2":{"55":1}}],["customlogic",{"2":{"40":2}}],["custom",{"0":{"20":1,"22":1,"90":1},"1":{"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"91":1},"2":{"3":1,"8":1,"10":1,"20":1,"22":1,"32":1,"40":1,"51":1,"81":1,"90":2,"105":1,"106":1,"138":1,"165":1,"180":1,"193":3,"197":1,"199":2}}],["child",{"2":{"232":1}}],["chip",{"2":{"205":1}}],["chip>",{"2":{"197":4}}],["chunk",{"2":{"216":4,"217":2,"218":2,"222":1}}],["chunks",{"2":{"211":1,"212":2,"213":2,"216":3,"217":7,"221":3,"222":1,"223":1}}],["choose",{"2":{"154":1}}],["chown",{"2":{"123":1}}],["changing",{"2":{"183":1}}],["changed",{"2":{"183":1,"192":1}}],["change",{"2":{"141":1,"142":1,"143":2,"165":6,"174":1,"176":1,"209":1}}],["changes",{"2":{"138":1,"141":1,"142":1,"144":1,"183":1,"233":1}}],["charles",{"2":{"165":2}}],["characters",{"2":{"51":1}}],["checkout",{"2":{"144":1,"233":1}}],["checkownership",{"2":{"66":1}}],["checked",{"2":{"66":1}}],["checking",{"0":{"118":1},"2":{"66":1,"213":1,"218":1}}],["check",{"2":{"51":1,"65":2,"122":1}}],["checksums",{"2":{"216":2,"220":3}}],["checksum",{"2":{"216":3,"220":1,"221":3,"222":3}}],["checkschema",{"2":{"55":1}}],["checks",{"2":{"43":1,"47":1,"105":1,"164":1}}],["closed",{"2":{"192":1}}],["close",{"2":{"190":1,"192":1}}],["close=",{"2":{"190":1}}],["clone",{"2":{"110":2,"165":2}}],["cloning",{"2":{"109":1}}],["cleared",{"2":{"192":1}}],["clear",{"2":{"190":1,"192":2}}],["clear=",{"2":{"190":1}}],["cleanup",{"2":{"34":1,"44":1,"45":1,"46":2}}],["clean",{"2":{"0":1,"14":1,"32":1,"46":1,"47":1}}],["class",{"2":{"185":1}}],["classes",{"0":{"173":1}}],["class=",{"2":{"171":1,"172":1,"189":2,"196":1}}],["clipboard",{"2":{"203":1}}],["clicked",{"2":{"192":1}}],["click",{"2":{"138":1}}],["clients",{"2":{"14":1,"19":1,"20":1,"144":1,"233":1}}],["client",{"2":{"13":1,"14":1,"18":1,"20":2,"54":1,"56":1,"62":1,"85":1,"90":2,"91":1,"105":3,"106":1,"128":1,"163":2,"164":1,"177":4,"194":1,"196":1,"199":1,"213":2,"216":3,"218":1}}],["clutter",{"2":{"51":1}}],["cluttered",{"2":{"47":1}}],["clustermetrics",{"2":{"93":1}}],["clustered",{"0":{"92":1},"1":{"93":1,"94":1},"2":{"92":1,"94":1}}],["clustering",{"2":{"87":1,"103":1,"107":1}}],["cluster",{"0":{"9":1},"1":{"10":1,"11":1,"12":1,"13":1},"2":{"9":3,"10":1,"12":4,"13":3,"34":1,"36":1,"93":2,"107":1}}],["cached",{"2":{"154":1}}],["caches",{"2":{"154":2}}],["cache",{"0":{"154":1},"2":{"154":4}}],["care",{"2":{"195":1,"196":1,"197":1}}],["carefully",{"2":{"160":1}}],["carbonate",{"2":{"144":1}}],["cardinality",{"2":{"87":1}}],["captures",{"2":{"87":2}}],["capture",{"2":{"84":1}}],["capturing",{"2":{"81":1}}],["caveats",{"2":{"72":1,"77":1}}],["categories",{"2":{"88":1}}],["categorized",{"2":{"68":1}}],["catches",{"2":{"23":1,"28":1}}],["catch",{"2":{"15":1,"16":1,"17":3,"25":1,"27":1,"29":1,"31":1,"105":1,"185":2}}],["cas",{"2":{"63":1,"146":1,"177":1}}],["cases",{"0":{"83":1},"2":{"198":1}}],["case",{"0":{"65":1},"2":{"42":1,"43":1,"44":1,"45":1,"191":1}}],["calls",{"2":{"141":1,"167":1}}],["called",{"2":{"46":1,"160":1,"174":1,"193":1,"198":1,"226":1}}],["call",{"2":{"40":2,"72":1,"154":1,"167":1,"209":1,"219":1}}],["callback",{"2":{"11":3,"80":1}}],["cause",{"2":{"25":1}}],["caused",{"2":{"5":1}}],["caught",{"2":{"14":1}}],["cannot",{"2":{"143":2,"159":1,"160":1,"162":2,"218":1}}],["can",{"2":{"1":2,"13":1,"51":1,"52":1,"54":1,"59":1,"60":1,"65":3,"66":1,"73":1,"82":1,"85":1,"90":1,"98":2,"103":1,"114":1,"133":1,"137":1,"141":1,"143":1,"154":2,"162":2,"165":2,"175":1,"178":1,"183":2,"191":3,"196":2,"197":5,"198":1,"199":6,"207":1,"212":1,"229":2}}],["cross",{"2":{"105":1}}],["cron",{"2":{"34":1,"35":3,"36":11,"37":1,"42":1,"107":1}}],["crud",{"2":{"64":1,"65":1}}],["cryptographically",{"2":{"63":1}}],["crash",{"2":{"14":1}}],["credential",{"2":{"163":1,"213":1}}],["credentials",{"2":{"1":1,"91":2,"138":1}}],["creation",{"2":{"160":1}}],["createdat",{"2":{"58":4}}],["created",{"2":{"55":1,"81":1,"154":1,"160":1,"217":1}}],["creates",{"2":{"36":1,"56":1,"90":1,"105":1,"153":1}}],["createtasklogger",{"2":{"36":3}}],["createuser",{"2":{"27":2,"29":2}}],["createerror",{"2":{"20":1,"25":1,"27":1,"29":1,"31":1,"65":2,"66":1}}],["create",{"0":{"165":1},"2":{"8":2,"36":1,"51":1,"52":1,"53":1,"65":2,"105":1,"141":1,"142":2,"143":4,"144":2,"146":1,"163":1,"165":1,"167":1,"209":1,"213":1,"226":1,"233":2}}],["critical",{"2":{"0":1,"14":1,"38":1,"42":1,"43":1,"162":1}}],["copy",{"2":{"203":1,"231":1}}],["copytext",{"0":{"203":1},"1":{"204":1},"2":{"203":1}}],["covers",{"2":{"140":1}}],["cookie",{"2":{"135":1}}],["cookies",{"2":{"105":2}}],["coding",{"0":{"180":1,"225":1},"1":{"226":1,"227":1,"228":1,"229":1,"230":1},"2":{"96":1}}],["code=",{"2":{"88":10}}],["codes",{"2":{"86":1,"88":2}}],["codebase",{"2":{"14":1,"32":1}}],["code",{"0":{"54":1,"91":1,"121":1,"148":1,"150":1,"151":1,"152":1,"153":1,"228":1},"2":{"0":1,"1":1,"7":1,"8":1,"14":1,"15":1,"16":1,"20":1,"27":1,"33":1,"47":2,"48":2,"51":1,"52":1,"65":1,"66":1,"76":3,"88":1,"95":1,"102":1,"121":1,"154":2,"174":1,"183":1,"228":2,"230":2}}],["color=",{"2":{"172":1,"207":1}}],["colorbystatus",{"2":{"172":2}}],["color",{"2":{"172":9,"205":1,"208":2}}],["colors",{"0":{"172":1},"2":{"172":4}}],["colo25",{"2":{"143":1}}],["colo23>",{"2":{"144":5,"233":5}}],["colo23",{"2":{"143":1,"144":1}}],["colo",{"0":{"144":1,"233":1},"2":{"143":2,"159":1,"160":1,"163":2,"212":3,"213":1}}],["collects",{"2":{"88":1,"128":1,"137":1}}],["collecting",{"2":{"85":1,"86":1}}],["collection",{"2":{"47":1,"68":1,"81":5,"84":1,"94":1}}],["collect",{"2":{"81":3,"85":1,"93":1,"130":1}}],["collector",{"2":{"76":2}}],["collected",{"0":{"88":1},"2":{"67":1,"84":1,"88":1,"132":1}}],["column",{"2":{"61":1,"197":5,"199":8,"200":1}}],["columns=",{"2":{"195":2,"196":2,"197":2}}],["columns",{"0":{"59":1},"2":{"59":2,"60":2,"199":2}}],["corruption",{"2":{"212":1}}],["corresponding",{"2":{"80":1,"160":2,"211":1,"216":1,"217":2,"221":1}}],["correct",{"2":{"47":1,"54":1,"105":1}}],["cors",{"2":{"105":2}}],["core",{"2":{"9":1,"12":1,"35":1,"36":1,"39":1,"87":1,"89":1,"90":1,"91":1,"107":2,"143":2}}],["cores",{"2":{"9":1,"107":1}}],["could",{"2":{"14":1,"185":1,"220":1}}],["count=",{"2":{"195":1,"196":1,"197":1}}],["counter",{"2":{"74":1,"90":2}}],["count",{"2":{"11":1,"12":1,"69":2,"88":1}}],["combination",{"2":{"191":1}}],["come",{"2":{"183":1}}],["comes",{"2":{"176":1}}],["comma",{"2":{"232":1}}],["commands",{"2":{"56":1,"143":1}}],["command",{"2":{"2":1,"4":1,"56":1,"98":1,"103":1}}],["community",{"2":{"171":1}}],["communicate",{"2":{"144":1,"233":1}}],["common",{"0":{"116":1,"125":1},"1":{"117":1,"118":1,"119":1},"2":{"95":1,"165":1,"226":2}}],["comment",{"2":{"95":1}}],["compression",{"2":{"105":4,"106":2}}],["comprehensive",{"0":{"101":1}}],["composition",{"2":{"209":1}}],["composables",{"2":{"209":1}}],["composable",{"0":{"209":1}}],["composer",{"2":{"168":1}}],["compose",{"2":{"103":1,"110":1,"111":1,"114":2,"115":1,"117":2,"118":3,"119":2,"125":2,"126":6,"128":1,"129":1,"134":1,"138":2,"141":3,"144":4,"165":2,"232":1,"233":4}}],["component",{"2":{"105":2,"113":1,"154":3,"170":1,"180":1,"182":1,"199":1,"208":1,"209":1}}],["components",{"0":{"179":1,"186":1},"1":{"187":1,"188":1,"189":1,"190":1,"191":1,"192":1,"193":1,"194":1,"195":1,"196":1,"197":1,"198":1,"199":1,"200":1,"201":1,"202":1,"203":1,"204":1,"205":1,"206":1,"207":1,"208":1,"209":1},"2":{"40":1,"140":2,"162":3,"171":2,"179":1,"183":1}}],["complex",{"2":{"122":1}}],["complexity",{"2":{"98":1,"103":1,"154":1}}],["completed",{"2":{"81":1,"220":1}}],["complete",{"2":{"13":1,"103":1,"160":1,"220":1}}],["compatible",{"2":{"86":1}}],["compare",{"2":{"72":1,"73":1,"77":1}}],["comparison",{"0":{"50":1},"1":{"51":1,"52":1,"53":1,"54":1}}],["compute",{"2":{"233":4}}],["computed",{"2":{"81":2,"195":3,"196":4,"197":3,"220":1}}],["computing",{"2":{"220":1}}],["computationally",{"2":{"161":1}}],["computation",{"0":{"81":1,"82":1},"2":{"220":2}}],["compiled",{"2":{"76":1}}],["com",{"2":{"6":1,"31":2,"95":1,"110":1,"141":1,"165":1,"171":1,"177":1}}],["concurrency",{"2":{"232":3}}],["concurrent",{"2":{"9":1}}],["concat",{"2":{"195":1,"196":1,"197":1}}],["convenient",{"2":{"212":1}}],["conventions",{"0":{"180":1}}],["converted",{"2":{"105":1}}],["convert",{"2":{"55":1}}],["converts",{"2":{"24":1,"55":1,"58":1}}],["conditions",{"2":{"162":2}}],["conda",{"2":{"143":2}}],["connection",{"2":{"113":1}}],["connected",{"2":{"58":1}}],["connect",{"2":{"56":1,"141":1,"143":2,"177":4}}],["context",{"2":{"90":1}}],["contents",{"0":{"156":1},"2":{"160":1}}],["content",{"2":{"47":1,"58":1,"165":1}}],["continuous",{"2":{"84":1}}],["continues",{"2":{"44":1}}],["controlsheight",{"2":{"199":1}}],["controlsmargin",{"2":{"199":1}}],["controls",{"0":{"178":1},"2":{"199":3,"200":1}}],["control",{"0":{"162":1,"218":1},"1":{"219":1},"2":{"64":1,"95":1,"100":1,"105":2,"156":1,"157":1,"159":1,"160":1,"162":2,"174":1,"211":1,"218":1}}],["contained",{"2":{"218":1}}],["containers",{"2":{"119":1,"121":2}}],["container",{"2":{"111":1,"118":1,"121":1,"125":1,"126":1,"199":1,"203":1}}],["containing",{"2":{"135":1}}],["contain",{"2":{"47":1}}],["contains",{"2":{"3":2,"36":1,"56":1,"134":1,"154":1,"162":2,"174":1,"197":1,"215":3,"218":1}}],["confused",{"2":{"199":1}}],["conf",{"2":{"135":2}}],["conflicts",{"2":{"111":1,"125":1}}],["conflict",{"2":{"27":1}}],["configre",{"2":{"66":1}}],["configuring",{"2":{"101":1,"159":1}}],["configures",{"2":{"95":1,"107":1,"134":1}}],["configured",{"2":{"56":1,"64":1,"73":1,"87":1,"98":1,"128":1,"133":2,"138":1,"160":2,"232":1}}],["configurable",{"2":{"10":1,"13":1,"153":1}}],["configurations",{"2":{"1":1,"5":1,"87":1,"138":1}}],["configuration",{"0":{"0":1,"1":1,"3":1,"4":1,"11":1,"87":1,"112":1,"114":1,"129":1,"134":1,"174":1,"176":1},"1":{"1":1,"2":1,"3":1,"4":2,"5":1,"6":1,"7":1,"8":1,"113":1,"114":1},"2":{"0":2,"1":2,"2":3,"3":2,"4":1,"8":2,"43":1,"82":1,"93":1,"100":1,"107":2,"114":1,"129":1,"134":1,"137":1,"140":1,"174":2,"176":3}}],["config",{"0":{"226":1,"227":1},"2":{"0":1,"2":1,"7":1,"8":6,"95":1,"96":1,"107":1,"129":1,"134":1,"135":1,"141":2,"143":1,"165":6,"167":1,"174":6,"177":2,"183":3,"184":1,"197":2,"199":6,"207":1,"217":1,"226":14,"227":3,"228":2,"229":2,"230":1,"231":1,"232":1,"233":1}}],["consolidate",{"2":{"182":1}}],["consolidates",{"2":{"107":1}}],["console",{"2":{"8":1,"12":2,"20":1,"188":1,"189":1}}],["consumption",{"2":{"75":1,"77":1}}],["consumed",{"2":{"74":1}}],["considered",{"2":{"162":1}}],["consists",{"2":{"140":1}}],["consistency",{"2":{"56":1,"102":1,"154":1,"161":1}}],["consistent",{"2":{"2":1,"33":1,"48":1,"58":1,"92":1,"94":1,"96":1}}],["consistently",{"2":{"0":1,"83":1}}],["consicely",{"2":{"66":1}}],["constants",{"0":{"175":1,"176":1},"1":{"176":1},"2":{"175":2,"176":3}}],["constrained",{"2":{"178":1}}],["constraints",{"2":{"26":1,"84":1}}],["constraint",{"0":{"26":1},"1":{"27":1},"2":{"26":1}}],["constructs",{"2":{"164":2}}],["constructed",{"2":{"162":1}}],["const",{"2":{"8":2,"17":2,"25":2,"27":2,"29":2,"31":2,"36":4,"51":1,"52":1,"53":2,"55":1,"61":1,"63":1,"65":5,"66":3,"172":2,"184":4,"188":1,"189":4,"190":4,"195":21,"196":24,"197":21,"209":2}}],["aware",{"2":{"194":1}}],["await",{"2":{"17":2,"25":2,"27":2,"29":2,"31":2,"40":1,"53":1,"60":2,"63":1,"65":1,"66":1}}],["audiowide",{"2":{"181":3}}],["autoscale=10",{"2":{"232":1}}],["autoscale=8",{"2":{"229":1}}],["autoscale=2",{"2":{"144":1,"232":1,"233":1}}],["autocomplete>",{"2":{"189":1}}],["autocomplete",{"0":{"187":1},"1":{"188":1,"189":1,"190":1,"191":1,"192":1,"193":1},"2":{"188":1,"189":1,"190":1,"191":3,"192":2,"193":2}}],["autoimporting",{"2":{"171":1}}],["autoincrement",{"2":{"58":1}}],["autogen",{"2":{"95":1}}],["autoregister",{"2":{"87":1}}],["auto",{"0":{"58":1},"2":{"95":1,"170":2,"171":1,"209":1,"232":1}}],["automated",{"2":{"103":1}}],["automates",{"2":{"56":1}}],["automatic",{"2":{"95":1,"111":2}}],["automatically",{"2":{"10":1,"15":1,"17":1,"53":1,"54":1,"58":1,"87":1,"89":1,"90":1,"133":1,"174":1,"230":1}}],["automating",{"2":{"34":1}}],["authservice",{"2":{"91":1}}],["authfailures",{"2":{"90":1,"91":1}}],["authorize",{"2":{"66":1}}],["authorized",{"2":{"65":1,"66":1,"157":1,"161":1,"164":1,"212":1,"218":2}}],["authorization",{"0":{"64":1,"135":1},"1":{"65":1,"66":1},"2":{"63":1,"64":1,"66":1,"154":1,"218":1}}],["authenticated",{"2":{"177":1}}],["authenticateuser",{"2":{"91":1}}],["authenticate",{"2":{"63":4,"65":3,"66":2,"106":1,"146":2}}],["authentication",{"0":{"63":1,"135":1,"177":1,"178":1},"1":{"178":1},"2":{"63":3,"90":2,"98":1,"101":1,"105":2,"113":1,"134":1,"135":2,"177":3,"178":3}}],["authnetication",{"2":{"63":1}}],["auth",{"0":{"145":1,"146":1,"147":1},"1":{"146":1,"147":1,"148":1,"149":1,"150":1,"151":1,"152":1,"153":1,"154":1},"2":{"6":2,"63":2,"65":1,"66":1,"90":2,"91":1,"106":1,"141":2,"143":2,"144":1,"153":1,"154":2,"165":1,"177":7,"233":1}}],["ae5f",{"2":{"160":1}}],["affected",{"2":{"138":1}}],["after",{"0":{"151":1},"2":{"25":1,"27":1,"29":1,"31":1,"43":1,"45":2,"63":1,"81":1,"138":1,"146":1,"184":1,"216":2,"218":1,"221":2}}],["ability",{"2":{"194":1}}],["absolute",{"2":{"182":1}}],["abstraction",{"2":{"100":1}}],["abstractions",{"0":{"100":1},"2":{"98":1}}],["able",{"2":{"158":1,"212":1}}],["about",{"2":{"85":2,"88":1,"165":1,"215":2}}],["above",{"2":{"33":1,"66":1,"163":1,"182":1,"199":2,"200":1,"213":1,"216":1}}],["airbnb",{"2":{"96":1}}],["among",{"2":{"217":1}}],["amount",{"2":{"77":1}}],["ambiguous",{"2":{"141":1}}],["amp",{"2":{"65":1,"226":1}}],["ago",{"2":{"182":1}}],["again",{"2":{"146":2}}],["against",{"2":{"65":1,"143":1}}],["aggregation",{"0":{"92":1},"1":{"93":1,"94":1},"2":{"93":1}}],["aggregates",{"2":{"88":1}}],["aggregated",{"2":{"13":1,"92":1,"93":1}}],["age",{"2":{"51":5,"52":3,"53":3,"54":2}}],["avoided",{"2":{"212":1}}],["avoid",{"0":{"60":1},"2":{"37":1,"58":1,"87":1,"111":1}}],["available",{"2":{"9":1,"76":2,"232":1}}],["attr",{"2":{"184":1}}],["attempting",{"2":{"158":1}}],["attempts",{"2":{"90":2}}],["attached",{"2":{"203":1}}],["attaches",{"2":{"65":1,"105":1,"218":1}}],["attach",{"2":{"105":2}}],["at",{"2":{"32":1,"34":1,"38":1,"39":1,"51":1,"55":1,"81":1,"107":1,"129":1,"133":1,"183":1,"194":1,"199":2,"217":1,"229":1}}],["axioserrorhandler",{"2":{"31":1,"32":1}}],["axios",{"0":{"30":1},"1":{"31":1},"2":{"30":1,"31":4,"106":1,"185":2}}],["always",{"2":{"232":1}}],["alertforenvironments",{"2":{"207":1}}],["alerting",{"2":{"85":1,"94":1}}],["alert",{"2":{"69":1,"175":1,"207":2,"208":3}}],["alias",{"2":{"126":5,"160":6,"233":1}}],["aliases",{"2":{"126":1}}],["aliasing",{"0":{"59":1},"2":{"59":1}}],["alive",{"2":{"69":1}}],["already",{"2":{"27":1,"49":1}}],["also",{"2":{"13":1,"54":1,"88":1,"119":1,"162":1,"163":1,"197":1,"201":1,"212":1}}],["allocated",{"2":{"75":1,"76":3,"77":1,"232":1}}],["allow",{"2":{"146":1,"157":1,"159":1,"199":1}}],["allowed",{"2":{"11":1,"55":1,"160":1}}],["allows",{"2":{"10":1,"12":1,"34":1,"101":1,"160":1,"161":1,"183":1,"211":1}}],["allowing",{"2":{"2":1,"36":1,"131":1,"199":1}}],["all",{"2":{"2":1,"5":1,"18":1,"35":1,"54":1,"60":1,"62":1,"63":1,"69":1,"88":1,"89":1,"92":1,"94":1,"105":1,"106":1,"107":1,"117":1,"118":1,"140":1,"160":1,"167":1,"174":2,"182":1,"184":1,"216":1,"217":1,"220":2,"221":1}}],["anonymous",{"2":{"178":1}}],["another",{"2":{"81":1,"143":1,"174":1}}],["antfu",{"2":{"171":1}}],["analyze",{"2":{"132":1,"170":1}}],["analysis",{"2":{"77":1}}],["anyone",{"2":{"159":1}}],["any",{"2":{"14":1,"15":1,"64":1,"65":6,"66":1,"141":1,"159":1,"162":1,"178":1,"202":2,"216":1,"223":1}}],["an",{"2":{"11":1,"15":1,"36":1,"52":1,"61":1,"64":1,"67":1,"80":1,"105":1,"141":1,"143":1,"144":1,"154":6,"157":1,"159":1,"162":1,"163":2,"164":1,"167":1,"174":1,"196":1,"197":1,"200":1,"207":1,"213":1,"216":2,"217":2,"218":2,"219":1,"222":1,"233":1}}],["and",{"0":{"59":1,"70":1,"127":1,"135":1,"141":1,"142":1,"158":1,"182":1,"185":1,"197":1,"212":1,"233":1},"1":{"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"128":1,"129":1,"130":1,"131":1,"132":1,"133":1,"134":1,"135":1,"136":1,"137":1,"138":1,"159":1},"2":{"0":2,"1":4,"2":1,"5":1,"7":1,"8":1,"9":3,"10":1,"12":1,"14":2,"16":1,"17":1,"18":1,"20":2,"23":1,"24":1,"26":1,"28":1,"30":1,"32":1,"33":1,"34":2,"35":2,"36":1,"37":1,"40":2,"42":1,"43":1,"46":4,"47":5,"49":2,"51":3,"52":2,"53":1,"54":4,"55":2,"56":2,"58":1,"59":1,"60":1,"62":2,"63":3,"64":1,"65":4,"70":1,"72":1,"75":1,"77":1,"80":1,"81":2,"85":5,"86":1,"90":1,"92":1,"93":1,"94":2,"95":2,"96":1,"98":3,"100":1,"101":1,"102":3,"103":4,"105":10,"106":5,"107":3,"110":1,"111":1,"119":1,"121":1,"127":3,"128":2,"130":1,"131":1,"132":1,"134":1,"137":2,"138":3,"139":1,"140":1,"141":1,"143":5,"144":4,"153":3,"154":7,"157":2,"158":1,"159":2,"160":3,"161":5,"162":7,"163":3,"164":6,"165":7,"167":2,"168":2,"170":3,"171":2,"174":4,"182":3,"191":1,"193":1,"196":1,"197":3,"198":2,"199":4,"200":1,"209":2,"212":2,"213":3,"215":1,"217":2,"218":1,"220":2,"223":3,"226":3,"227":1,"228":2,"231":1,"232":1,"233":4}}],["actually",{"2":{"232":1}}],["actual",{"2":{"216":1}}],["activate",{"2":{"143":2}}],["active",{"0":{"69":1,"70":2},"1":{"70":1,"79":2,"80":2,"81":2,"82":2,"83":2,"84":2},"2":{"69":7,"70":2,"146":1,"172":1,"229":1}}],["action",{"2":{"66":1}}],["actions",{"2":{"65":1}}],["achieve",{"2":{"212":1}}],["acl",{"2":{"160":1}}],["account",{"2":{"144":1,"205":2,"233":1}}],["accurate",{"2":{"94":1}}],["accurately",{"2":{"84":1}}],["accel",{"2":{"164":1}}],["accepting",{"2":{"232":1}}],["accept",{"2":{"122":1}}],["accepts",{"2":{"11":1,"218":1}}],["accessed",{"2":{"197":2}}],["accessible",{"2":{"92":1,"133":1,"158":1,"162":1}}],["accessing",{"2":{"2":1}}],["accesscontrols",{"2":{"64":1}}],["accesscontrol",{"0":{"66":1},"2":{"64":2,"66":4}}],["access",{"0":{"162":1,"218":1},"1":{"219":1},"2":{"8":1,"64":1,"105":2,"122":1,"125":1,"133":2,"136":1,"138":1,"141":1,"143":1,"156":1,"157":2,"158":2,"159":6,"160":5,"161":2,"162":7,"163":2,"164":1,"178":1,"197":1,"211":2,"212":1,"213":1,"218":1,"226":1}}],["ac",{"2":{"65":2}}],["across",{"2":{"0":1,"9":1,"56":1,"93":1,"94":1,"102":1,"107":1,"175":1}}],["arise",{"2":{"212":1}}],["archive",{"2":{"160":1,"203":1}}],["archived",{"2":{"160":1}}],["architecture",{"0":{"97":1,"163":1,"210":1,"213":1},"1":{"164":1,"211":1,"212":1,"213":1,"214":1,"215":1,"216":1,"217":1,"218":1,"219":1,"220":1,"221":1,"222":1,"223":1},"2":{"156":1,"157":1,"163":1,"212":1,"213":1}}],["array",{"2":{"52":1,"55":2,"191":1,"199":9,"206":2}}],["argument",{"2":{"36":1,"198":2}}],["arguments",{"2":{"2":1,"4":1}}],["are",{"2":{"1":1,"3":1,"4":1,"5":1,"12":1,"14":1,"15":1,"18":1,"22":1,"32":1,"36":1,"38":2,"39":1,"42":1,"43":1,"44":1,"45":1,"46":2,"51":1,"54":1,"58":1,"63":1,"64":1,"65":2,"72":1,"76":4,"81":1,"82":1,"84":1,"86":1,"88":1,"89":1,"92":2,"98":1,"105":1,"107":1,"127":1,"128":2,"137":1,"141":1,"144":1,"154":1,"159":1,"160":5,"161":2,"162":1,"167":1,"170":1,"171":4,"174":4,"175":2,"176":1,"177":1,"182":1,"184":1,"185":1,"191":1,"208":1,"211":1,"213":1,"216":5,"217":4,"218":1,"220":1,"221":2,"223":3,"227":1,"228":1,"232":1,"233":1}}],["apps",{"2":{"232":1}}],["append",{"2":{"193":1}}],["appendinner",{"2":{"193":3}}],["apptitle",{"2":{"165":1}}],["appuser",{"2":{"141":1}}],["applying",{"2":{"194":1}}],["apply",{"2":{"105":2,"106":1}}],["applies",{"2":{"56":1,"105":2}}],["applied",{"2":{"4":1}}],["applications",{"2":{"9":2,"84":1,"85":1,"98":1,"101":1,"103":1}}],["application",{"2":{"0":2,"1":3,"2":3,"3":1,"5":1,"8":1,"9":3,"14":1,"34":1,"37":1,"38":1,"43":2,"44":1,"45":1,"46":2,"64":1,"67":1,"80":1,"83":1,"85":2,"89":1,"101":1,"107":4,"114":1,"123":1,"127":2,"130":1,"137":1,"138":1,"139":1,"140":1,"154":1,"160":1,"163":1,"174":2,"176":3,"213":1}}],["approaches",{"0":{"50":1},"1":{"51":1,"52":1,"53":1,"54":1}}],["approach",{"2":{"32":1,"40":1,"48":1,"51":1,"98":1,"101":1,"154":2,"161":1}}],["appropriate",{"2":{"20":1,"26":1,"32":1,"105":1,"106":1,"154":1,"213":3}}],["app",{"0":{"148":1},"2":{"32":1,"53":1,"89":2,"105":2,"107":1,"128":1,"131":1,"133":1,"135":1,"141":4,"143":1,"144":7,"146":2,"154":1,"163":1,"165":6,"179":1,"183":1,"207":1,"213":1,"226":3,"229":2,"230":1,"231":1,"232":6,"233":7}}],["api>",{"2":{"144":1,"232":1,"233":1}}],["api",{"0":{"141":1,"185":1,"233":1},"2":{"1":1,"2":1,"31":2,"35":1,"36":1,"56":1,"63":1,"74":1,"95":4,"107":1,"110":4,"111":1,"113":2,"115":2,"117":1,"118":2,"121":2,"122":3,"123":1,"125":1,"140":1,"141":14,"142":2,"143":4,"144":9,"153":2,"154":6,"159":2,"160":2,"162":1,"163":3,"164":1,"165":2,"167":5,"168":3,"169":2,"177":2,"182":1,"184":2,"185":1,"212":1,"213":5,"216":1,"218":4,"219":1,"233":9}}],["aspects",{"2":{"176":1}}],["ask",{"2":{"165":2}}],["asc",{"2":{"55":1}}],["assigned",{"2":{"226":1}}],["associated",{"2":{"162":1,"216":1,"221":1}}],["assuming",{"2":{"49":1}}],["assert",{"2":{"29":5}}],["assertions",{"2":{"106":1}}],["assertionerrorhandler",{"2":{"29":1,"32":1}}],["assertionerror",{"2":{"28":1,"29":1}}],["assertion",{"0":{"28":1},"1":{"29":1},"2":{"135":1}}],["async=",{"2":{"190":1}}],["async",{"0":{"190":1},"2":{"17":2,"25":2,"27":2,"29":2,"31":2,"36":2,"40":2,"53":1,"54":1,"63":1,"65":1,"66":1,"105":2,"191":1}}],["asynchandler",{"2":{"15":1,"17":4,"25":2,"27":2,"29":2,"31":2,"53":3,"54":1,"63":1,"65":1,"66":1}}],["asynchronous",{"0":{"15":1},"1":{"16":1,"17":1},"2":{"15":2,"16":1,"17":1,"54":1,"105":1,"191":1}}],["as",{"2":{"1":2,"9":2,"32":1,"35":1,"36":2,"38":1,"42":1,"49":1,"56":1,"58":1,"59":1,"63":2,"73":1,"80":1,"81":2,"83":1,"86":1,"98":1,"106":1,"131":1,"133":1,"134":1,"135":3,"136":1,"138":1,"143":2,"146":2,"154":2,"159":1,"160":3,"162":2,"164":2,"165":1,"167":1,"180":1,"182":2,"197":3,"199":1,"216":2,"217":2,"218":1,"219":1,"220":2,"223":1,"226":3}}],["adjust",{"2":{"176":1}}],["adjusted",{"2":{"82":1}}],["adjacent",{"2":{"163":1}}],["adherence",{"0":{"102":1},"2":{"103":1}}],["admin",{"2":{"64":1,"65":3,"133":1,"135":1,"138":1,"162":1,"178":2,"184":1}}],["added",{"2":{"63":3,"89":1,"199":2}}],["additional",{"0":{"181":1},"2":{"56":1,"90":1,"98":1,"194":1}}],["adding",{"0":{"181":1},"2":{"48":1}}],["address",{"2":{"15":1,"51":1,"197":1}}],["add",{"2":{"8":2,"40":1,"63":1,"74":1,"90":1,"95":1,"115":1,"126":1,"138":2,"143":1,"165":2,"174":3,"181":2,"182":1,"184":1,"226":2,"231":2}}],["ad",{"2":{"1":1}}],["a",{"0":{"65":1,"144":1,"152":1,"164":1,"165":1},"2":{"0":1,"2":1,"5":1,"7":1,"8":3,"9":7,"10":1,"11":3,"12":4,"13":3,"14":2,"16":1,"18":1,"19":1,"20":1,"23":1,"24":1,"28":1,"30":1,"35":1,"36":4,"40":1,"46":1,"48":1,"51":1,"53":1,"54":1,"56":2,"58":3,"61":1,"63":1,"64":1,"66":1,"69":1,"72":1,"76":2,"77":1,"80":2,"81":4,"83":1,"84":1,"85":2,"86":2,"88":1,"90":1,"92":2,"93":1,"95":1,"98":6,"101":1,"103":2,"105":6,"106":2,"128":1,"135":2,"137":1,"138":1,"140":1,"143":4,"144":3,"146":2,"153":3,"154":4,"156":1,"159":3,"160":7,"161":2,"162":8,"163":5,"164":8,"167":1,"171":2,"174":2,"182":2,"183":2,"184":4,"185":1,"191":2,"193":1,"197":4,"198":4,"199":9,"203":2,"205":1,"209":2,"211":1,"212":1,"213":5,"215":2,"216":3,"217":2,"218":6,"220":1,"226":5,"227":1,"229":2,"231":1,"232":1,"233":3}}]],"serializationVersion":2}';export{e as default}; diff --git a/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.Crn_Pjbv.js b/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.Crn_Pjbv.js new file mode 100644 index 000000000..e4d3363d1 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.Crn_Pjbv.js @@ -0,0 +1,8 @@ +var Nt=Object.defineProperty;var Ft=(a,e,t)=>e in a?Nt(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var Ce=(a,e,t)=>Ft(a,typeof e!="symbol"?e+"":e,t);import{V as Ot,D as le,h as ge,ah as et,ai as Rt,aj as Ct,ak as At,q as $e,al as Mt,d as Lt,am as tt,p as fe,an as Dt,ao as Pt,s as zt,ap as Vt,v as Ae,P as he,O as _e,aq as $t,ar as jt,W as Bt,R as Wt,$ as Kt,b as Jt,o as H,j as _,a0 as qt,as as Ut,k as L,at as Gt,au as Ht,c as Z,e as Se,n as nt,B as st,F as it,a as pe,t as me,av as Qt,aw as rt,ax as Yt,a5 as Zt,aa as Xt,ay as en,_ as tn}from"./framework.C9SxlbOG.js";import{u as nn,c as sn}from"./theme.BqY363-_.js";const rn={root:()=>Ot(()=>import("./@localSearchIndexroot.BA45FNPv.js"),[])};/*! +* tabbable 6.2.0 +* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE +*/var mt=["input:not([inert])","select:not([inert])","textarea:not([inert])","a[href]:not([inert])","button:not([inert])","[tabindex]:not(slot):not([inert])","audio[controls]:not([inert])","video[controls]:not([inert])",'[contenteditable]:not([contenteditable="false"]):not([inert])',"details>summary:first-of-type:not([inert])","details:not([inert])"],ke=mt.join(","),vt=typeof Element>"u",re=vt?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,Ne=!vt&&Element.prototype.getRootNode?function(a){var e;return a==null||(e=a.getRootNode)===null||e===void 0?void 0:e.call(a)}:function(a){return a==null?void 0:a.ownerDocument},Fe=function a(e,t){var n;t===void 0&&(t=!0);var s=e==null||(n=e.getAttribute)===null||n===void 0?void 0:n.call(e,"inert"),r=s===""||s==="true",i=r||t&&e&&a(e.parentNode);return i},an=function(e){var t,n=e==null||(t=e.getAttribute)===null||t===void 0?void 0:t.call(e,"contenteditable");return n===""||n==="true"},gt=function(e,t,n){if(Fe(e))return[];var s=Array.prototype.slice.apply(e.querySelectorAll(ke));return t&&re.call(e,ke)&&s.unshift(e),s=s.filter(n),s},bt=function a(e,t,n){for(var s=[],r=Array.from(e);r.length;){var i=r.shift();if(!Fe(i,!1))if(i.tagName==="SLOT"){var o=i.assignedElements(),l=o.length?o:i.children,c=a(l,!0,n);n.flatten?s.push.apply(s,c):s.push({scopeParent:i,candidates:c})}else{var f=re.call(i,ke);f&&n.filter(i)&&(t||!e.includes(i))&&s.push(i);var v=i.shadowRoot||typeof n.getShadowRoot=="function"&&n.getShadowRoot(i),h=!Fe(v,!1)&&(!n.shadowRootFilter||n.shadowRootFilter(i));if(v&&h){var b=a(v===!0?i.children:v.children,!0,n);n.flatten?s.push.apply(s,b):s.push({scopeParent:i,candidates:b})}else r.unshift.apply(r,i.children)}}return s},yt=function(e){return!isNaN(parseInt(e.getAttribute("tabindex"),10))},ie=function(e){if(!e)throw new Error("No node provided");return e.tabIndex<0&&(/^(AUDIO|VIDEO|DETAILS)$/.test(e.tagName)||an(e))&&!yt(e)?0:e.tabIndex},on=function(e,t){var n=ie(e);return n<0&&t&&!yt(e)?0:n},ln=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},wt=function(e){return e.tagName==="INPUT"},cn=function(e){return wt(e)&&e.type==="hidden"},un=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(n){return n.tagName==="SUMMARY"});return t},dn=function(e,t){for(var n=0;nsummary:first-of-type"),i=r?e.parentElement:e;if(re.call(i,"details:not([open]) *"))return!0;if(!n||n==="full"||n==="legacy-full"){if(typeof s=="function"){for(var o=e;e;){var l=e.parentElement,c=Ne(e);if(l&&!l.shadowRoot&&s(l)===!0)return at(e);e.assignedSlot?e=e.assignedSlot:!l&&c!==e.ownerDocument?e=c.host:e=l}e=o}if(mn(e))return!e.getClientRects().length;if(n!=="legacy-full")return!0}else if(n==="non-zero-area")return at(e);return!1},gn=function(e){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(e.tagName))for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var n=0;n=0)},yn=function a(e){var t=[],n=[];return e.forEach(function(s,r){var i=!!s.scopeParent,o=i?s.scopeParent:s,l=on(o,i),c=i?a(s.candidates):o;l===0?i?t.push.apply(t,c):t.push(o):n.push({documentOrder:r,tabIndex:l,item:s,isScope:i,content:c})}),n.sort(ln).reduce(function(s,r){return r.isScope?s.push.apply(s,r.content):s.push(r.content),s},[]).concat(t)},wn=function(e,t){t=t||{};var n;return t.getShadowRoot?n=bt([e],t.includeContainer,{filter:je.bind(null,t),flatten:!1,getShadowRoot:t.getShadowRoot,shadowRootFilter:bn}):n=gt(e,t.includeContainer,je.bind(null,t)),yn(n)},xn=function(e,t){t=t||{};var n;return t.getShadowRoot?n=bt([e],t.includeContainer,{filter:Oe.bind(null,t),flatten:!0,getShadowRoot:t.getShadowRoot}):n=gt(e,t.includeContainer,Oe.bind(null,t)),n},ae=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return re.call(e,ke)===!1?!1:je(t,e)},_n=mt.concat("iframe").join(","),Me=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return re.call(e,_n)===!1?!1:Oe(t,e)};/*! +* focus-trap 7.6.4 +* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE +*/function Be(a,e){(e==null||e>a.length)&&(e=a.length);for(var t=0,n=Array(e);t0){var n=e[e.length-1];n!==t&&n._setPausedState(!0)}var s=e.indexOf(t);s===-1||e.splice(s,1),e.push(t)},deactivateTrap:function(e,t){var n=e.indexOf(t);n!==-1&&e.splice(n,1),e.length>0&&!e[e.length-1]._isManuallyPaused()&&e[e.length-1]._setPausedState(!1)}},Rn=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},Cn=function(e){return(e==null?void 0:e.key)==="Escape"||(e==null?void 0:e.key)==="Esc"||(e==null?void 0:e.keyCode)===27},be=function(e){return(e==null?void 0:e.key)==="Tab"||(e==null?void 0:e.keyCode)===9},An=function(e){return be(e)&&!e.shiftKey},Mn=function(e){return be(e)&&e.shiftKey},ut=function(e){return setTimeout(e,0)},ve=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),s=1;s1&&arguments[1]!==void 0?arguments[1]:{},g=d.hasFallback,E=g===void 0?!1:g,T=d.params,F=T===void 0?[]:T,S=r[u];if(typeof S=="function"&&(S=S.apply(void 0,kn(F))),S===!0&&(S=void 0),!S){if(S===void 0||S===!1)return S;throw new Error("`".concat(u,"` was specified but was not a node, or did not return a node"))}var R=S;if(typeof S=="string"){try{R=n.querySelector(S)}catch(m){throw new Error("`".concat(u,'` appears to be an invalid selector; error="').concat(m.message,'"'))}if(!R&&!E)throw new Error("`".concat(u,"` as selector refers to no known node"))}return R},v=function(){var u=f("initialFocus",{hasFallback:!0});if(u===!1)return!1;if(u===void 0||u&&!Me(u,r.tabbableOptions))if(c(n.activeElement)>=0)u=n.activeElement;else{var d=i.tabbableGroups[0],g=d&&d.firstTabbableNode;u=g||f("fallbackFocus")}else u===null&&(u=f("fallbackFocus"));if(!u)throw new Error("Your focus-trap needs to have at least one focusable element");return u},h=function(){if(i.containerGroups=i.containers.map(function(u){var d=wn(u,r.tabbableOptions),g=xn(u,r.tabbableOptions),E=d.length>0?d[0]:void 0,T=d.length>0?d[d.length-1]:void 0,F=g.find(function(m){return ae(m)}),S=g.slice().reverse().find(function(m){return ae(m)}),R=!!d.find(function(m){return ie(m)>0});return{container:u,tabbableNodes:d,focusableNodes:g,posTabIndexesFound:R,firstTabbableNode:E,lastTabbableNode:T,firstDomTabbableNode:F,lastDomTabbableNode:S,nextTabbableNode:function(p){var I=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,O=d.indexOf(p);return O<0?I?g.slice(g.indexOf(p)+1).find(function(P){return ae(P)}):g.slice(0,g.indexOf(p)).reverse().find(function(P){return ae(P)}):d[O+(I?1:-1)]}}}),i.tabbableGroups=i.containerGroups.filter(function(u){return u.tabbableNodes.length>0}),i.tabbableGroups.length<=0&&!f("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times");if(i.containerGroups.find(function(u){return u.posTabIndexesFound})&&i.containerGroups.length>1)throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.")},b=function(u){var d=u.activeElement;if(d)return d.shadowRoot&&d.shadowRoot.activeElement!==null?b(d.shadowRoot):d},y=function(u){if(u!==!1&&u!==b(document)){if(!u||!u.focus){y(v());return}u.focus({preventScroll:!!r.preventScroll}),i.mostRecentlyFocusedNode=u,Rn(u)&&u.select()}},x=function(u){var d=f("setReturnFocus",{params:[u]});return d||(d===!1?!1:u)},w=function(u){var d=u.target,g=u.event,E=u.isBackward,T=E===void 0?!1:E;d=d||Ee(g),h();var F=null;if(i.tabbableGroups.length>0){var S=c(d,g),R=S>=0?i.containerGroups[S]:void 0;if(S<0)T?F=i.tabbableGroups[i.tabbableGroups.length-1].lastTabbableNode:F=i.tabbableGroups[0].firstTabbableNode;else if(T){var m=i.tabbableGroups.findIndex(function(V){var k=V.firstTabbableNode;return d===k});if(m<0&&(R.container===d||Me(d,r.tabbableOptions)&&!ae(d,r.tabbableOptions)&&!R.nextTabbableNode(d,!1))&&(m=S),m>=0){var p=m===0?i.tabbableGroups.length-1:m-1,I=i.tabbableGroups[p];F=ie(d)>=0?I.lastTabbableNode:I.lastDomTabbableNode}else be(g)||(F=R.nextTabbableNode(d,!1))}else{var O=i.tabbableGroups.findIndex(function(V){var k=V.lastTabbableNode;return d===k});if(O<0&&(R.container===d||Me(d,r.tabbableOptions)&&!ae(d,r.tabbableOptions)&&!R.nextTabbableNode(d))&&(O=S),O>=0){var P=O===i.tabbableGroups.length-1?0:O+1,z=i.tabbableGroups[P];F=ie(d)>=0?z.firstTabbableNode:z.firstDomTabbableNode}else be(g)||(F=R.nextTabbableNode(d))}}else F=f("fallbackFocus");return F},C=function(u){var d=Ee(u);if(!(c(d,u)>=0)){if(ve(r.clickOutsideDeactivates,u)){o.deactivate({returnFocus:r.returnFocusOnDeactivate});return}ve(r.allowOutsideClick,u)||u.preventDefault()}},A=function(u){var d=Ee(u),g=c(d,u)>=0;if(g||d instanceof Document)g&&(i.mostRecentlyFocusedNode=d);else{u.stopImmediatePropagation();var E,T=!0;if(i.mostRecentlyFocusedNode)if(ie(i.mostRecentlyFocusedNode)>0){var F=c(i.mostRecentlyFocusedNode),S=i.containerGroups[F].tabbableNodes;if(S.length>0){var R=S.findIndex(function(m){return m===i.mostRecentlyFocusedNode});R>=0&&(r.isKeyForward(i.recentNavEvent)?R+1=0&&(E=S[R-1],T=!1))}}else i.containerGroups.some(function(m){return m.tabbableNodes.some(function(p){return ie(p)>0})})||(T=!1);else T=!1;T&&(E=w({target:i.mostRecentlyFocusedNode,isBackward:r.isKeyBackward(i.recentNavEvent)})),y(E||i.mostRecentlyFocusedNode||v())}i.recentNavEvent=void 0},J=function(u){var d=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;i.recentNavEvent=u;var g=w({event:u,isBackward:d});g&&(be(u)&&u.preventDefault(),y(g))},Q=function(u){(r.isKeyForward(u)||r.isKeyBackward(u))&&J(u,r.isKeyBackward(u))},W=function(u){Cn(u)&&ve(r.escapeDeactivates,u)!==!1&&(u.preventDefault(),o.deactivate())},$=function(u){var d=Ee(u);c(d,u)>=0||ve(r.clickOutsideDeactivates,u)||ve(r.allowOutsideClick,u)||(u.preventDefault(),u.stopImmediatePropagation())},j=function(){if(i.active)return ct.activateTrap(s,o),i.delayInitialFocusTimer=r.delayInitialFocus?ut(function(){y(v())}):y(v()),n.addEventListener("focusin",A,!0),n.addEventListener("mousedown",C,{capture:!0,passive:!1}),n.addEventListener("touchstart",C,{capture:!0,passive:!1}),n.addEventListener("click",$,{capture:!0,passive:!1}),n.addEventListener("keydown",Q,{capture:!0,passive:!1}),n.addEventListener("keydown",W),o},ye=function(){if(i.active)return n.removeEventListener("focusin",A,!0),n.removeEventListener("mousedown",C,!0),n.removeEventListener("touchstart",C,!0),n.removeEventListener("click",$,!0),n.removeEventListener("keydown",Q,!0),n.removeEventListener("keydown",W),o},M=function(u){var d=u.some(function(g){var E=Array.from(g.removedNodes);return E.some(function(T){return T===i.mostRecentlyFocusedNode})});d&&y(v())},q=typeof window<"u"&&"MutationObserver"in window?new MutationObserver(M):void 0,U=function(){q&&(q.disconnect(),i.active&&!i.paused&&i.containers.map(function(u){q.observe(u,{subtree:!0,childList:!0})}))};return o={get active(){return i.active},get paused(){return i.paused},activate:function(u){if(i.active)return this;var d=l(u,"onActivate"),g=l(u,"onPostActivate"),E=l(u,"checkCanFocusTrap");E||h(),i.active=!0,i.paused=!1,i.nodeFocusedBeforeActivation=n.activeElement,d==null||d();var T=function(){E&&h(),j(),U(),g==null||g()};return E?(E(i.containers.concat()).then(T,T),this):(T(),this)},deactivate:function(u){if(!i.active)return this;var d=lt({onDeactivate:r.onDeactivate,onPostDeactivate:r.onPostDeactivate,checkCanReturnFocus:r.checkCanReturnFocus},u);clearTimeout(i.delayInitialFocusTimer),i.delayInitialFocusTimer=void 0,ye(),i.active=!1,i.paused=!1,U(),ct.deactivateTrap(s,o);var g=l(d,"onDeactivate"),E=l(d,"onPostDeactivate"),T=l(d,"checkCanReturnFocus"),F=l(d,"returnFocus","returnFocusOnDeactivate");g==null||g();var S=function(){ut(function(){F&&y(x(i.nodeFocusedBeforeActivation)),E==null||E()})};return F&&T?(T(x(i.nodeFocusedBeforeActivation)).then(S,S),this):(S(),this)},pause:function(u){return i.active?(i.manuallyPaused=!0,this._setPausedState(!0,u)):this},unpause:function(u){return i.active?(i.manuallyPaused=!1,s[s.length-1]!==this?this:this._setPausedState(!1,u)):this},updateContainerElements:function(u){var d=[].concat(u).filter(Boolean);return i.containers=d.map(function(g){return typeof g=="string"?n.querySelector(g):g}),i.active&&h(),U(),this}},Object.defineProperties(o,{_isManuallyPaused:{value:function(){return i.manuallyPaused}},_setPausedState:{value:function(u,d){if(i.paused===u)return this;if(i.paused=u,u){var g=l(d,"onPause"),E=l(d,"onPostPause");g==null||g(),ye(),U(),E==null||E()}else{var T=l(d,"onUnpause"),F=l(d,"onPostUnpause");T==null||T(),h(),j(),U(),F==null||F()}return this}}}),o.updateContainerElements(e),o};function Pn(a,e={}){let t;const{immediate:n,...s}=e,r=le(!1),i=le(!1),o=h=>t&&t.activate(h),l=h=>t&&t.deactivate(h),c=()=>{t&&(t.pause(),i.value=!0)},f=()=>{t&&(t.unpause(),i.value=!1)},v=ge(()=>{const h=et(a);return Rt(h).map(b=>{const y=et(b);return typeof y=="string"?y:Ct(y)}).filter(At)});return $e(v,h=>{h.length&&(t=Dn(h,{...s,onActivate(){r.value=!0,e.onActivate&&e.onActivate()},onDeactivate(){r.value=!1,e.onDeactivate&&e.onDeactivate()}}),n&&o())},{flush:"post"}),Mt(()=>l()),{hasFocus:r,isPaused:i,activate:o,deactivate:l,pause:c,unpause:f}}class ce{constructor(e,t=!0,n=[],s=5e3){this.ctx=e,this.iframes=t,this.exclude=n,this.iframesTimeout=s}static matches(e,t){const n=typeof t=="string"?[t]:t,s=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(s){let r=!1;return n.every(i=>s.call(e,i)?(r=!0,!1):!0),r}else return!1}getContexts(){let e,t=[];return typeof this.ctx>"u"||!this.ctx?e=[]:NodeList.prototype.isPrototypeOf(this.ctx)?e=Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?e=this.ctx:typeof this.ctx=="string"?e=Array.prototype.slice.call(document.querySelectorAll(this.ctx)):e=[this.ctx],e.forEach(n=>{const s=t.filter(r=>r.contains(n)).length>0;t.indexOf(n)===-1&&!s&&t.push(n)}),t}getIframeContents(e,t,n=()=>{}){let s;try{const r=e.contentWindow;if(s=r.document,!r||!s)throw new Error("iframe inaccessible")}catch{n()}s&&t(s)}isIframeBlank(e){const t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}observeIframeLoad(e,t,n){let s=!1,r=null;const i=()=>{if(!s){s=!0,clearTimeout(r);try{this.isIframeBlank(e)||(e.removeEventListener("load",i),this.getIframeContents(e,t,n))}catch{n()}}};e.addEventListener("load",i),r=setTimeout(i,this.iframesTimeout)}onIframeReady(e,t,n){try{e.contentWindow.document.readyState==="complete"?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch{n()}}waitForIframes(e,t){let n=0;this.forEachIframe(e,()=>!0,s=>{n++,this.waitForIframes(s.querySelector("html"),()=>{--n||t()})},s=>{s||t()})}forEachIframe(e,t,n,s=()=>{}){let r=e.querySelectorAll("iframe"),i=r.length,o=0;r=Array.prototype.slice.call(r);const l=()=>{--i<=0&&s(o)};i||l(),r.forEach(c=>{ce.matches(c,this.exclude)?l():this.onIframeReady(c,f=>{t(c)&&(o++,n(f)),l()},l)})}createIterator(e,t,n){return document.createNodeIterator(e,t,n,!1)}createInstanceOnIframe(e){return new ce(e.querySelector("html"),this.iframes)}compareNodeIframe(e,t,n){const s=e.compareDocumentPosition(n),r=Node.DOCUMENT_POSITION_PRECEDING;if(s&r)if(t!==null){const i=t.compareDocumentPosition(n),o=Node.DOCUMENT_POSITION_FOLLOWING;if(i&o)return!0}else return!0;return!1}getIteratorNode(e){const t=e.previousNode();let n;return t===null?n=e.nextNode():n=e.nextNode()&&e.nextNode(),{prevNode:t,node:n}}checkIframeFilter(e,t,n,s){let r=!1,i=!1;return s.forEach((o,l)=>{o.val===n&&(r=l,i=o.handled)}),this.compareNodeIframe(e,t,n)?(r===!1&&!i?s.push({val:n,handled:!0}):r!==!1&&!i&&(s[r].handled=!0),!0):(r===!1&&s.push({val:n,handled:!1}),!1)}handleOpenIframes(e,t,n,s){e.forEach(r=>{r.handled||this.getIframeContents(r.val,i=>{this.createInstanceOnIframe(i).forEachNode(t,n,s)})})}iterateThroughNodes(e,t,n,s,r){const i=this.createIterator(t,e,s);let o=[],l=[],c,f,v=()=>({prevNode:f,node:c}=this.getIteratorNode(i),c);for(;v();)this.iframes&&this.forEachIframe(t,h=>this.checkIframeFilter(c,f,h,o),h=>{this.createInstanceOnIframe(h).forEachNode(e,b=>l.push(b),s)}),l.push(c);l.forEach(h=>{n(h)}),this.iframes&&this.handleOpenIframes(o,e,n,s),r()}forEachNode(e,t,n,s=()=>{}){const r=this.getContexts();let i=r.length;i||s(),r.forEach(o=>{const l=()=>{this.iterateThroughNodes(e,o,t,n,()=>{--i<=0&&s()})};this.iframes?this.waitForIframes(o,l):l()})}}let zn=class{constructor(e){this.ctx=e,this.ie=!1;const t=window.navigator.userAgent;(t.indexOf("MSIE")>-1||t.indexOf("Trident")>-1)&&(this.ie=!0)}set opt(e){this._opt=Object.assign({},{element:"",className:"",exclude:[],iframes:!1,iframesTimeout:5e3,separateWordSearch:!0,diacritics:!0,synonyms:{},accuracy:"partially",acrossElements:!1,caseSensitive:!1,ignoreJoiners:!1,ignoreGroups:0,ignorePunctuation:[],wildcards:"disabled",each:()=>{},noMatch:()=>{},filter:()=>!0,done:()=>{},debug:!1,log:window.console},e)}get opt(){return this._opt}get iterator(){return new ce(this.ctx,this.opt.iframes,this.opt.exclude,this.opt.iframesTimeout)}log(e,t="debug"){const n=this.opt.log;this.opt.debug&&typeof n=="object"&&typeof n[t]=="function"&&n[t](`mark.js: ${e}`)}escapeStr(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}createRegExp(e){return this.opt.wildcards!=="disabled"&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),this.opt.wildcards!=="disabled"&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),e}createSynonymsRegExp(e){const t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",s=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(let r in t)if(t.hasOwnProperty(r)){const i=t[r],o=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(r):this.escapeStr(r),l=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(i):this.escapeStr(i);o!==""&&l!==""&&(e=e.replace(new RegExp(`(${this.escapeStr(o)}|${this.escapeStr(l)})`,`gm${n}`),s+`(${this.processSynomyms(o)}|${this.processSynomyms(l)})`+s))}return e}processSynomyms(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}setupWildcardsRegExp(e){return e=e.replace(/(?:\\)*\?/g,t=>t.charAt(0)==="\\"?"?":""),e.replace(/(?:\\)*\*/g,t=>t.charAt(0)==="\\"?"*":"")}createWildcardsRegExp(e){let t=this.opt.wildcards==="withSpaces";return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}setupIgnoreJoinersRegExp(e){return e.replace(/[^(|)\\]/g,(t,n,s)=>{let r=s.charAt(n+1);return/[(|)\\]/.test(r)||r===""?t:t+"\0"})}createJoinersRegExp(e){let t=[];const n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join(`[${t.join("")}]*`):e}createDiacriticsRegExp(e){const t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"];let s=[];return e.split("").forEach(r=>{n.every(i=>{if(i.indexOf(r)!==-1){if(s.indexOf(i)>-1)return!1;e=e.replace(new RegExp(`[${i}]`,`gm${t}`),`[${i}]`),s.push(i)}return!0})}),e}createMergedBlanksRegExp(e){return e.replace(/[\s]+/gmi,"[\\s]+")}createAccuracyRegExp(e){const t="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿";let n=this.opt.accuracy,s=typeof n=="string"?n:n.value,r=typeof n=="string"?[]:n.limiters,i="";switch(r.forEach(o=>{i+=`|${this.escapeStr(o)}`}),s){case"partially":default:return`()(${e})`;case"complementary":return i="\\s"+(i||this.escapeStr(t)),`()([^${i}]*${e}[^${i}]*)`;case"exactly":return`(^|\\s${i})(${e})(?=$|\\s${i})`}}getSeparatedKeywords(e){let t=[];return e.forEach(n=>{this.opt.separateWordSearch?n.split(" ").forEach(s=>{s.trim()&&t.indexOf(s)===-1&&t.push(s)}):n.trim()&&t.indexOf(n)===-1&&t.push(n)}),{keywords:t.sort((n,s)=>s.length-n.length),length:t.length}}isNumeric(e){return Number(parseFloat(e))==e}checkRanges(e){if(!Array.isArray(e)||Object.prototype.toString.call(e[0])!=="[object Object]")return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];const t=[];let n=0;return e.sort((s,r)=>s.start-r.start).forEach(s=>{let{start:r,end:i,valid:o}=this.callNoMatchOnInvalidRanges(s,n);o&&(s.start=r,s.length=i-r,t.push(s),n=i)}),t}callNoMatchOnInvalidRanges(e,t){let n,s,r=!1;return e&&typeof e.start<"u"?(n=parseInt(e.start,10),s=n+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&s-t>0&&s-n>0?r=!0:(this.log(`Ignoring invalid or overlapping range: ${JSON.stringify(e)}`),this.opt.noMatch(e))):(this.log(`Ignoring invalid range: ${JSON.stringify(e)}`),this.opt.noMatch(e)),{start:n,end:s,valid:r}}checkWhitespaceRanges(e,t,n){let s,r=!0,i=n.length,o=t-i,l=parseInt(e.start,10)-o;return l=l>i?i:l,s=l+parseInt(e.length,10),s>i&&(s=i,this.log(`End range automatically set to the max value of ${i}`)),l<0||s-l<0||l>i||s>i?(r=!1,this.log(`Invalid range: ${JSON.stringify(e)}`),this.opt.noMatch(e)):n.substring(l,s).replace(/\s+/g,"")===""&&(r=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:l,end:s,valid:r}}getTextNodes(e){let t="",n=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,s=>{n.push({start:t.length,end:(t+=s.textContent).length,node:s})},s=>this.matchesExclude(s.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT,()=>{e({value:t,nodes:n})})}matchesExclude(e){return ce.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}wrapRangeInTextNode(e,t,n){const s=this.opt.element?this.opt.element:"mark",r=e.splitText(t),i=r.splitText(n-t);let o=document.createElement(s);return o.setAttribute("data-markjs","true"),this.opt.className&&o.setAttribute("class",this.opt.className),o.textContent=r.textContent,r.parentNode.replaceChild(o,r),i}wrapRangeInMappedTextNode(e,t,n,s,r){e.nodes.every((i,o)=>{const l=e.nodes[o+1];if(typeof l>"u"||l.start>t){if(!s(i.node))return!1;const c=t-i.start,f=(n>i.end?i.end:n)-i.start,v=e.value.substr(0,i.start),h=e.value.substr(f+i.start);if(i.node=this.wrapRangeInTextNode(i.node,c,f),e.value=v+h,e.nodes.forEach((b,y)=>{y>=o&&(e.nodes[y].start>0&&y!==o&&(e.nodes[y].start-=f),e.nodes[y].end-=f)}),n-=f,r(i.node.previousSibling,i.start),n>i.end)t=i.end;else return!1}return!0})}wrapMatches(e,t,n,s,r){const i=t===0?0:t+1;this.getTextNodes(o=>{o.nodes.forEach(l=>{l=l.node;let c;for(;(c=e.exec(l.textContent))!==null&&c[i]!=="";){if(!n(c[i],l))continue;let f=c.index;if(i!==0)for(let v=1;v{let l;for(;(l=e.exec(o.value))!==null&&l[i]!=="";){let c=l.index;if(i!==0)for(let v=1;vn(l[i],v),(v,h)=>{e.lastIndex=h,s(v)})}r()})}wrapRangeFromIndex(e,t,n,s){this.getTextNodes(r=>{const i=r.value.length;e.forEach((o,l)=>{let{start:c,end:f,valid:v}=this.checkWhitespaceRanges(o,i,r.value);v&&this.wrapRangeInMappedTextNode(r,c,f,h=>t(h,o,r.value.substring(c,f),l),h=>{n(h,o)})}),s()})}unwrapMatches(e){const t=e.parentNode;let n=document.createDocumentFragment();for(;e.firstChild;)n.appendChild(e.removeChild(e.firstChild));t.replaceChild(n,e),this.ie?this.normalizeTextNode(t):t.normalize()}normalizeTextNode(e){if(e){if(e.nodeType===3)for(;e.nextSibling&&e.nextSibling.nodeType===3;)e.nodeValue+=e.nextSibling.nodeValue,e.parentNode.removeChild(e.nextSibling);else this.normalizeTextNode(e.firstChild);this.normalizeTextNode(e.nextSibling)}}markRegExp(e,t){this.opt=t,this.log(`Searching with expression "${e}"`);let n=0,s="wrapMatches";const r=i=>{n++,this.opt.each(i)};this.opt.acrossElements&&(s="wrapMatchesAcrossElements"),this[s](e,this.opt.ignoreGroups,(i,o)=>this.opt.filter(o,i,n),r,()=>{n===0&&this.opt.noMatch(e),this.opt.done(n)})}mark(e,t){this.opt=t;let n=0,s="wrapMatches";const{keywords:r,length:i}=this.getSeparatedKeywords(typeof e=="string"?[e]:e),o=this.opt.caseSensitive?"":"i",l=c=>{let f=new RegExp(this.createRegExp(c),`gm${o}`),v=0;this.log(`Searching with expression "${f}"`),this[s](f,1,(h,b)=>this.opt.filter(b,c,n,v),h=>{v++,n++,this.opt.each(h)},()=>{v===0&&this.opt.noMatch(c),r[i-1]===c?this.opt.done(n):l(r[r.indexOf(c)+1])})};this.opt.acrossElements&&(s="wrapMatchesAcrossElements"),i===0?this.opt.done(n):l(r[0])}markRanges(e,t){this.opt=t;let n=0,s=this.checkRanges(e);s&&s.length?(this.log("Starting to mark with the following ranges: "+JSON.stringify(s)),this.wrapRangeFromIndex(s,(r,i,o,l)=>this.opt.filter(r,i,o,l),(r,i)=>{n++,this.opt.each(r,i)},()=>{this.opt.done(n)})):this.opt.done(n)}unmark(e){this.opt=e;let t=this.opt.element?this.opt.element:"*";t+="[data-markjs]",this.opt.className&&(t+=`.${this.opt.className}`),this.log(`Removal selector "${t}"`),this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT,n=>{this.unwrapMatches(n)},n=>{const s=ce.matches(n,t),r=this.matchesExclude(n);return!s||r?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},this.opt.done)}};function Vn(a){const e=new zn(a);return this.mark=(t,n)=>(e.mark(t,n),this),this.markRegExp=(t,n)=>(e.markRegExp(t,n),this),this.markRanges=(t,n)=>(e.markRanges(t,n),this),this.unmark=t=>(e.unmark(t),this),this}const $n="ENTRIES",xt="KEYS",_t="VALUES",D="";class Le{constructor(e,t){const n=e._tree,s=Array.from(n.keys());this.set=e,this._type=t,this._path=s.length>0?[{node:n,keys:s}]:[]}next(){const e=this.dive();return this.backtrack(),e}dive(){if(this._path.length===0)return{done:!0,value:void 0};const{node:e,keys:t}=oe(this._path);if(oe(t)===D)return{done:!1,value:this.result()};const n=e.get(oe(t));return this._path.push({node:n,keys:Array.from(n.keys())}),this.dive()}backtrack(){if(this._path.length===0)return;const e=oe(this._path).keys;e.pop(),!(e.length>0)&&(this._path.pop(),this.backtrack())}key(){return this.set._prefix+this._path.map(({keys:e})=>oe(e)).filter(e=>e!==D).join("")}value(){return oe(this._path).node.get(D)}result(){switch(this._type){case _t:return this.value();case xt:return this.key();default:return[this.key(),this.value()]}}[Symbol.iterator](){return this}}const oe=a=>a[a.length-1],jn=(a,e,t)=>{const n=new Map;if(e===void 0)return n;const s=e.length+1,r=s+t,i=new Uint8Array(r*s).fill(t+1);for(let o=0;o{const l=r*i;e:for(const c of a.keys())if(c===D){const f=s[l-1];f<=t&&n.set(o,[a.get(c),f])}else{let f=r;for(let v=0;vt)continue e}St(a.get(c),e,t,n,s,f,i,o+c)}};class X{constructor(e=new Map,t=""){this._size=void 0,this._tree=e,this._prefix=t}atPrefix(e){if(!e.startsWith(this._prefix))throw new Error("Mismatched prefix");const[t,n]=Re(this._tree,e.slice(this._prefix.length));if(t===void 0){const[s,r]=qe(n);for(const i of s.keys())if(i!==D&&i.startsWith(r)){const o=new Map;return o.set(i.slice(r.length),s.get(i)),new X(o,e)}}return new X(t,e)}clear(){this._size=void 0,this._tree.clear()}delete(e){return this._size=void 0,Bn(this._tree,e)}entries(){return new Le(this,$n)}forEach(e){for(const[t,n]of this)e(t,n,this)}fuzzyGet(e,t){return jn(this._tree,e,t)}get(e){const t=We(this._tree,e);return t!==void 0?t.get(D):void 0}has(e){const t=We(this._tree,e);return t!==void 0&&t.has(D)}keys(){return new Le(this,xt)}set(e,t){if(typeof e!="string")throw new Error("key must be a string");return this._size=void 0,De(this._tree,e).set(D,t),this}get size(){if(this._size)return this._size;this._size=0;const e=this.entries();for(;!e.next().done;)this._size+=1;return this._size}update(e,t){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;const n=De(this._tree,e);return n.set(D,t(n.get(D))),this}fetch(e,t){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;const n=De(this._tree,e);let s=n.get(D);return s===void 0&&n.set(D,s=t()),s}values(){return new Le(this,_t)}[Symbol.iterator](){return this.entries()}static from(e){const t=new X;for(const[n,s]of e)t.set(n,s);return t}static fromObject(e){return X.from(Object.entries(e))}}const Re=(a,e,t=[])=>{if(e.length===0||a==null)return[a,t];for(const n of a.keys())if(n!==D&&e.startsWith(n))return t.push([a,n]),Re(a.get(n),e.slice(n.length),t);return t.push([a,e]),Re(void 0,"",t)},We=(a,e)=>{if(e.length===0||a==null)return a;for(const t of a.keys())if(t!==D&&e.startsWith(t))return We(a.get(t),e.slice(t.length))},De=(a,e)=>{const t=e.length;e:for(let n=0;a&&n{const[t,n]=Re(a,e);if(t!==void 0){if(t.delete(D),t.size===0)Et(n);else if(t.size===1){const[s,r]=t.entries().next().value;Tt(n,s,r)}}},Et=a=>{if(a.length===0)return;const[e,t]=qe(a);if(e.delete(t),e.size===0)Et(a.slice(0,-1));else if(e.size===1){const[n,s]=e.entries().next().value;n!==D&&Tt(a.slice(0,-1),n,s)}},Tt=(a,e,t)=>{if(a.length===0)return;const[n,s]=qe(a);n.set(s+e,t),n.delete(s)},qe=a=>a[a.length-1],Ue="or",It="and",Wn="and_not";class ue{constructor(e){if((e==null?void 0:e.fields)==null)throw new Error('MiniSearch: option "fields" must be provided');const t=e.autoVacuum==null||e.autoVacuum===!0?Ve:e.autoVacuum;this._options={...ze,...e,autoVacuum:t,searchOptions:{...dt,...e.searchOptions||{}},autoSuggestOptions:{...Gn,...e.autoSuggestOptions||{}}},this._index=new X,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldIds={},this._fieldLength=new Map,this._avgFieldLength=[],this._nextId=0,this._storedFields=new Map,this._dirtCount=0,this._currentVacuum=null,this._enqueuedVacuum=null,this._enqueuedVacuumConditions=Je,this.addFields(this._options.fields)}add(e){const{extractField:t,tokenize:n,processTerm:s,fields:r,idField:i}=this._options,o=t(e,i);if(o==null)throw new Error(`MiniSearch: document does not have ID field "${i}"`);if(this._idToShortId.has(o))throw new Error(`MiniSearch: duplicate ID ${o}`);const l=this.addDocumentId(o);this.saveStoredFields(l,e);for(const c of r){const f=t(e,c);if(f==null)continue;const v=n(f.toString(),c),h=this._fieldIds[c],b=new Set(v).size;this.addFieldLength(l,h,this._documentCount-1,b);for(const y of v){const x=s(y,c);if(Array.isArray(x))for(const w of x)this.addTerm(h,l,w);else x&&this.addTerm(h,l,x)}}}addAll(e){for(const t of e)this.add(t)}addAllAsync(e,t={}){const{chunkSize:n=10}=t,s={chunk:[],promise:Promise.resolve()},{chunk:r,promise:i}=e.reduce(({chunk:o,promise:l},c,f)=>(o.push(c),(f+1)%n===0?{chunk:[],promise:l.then(()=>new Promise(v=>setTimeout(v,0))).then(()=>this.addAll(o))}:{chunk:o,promise:l}),s);return i.then(()=>this.addAll(r))}remove(e){const{tokenize:t,processTerm:n,extractField:s,fields:r,idField:i}=this._options,o=s(e,i);if(o==null)throw new Error(`MiniSearch: document does not have ID field "${i}"`);const l=this._idToShortId.get(o);if(l==null)throw new Error(`MiniSearch: cannot remove document with ID ${o}: it is not in the index`);for(const c of r){const f=s(e,c);if(f==null)continue;const v=t(f.toString(),c),h=this._fieldIds[c],b=new Set(v).size;this.removeFieldLength(l,h,this._documentCount,b);for(const y of v){const x=n(y,c);if(Array.isArray(x))for(const w of x)this.removeTerm(h,l,w);else x&&this.removeTerm(h,l,x)}}this._storedFields.delete(l),this._documentIds.delete(l),this._idToShortId.delete(o),this._fieldLength.delete(l),this._documentCount-=1}removeAll(e){if(e)for(const t of e)this.remove(t);else{if(arguments.length>0)throw new Error("Expected documents to be present. Omit the argument to remove all documents.");this._index=new X,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldLength=new Map,this._avgFieldLength=[],this._storedFields=new Map,this._nextId=0}}discard(e){const t=this._idToShortId.get(e);if(t==null)throw new Error(`MiniSearch: cannot discard document with ID ${e}: it is not in the index`);this._idToShortId.delete(e),this._documentIds.delete(t),this._storedFields.delete(t),(this._fieldLength.get(t)||[]).forEach((n,s)=>{this.removeFieldLength(t,s,this._documentCount,n)}),this._fieldLength.delete(t),this._documentCount-=1,this._dirtCount+=1,this.maybeAutoVacuum()}maybeAutoVacuum(){if(this._options.autoVacuum===!1)return;const{minDirtFactor:e,minDirtCount:t,batchSize:n,batchWait:s}=this._options.autoVacuum;this.conditionalVacuum({batchSize:n,batchWait:s},{minDirtCount:t,minDirtFactor:e})}discardAll(e){const t=this._options.autoVacuum;try{this._options.autoVacuum=!1;for(const n of e)this.discard(n)}finally{this._options.autoVacuum=t}this.maybeAutoVacuum()}replace(e){const{idField:t,extractField:n}=this._options,s=n(e,t);this.discard(s),this.add(e)}vacuum(e={}){return this.conditionalVacuum(e)}conditionalVacuum(e,t){return this._currentVacuum?(this._enqueuedVacuumConditions=this._enqueuedVacuumConditions&&t,this._enqueuedVacuum!=null?this._enqueuedVacuum:(this._enqueuedVacuum=this._currentVacuum.then(()=>{const n=this._enqueuedVacuumConditions;return this._enqueuedVacuumConditions=Je,this.performVacuuming(e,n)}),this._enqueuedVacuum)):this.vacuumConditionsMet(t)===!1?Promise.resolve():(this._currentVacuum=this.performVacuuming(e),this._currentVacuum)}async performVacuuming(e,t){const n=this._dirtCount;if(this.vacuumConditionsMet(t)){const s=e.batchSize||Ke.batchSize,r=e.batchWait||Ke.batchWait;let i=1;for(const[o,l]of this._index){for(const[c,f]of l)for(const[v]of f)this._documentIds.has(v)||(f.size<=1?l.delete(c):f.delete(v));this._index.get(o).size===0&&this._index.delete(o),i%s===0&&await new Promise(c=>setTimeout(c,r)),i+=1}this._dirtCount-=n}await null,this._currentVacuum=this._enqueuedVacuum,this._enqueuedVacuum=null}vacuumConditionsMet(e){if(e==null)return!0;let{minDirtCount:t,minDirtFactor:n}=e;return t=t||Ve.minDirtCount,n=n||Ve.minDirtFactor,this.dirtCount>=t&&this.dirtFactor>=n}get isVacuuming(){return this._currentVacuum!=null}get dirtCount(){return this._dirtCount}get dirtFactor(){return this._dirtCount/(1+this._documentCount+this._dirtCount)}has(e){return this._idToShortId.has(e)}getStoredFields(e){const t=this._idToShortId.get(e);if(t!=null)return this._storedFields.get(t)}search(e,t={}){const{searchOptions:n}=this._options,s={...n,...t},r=this.executeQuery(e,t),i=[];for(const[o,{score:l,terms:c,match:f}]of r){const v=c.length||1,h={id:this._documentIds.get(o),score:l*v,terms:Object.keys(f),queryTerms:c,match:f};Object.assign(h,this._storedFields.get(o)),(s.filter==null||s.filter(h))&&i.push(h)}return e===ue.wildcard&&s.boostDocument==null||i.sort(ht),i}autoSuggest(e,t={}){t={...this._options.autoSuggestOptions,...t};const n=new Map;for(const{score:r,terms:i}of this.search(e,t)){const o=i.join(" "),l=n.get(o);l!=null?(l.score+=r,l.count+=1):n.set(o,{score:r,terms:i,count:1})}const s=[];for(const[r,{score:i,terms:o,count:l}]of n)s.push({suggestion:r,terms:o,score:i/l});return s.sort(ht),s}get documentCount(){return this._documentCount}get termCount(){return this._index.size}static loadJSON(e,t){if(t==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJS(JSON.parse(e),t)}static async loadJSONAsync(e,t){if(t==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJSAsync(JSON.parse(e),t)}static getDefault(e){if(ze.hasOwnProperty(e))return Pe(ze,e);throw new Error(`MiniSearch: unknown option "${e}"`)}static loadJS(e,t){const{index:n,documentIds:s,fieldLength:r,storedFields:i,serializationVersion:o}=e,l=this.instantiateMiniSearch(e,t);l._documentIds=Te(s),l._fieldLength=Te(r),l._storedFields=Te(i);for(const[c,f]of l._documentIds)l._idToShortId.set(f,c);for(const[c,f]of n){const v=new Map;for(const h of Object.keys(f)){let b=f[h];o===1&&(b=b.ds),v.set(parseInt(h,10),Te(b))}l._index.set(c,v)}return l}static async loadJSAsync(e,t){const{index:n,documentIds:s,fieldLength:r,storedFields:i,serializationVersion:o}=e,l=this.instantiateMiniSearch(e,t);l._documentIds=await Ie(s),l._fieldLength=await Ie(r),l._storedFields=await Ie(i);for(const[f,v]of l._documentIds)l._idToShortId.set(v,f);let c=0;for(const[f,v]of n){const h=new Map;for(const b of Object.keys(v)){let y=v[b];o===1&&(y=y.ds),h.set(parseInt(b,10),await Ie(y))}++c%1e3===0&&await kt(0),l._index.set(f,h)}return l}static instantiateMiniSearch(e,t){const{documentCount:n,nextId:s,fieldIds:r,averageFieldLength:i,dirtCount:o,serializationVersion:l}=e;if(l!==1&&l!==2)throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");const c=new ue(t);return c._documentCount=n,c._nextId=s,c._idToShortId=new Map,c._fieldIds=r,c._avgFieldLength=i,c._dirtCount=o||0,c._index=new X,c}executeQuery(e,t={}){if(e===ue.wildcard)return this.executeWildcardQuery(t);if(typeof e!="string"){const h={...t,...e,queries:void 0},b=e.queries.map(y=>this.executeQuery(y,h));return this.combineResults(b,h.combineWith)}const{tokenize:n,processTerm:s,searchOptions:r}=this._options,i={tokenize:n,processTerm:s,...r,...t},{tokenize:o,processTerm:l}=i,v=o(e).flatMap(h=>l(h)).filter(h=>!!h).map(Un(i)).map(h=>this.executeQuerySpec(h,i));return this.combineResults(v,i.combineWith)}executeQuerySpec(e,t){const n={...this._options.searchOptions,...t},s=(n.fields||this._options.fields).reduce((x,w)=>({...x,[w]:Pe(n.boost,w)||1}),{}),{boostDocument:r,weights:i,maxFuzzy:o,bm25:l}=n,{fuzzy:c,prefix:f}={...dt.weights,...i},v=this._index.get(e.term),h=this.termResults(e.term,e.term,1,e.termBoost,v,s,r,l);let b,y;if(e.prefix&&(b=this._index.atPrefix(e.term)),e.fuzzy){const x=e.fuzzy===!0?.2:e.fuzzy,w=x<1?Math.min(o,Math.round(e.term.length*x)):x;w&&(y=this._index.fuzzyGet(e.term,w))}if(b)for(const[x,w]of b){const C=x.length-e.term.length;if(!C)continue;y==null||y.delete(x);const A=f*x.length/(x.length+.3*C);this.termResults(e.term,x,A,e.termBoost,w,s,r,l,h)}if(y)for(const x of y.keys()){const[w,C]=y.get(x);if(!C)continue;const A=c*x.length/(x.length+C);this.termResults(e.term,x,A,e.termBoost,w,s,r,l,h)}return h}executeWildcardQuery(e){const t=new Map,n={...this._options.searchOptions,...e};for(const[s,r]of this._documentIds){const i=n.boostDocument?n.boostDocument(r,"",this._storedFields.get(s)):1;t.set(s,{score:i,terms:[],match:{}})}return t}combineResults(e,t=Ue){if(e.length===0)return new Map;const n=t.toLowerCase(),s=Kn[n];if(!s)throw new Error(`Invalid combination operator: ${t}`);return e.reduce(s)||new Map}toJSON(){const e=[];for(const[t,n]of this._index){const s={};for(const[r,i]of n)s[r]=Object.fromEntries(i);e.push([t,s])}return{documentCount:this._documentCount,nextId:this._nextId,documentIds:Object.fromEntries(this._documentIds),fieldIds:this._fieldIds,fieldLength:Object.fromEntries(this._fieldLength),averageFieldLength:this._avgFieldLength,storedFields:Object.fromEntries(this._storedFields),dirtCount:this._dirtCount,index:e,serializationVersion:2}}termResults(e,t,n,s,r,i,o,l,c=new Map){if(r==null)return c;for(const f of Object.keys(i)){const v=i[f],h=this._fieldIds[f],b=r.get(h);if(b==null)continue;let y=b.size;const x=this._avgFieldLength[h];for(const w of b.keys()){if(!this._documentIds.has(w)){this.removeTerm(h,w,t),y-=1;continue}const C=o?o(this._documentIds.get(w),t,this._storedFields.get(w)):1;if(!C)continue;const A=b.get(w),J=this._fieldLength.get(w)[h],Q=qn(A,y,this._documentCount,J,x,l),W=n*s*v*C*Q,$=c.get(w);if($){$.score+=W,Hn($.terms,e);const j=Pe($.match,t);j?j.push(f):$.match[t]=[f]}else c.set(w,{score:W,terms:[e],match:{[t]:[f]}})}}return c}addTerm(e,t,n){const s=this._index.fetch(n,pt);let r=s.get(e);if(r==null)r=new Map,r.set(t,1),s.set(e,r);else{const i=r.get(t);r.set(t,(i||0)+1)}}removeTerm(e,t,n){if(!this._index.has(n)){this.warnDocumentChanged(t,e,n);return}const s=this._index.fetch(n,pt),r=s.get(e);r==null||r.get(t)==null?this.warnDocumentChanged(t,e,n):r.get(t)<=1?r.size<=1?s.delete(e):r.delete(t):r.set(t,r.get(t)-1),this._index.get(n).size===0&&this._index.delete(n)}warnDocumentChanged(e,t,n){for(const s of Object.keys(this._fieldIds))if(this._fieldIds[s]===t){this._options.logger("warn",`MiniSearch: document with ID ${this._documentIds.get(e)} has changed before removal: term "${n}" was not present in field "${s}". Removing a document after it has changed can corrupt the index!`,"version_conflict");return}}addDocumentId(e){const t=this._nextId;return this._idToShortId.set(e,t),this._documentIds.set(t,e),this._documentCount+=1,this._nextId+=1,t}addFields(e){for(let t=0;tObject.prototype.hasOwnProperty.call(a,e)?a[e]:void 0,Kn={[Ue]:(a,e)=>{for(const t of e.keys()){const n=a.get(t);if(n==null)a.set(t,e.get(t));else{const{score:s,terms:r,match:i}=e.get(t);n.score=n.score+s,n.match=Object.assign(n.match,i),ft(n.terms,r)}}return a},[It]:(a,e)=>{const t=new Map;for(const n of e.keys()){const s=a.get(n);if(s==null)continue;const{score:r,terms:i,match:o}=e.get(n);ft(s.terms,i),t.set(n,{score:s.score+r,terms:s.terms,match:Object.assign(s.match,o)})}return t},[Wn]:(a,e)=>{for(const t of e.keys())a.delete(t);return a}},Jn={k:1.2,b:.7,d:.5},qn=(a,e,t,n,s,r)=>{const{k:i,b:o,d:l}=r;return Math.log(1+(t-e+.5)/(e+.5))*(l+a*(i+1)/(a+i*(1-o+o*n/s)))},Un=a=>(e,t,n)=>{const s=typeof a.fuzzy=="function"?a.fuzzy(e,t,n):a.fuzzy||!1,r=typeof a.prefix=="function"?a.prefix(e,t,n):a.prefix===!0,i=typeof a.boostTerm=="function"?a.boostTerm(e,t,n):1;return{term:e,fuzzy:s,prefix:r,termBoost:i}},ze={idField:"id",extractField:(a,e)=>a[e],tokenize:a=>a.split(Qn),processTerm:a=>a.toLowerCase(),fields:void 0,searchOptions:void 0,storeFields:[],logger:(a,e)=>{typeof(console==null?void 0:console[a])=="function"&&console[a](e)},autoVacuum:!0},dt={combineWith:Ue,prefix:!1,fuzzy:!1,maxFuzzy:6,boost:{},weights:{fuzzy:.45,prefix:.375},bm25:Jn},Gn={combineWith:It,prefix:(a,e,t)=>e===t.length-1},Ke={batchSize:1e3,batchWait:10},Je={minDirtFactor:.1,minDirtCount:20},Ve={...Ke,...Je},Hn=(a,e)=>{a.includes(e)||a.push(e)},ft=(a,e)=>{for(const t of e)a.includes(t)||a.push(t)},ht=({score:a},{score:e})=>e-a,pt=()=>new Map,Te=a=>{const e=new Map;for(const t of Object.keys(a))e.set(parseInt(t,10),a[t]);return e},Ie=async a=>{const e=new Map;let t=0;for(const n of Object.keys(a))e.set(parseInt(n,10),a[n]),++t%1e3===0&&await kt(0);return e},kt=a=>new Promise(e=>setTimeout(e,a)),Qn=/[\n\r\p{Z}\p{P}]+/u;class Yn{constructor(e=10){Ce(this,"max");Ce(this,"cache");this.max=e,this.cache=new Map}get(e){let t=this.cache.get(e);return t!==void 0&&(this.cache.delete(e),this.cache.set(e,t)),t}set(e,t){this.cache.has(e)?this.cache.delete(e):this.cache.size===this.max&&this.cache.delete(this.first()),this.cache.set(e,t)}first(){return this.cache.keys().next().value}clear(){this.cache.clear()}}const Zn=["aria-owns"],Xn={class:"shell"},es=["title"],ts={class:"search-actions before"},ns=["title"],ss=["aria-activedescendant","aria-controls","placeholder"],is={class:"search-actions"},rs=["title"],as=["disabled","title"],os=["id","role","aria-labelledby"],ls=["id","aria-selected"],cs=["href","aria-label","onMouseenter","onFocusin","data-index"],us={class:"titles"},ds=["innerHTML"],fs={class:"title main"},hs=["innerHTML"],ps={key:0,class:"excerpt-wrapper"},ms={key:0,class:"excerpt",inert:""},vs=["innerHTML"],gs={key:0,class:"no-results"},bs={class:"search-keyboard-shortcuts"},ys=["aria-label"],ws=["aria-label"],xs=["aria-label"],_s=["aria-label"],Ss=Lt({__name:"VPLocalSearchBox",emits:["close"],setup(a,{emit:e}){var S,R;const t=e,n=le(),s=le(),r=le(rn),i=nn(),{activate:o}=Pn(n,{immediate:!0,allowOutsideClick:!0,clickOutsideDeactivates:!0,escapeDeactivates:!0}),{localeIndex:l,theme:c}=i,f=tt(async()=>{var m,p,I,O,P,z,V,k,K;return rt(ue.loadJSON((I=await((p=(m=r.value)[l.value])==null?void 0:p.call(m)))==null?void 0:I.default,{fields:["title","titles","text"],storeFields:["title","titles"],searchOptions:{fuzzy:.2,prefix:!0,boost:{title:4,text:2,titles:1},...((O=c.value.search)==null?void 0:O.provider)==="local"&&((z=(P=c.value.search.options)==null?void 0:P.miniSearch)==null?void 0:z.searchOptions)},...((V=c.value.search)==null?void 0:V.provider)==="local"&&((K=(k=c.value.search.options)==null?void 0:k.miniSearch)==null?void 0:K.options)}))}),h=ge(()=>{var m,p;return((m=c.value.search)==null?void 0:m.provider)==="local"&&((p=c.value.search.options)==null?void 0:p.disableQueryPersistence)===!0}).value?fe(""):Dt("vitepress:local-search-filter",""),b=Pt("vitepress:local-search-detailed-list",((S=c.value.search)==null?void 0:S.provider)==="local"&&((R=c.value.search.options)==null?void 0:R.detailedView)===!0),y=ge(()=>{var m,p,I;return((m=c.value.search)==null?void 0:m.provider)==="local"&&(((p=c.value.search.options)==null?void 0:p.disableDetailedView)===!0||((I=c.value.search.options)==null?void 0:I.detailedView)===!1)}),x=ge(()=>{var p,I,O,P,z,V,k;const m=((p=c.value.search)==null?void 0:p.options)??c.value.algolia;return((z=(P=(O=(I=m==null?void 0:m.locales)==null?void 0:I[l.value])==null?void 0:O.translations)==null?void 0:P.button)==null?void 0:z.buttonText)||((k=(V=m==null?void 0:m.translations)==null?void 0:V.button)==null?void 0:k.buttonText)||"Search"});zt(()=>{y.value&&(b.value=!1)});const w=le([]),C=fe(!1);$e(h,()=>{C.value=!1});const A=tt(async()=>{if(s.value)return rt(new Vn(s.value))},null),J=new Yn(16);Vt(()=>[f.value,h.value,b.value],async([m,p,I],O,P)=>{var ee,we,Ge,He;(O==null?void 0:O[0])!==m&&J.clear();let z=!1;if(P(()=>{z=!0}),!m)return;w.value=m.search(p).slice(0,16),C.value=!0;const V=I?await Promise.all(w.value.map(B=>Q(B.id))):[];if(z)return;for(const{id:B,mod:te}of V){const ne=B.slice(0,B.indexOf("#"));let Y=J.get(ne);if(Y)continue;Y=new Map,J.set(ne,Y);const G=te.default??te;if(G!=null&&G.render||G!=null&&G.setup){const se=Yt(G);se.config.warnHandler=()=>{},se.provide(Zt,i),Object.defineProperties(se.config.globalProperties,{$frontmatter:{get(){return i.frontmatter.value}},$params:{get(){return i.page.value.params}}});const Qe=document.createElement("div");se.mount(Qe),Qe.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(de=>{var Xe;const xe=(Xe=de.querySelector("a"))==null?void 0:Xe.getAttribute("href"),Ye=(xe==null?void 0:xe.startsWith("#"))&&xe.slice(1);if(!Ye)return;let Ze="";for(;(de=de.nextElementSibling)&&!/^h[1-6]$/i.test(de.tagName);)Ze+=de.outerHTML;Y.set(Ye,Ze)}),se.unmount()}if(z)return}const k=new Set;if(w.value=w.value.map(B=>{const[te,ne]=B.id.split("#"),Y=J.get(te),G=(Y==null?void 0:Y.get(ne))??"";for(const se in B.match)k.add(se);return{...B,text:G}}),await he(),z)return;await new Promise(B=>{var te;(te=A.value)==null||te.unmark({done:()=>{var ne;(ne=A.value)==null||ne.markRegExp(T(k),{done:B})}})});const K=((ee=n.value)==null?void 0:ee.querySelectorAll(".result .excerpt"))??[];for(const B of K)(we=B.querySelector('mark[data-markjs="true"]'))==null||we.scrollIntoView({block:"center"});(He=(Ge=s.value)==null?void 0:Ge.firstElementChild)==null||He.scrollIntoView({block:"start"})},{debounce:200,immediate:!0});async function Q(m){const p=Xt(m.slice(0,m.indexOf("#")));try{if(!p)throw new Error(`Cannot find file for id: ${m}`);return{id:m,mod:await import(p)}}catch(I){return console.error(I),{id:m,mod:{}}}}const W=fe(),$=ge(()=>{var m;return((m=h.value)==null?void 0:m.length)<=0});function j(m=!0){var p,I;(p=W.value)==null||p.focus(),m&&((I=W.value)==null||I.select())}Ae(()=>{j()});function ye(m){m.pointerType==="mouse"&&j()}const M=fe(-1),q=fe(!0);$e(w,m=>{M.value=m.length?0:-1,U()});function U(){he(()=>{const m=document.querySelector(".result.selected");m==null||m.scrollIntoView({block:"nearest"})})}_e("ArrowUp",m=>{m.preventDefault(),M.value--,M.value<0&&(M.value=w.value.length-1),q.value=!0,U()}),_e("ArrowDown",m=>{m.preventDefault(),M.value++,M.value>=w.value.length&&(M.value=0),q.value=!0,U()});const N=$t();_e("Enter",m=>{if(m.isComposing||m.target instanceof HTMLButtonElement&&m.target.type!=="submit")return;const p=w.value[M.value];if(m.target instanceof HTMLInputElement&&!p){m.preventDefault();return}p&&(N.go(p.id),t("close"))}),_e("Escape",()=>{t("close")});const d=sn({modal:{displayDetails:"Display detailed list",resetButtonTitle:"Reset search",backButtonTitle:"Close search",noResultsText:"No results for",footer:{selectText:"to select",selectKeyAriaLabel:"enter",navigateText:"to navigate",navigateUpKeyAriaLabel:"up arrow",navigateDownKeyAriaLabel:"down arrow",closeText:"to close",closeKeyAriaLabel:"escape"}}});Ae(()=>{window.history.pushState(null,"",null)}),jt("popstate",m=>{m.preventDefault(),t("close")});const g=Bt(Wt?document.body:null);Ae(()=>{he(()=>{g.value=!0,he().then(()=>o())})}),Kt(()=>{g.value=!1});function E(){h.value="",he().then(()=>j(!1))}function T(m){return new RegExp([...m].sort((p,I)=>I.length-p.length).map(p=>`(${en(p)})`).join("|"),"gi")}function F(m){var O;if(!q.value)return;const p=(O=m.target)==null?void 0:O.closest(".result"),I=Number.parseInt(p==null?void 0:p.dataset.index);I>=0&&I!==M.value&&(M.value=I),q.value=!1}return(m,p)=>{var I,O,P,z,V;return H(),Jt(Qt,{to:"body"},[_("div",{ref_key:"el",ref:n,role:"button","aria-owns":(I=w.value)!=null&&I.length?"localsearch-list":void 0,"aria-expanded":"true","aria-haspopup":"listbox","aria-labelledby":"localsearch-label",class:"VPLocalSearchBox"},[_("div",{class:"backdrop",onClick:p[0]||(p[0]=k=>m.$emit("close"))}),_("div",Xn,[_("form",{class:"search-bar",onPointerup:p[4]||(p[4]=k=>ye(k)),onSubmit:p[5]||(p[5]=qt(()=>{},["prevent"]))},[_("label",{title:x.value,id:"localsearch-label",for:"localsearch-input"},p[7]||(p[7]=[_("span",{"aria-hidden":"true",class:"vpi-search search-icon local-search-icon"},null,-1)]),8,es),_("div",ts,[_("button",{class:"back-button",title:L(d)("modal.backButtonTitle"),onClick:p[1]||(p[1]=k=>m.$emit("close"))},p[8]||(p[8]=[_("span",{class:"vpi-arrow-left local-search-icon"},null,-1)]),8,ns)]),Ut(_("input",{ref_key:"searchInput",ref:W,"onUpdate:modelValue":p[2]||(p[2]=k=>Ht(h)?h.value=k:null),"aria-activedescendant":M.value>-1?"localsearch-item-"+M.value:void 0,"aria-autocomplete":"both","aria-controls":(O=w.value)!=null&&O.length?"localsearch-list":void 0,"aria-labelledby":"localsearch-label",autocapitalize:"off",autocomplete:"off",autocorrect:"off",class:"search-input",id:"localsearch-input",enterkeyhint:"go",maxlength:"64",placeholder:x.value,spellcheck:"false",type:"search"},null,8,ss),[[Gt,L(h)]]),_("div",is,[y.value?Se("",!0):(H(),Z("button",{key:0,class:nt(["toggle-layout-button",{"detailed-list":L(b)}]),type:"button",title:L(d)("modal.displayDetails"),onClick:p[3]||(p[3]=k=>M.value>-1&&(b.value=!L(b)))},p[9]||(p[9]=[_("span",{class:"vpi-layout-list local-search-icon"},null,-1)]),10,rs)),_("button",{class:"clear-button",type:"reset",disabled:$.value,title:L(d)("modal.resetButtonTitle"),onClick:E},p[10]||(p[10]=[_("span",{class:"vpi-delete local-search-icon"},null,-1)]),8,as)])],32),_("ul",{ref_key:"resultsEl",ref:s,id:(P=w.value)!=null&&P.length?"localsearch-list":void 0,role:(z=w.value)!=null&&z.length?"listbox":void 0,"aria-labelledby":(V=w.value)!=null&&V.length?"localsearch-label":void 0,class:"results",onMousemove:F},[(H(!0),Z(it,null,st(w.value,(k,K)=>(H(),Z("li",{key:k.id,id:"localsearch-item-"+K,"aria-selected":M.value===K?"true":"false",role:"option"},[_("a",{href:k.id,class:nt(["result",{selected:M.value===K}]),"aria-label":[...k.titles,k.title].join(" > "),onMouseenter:ee=>!q.value&&(M.value=K),onFocusin:ee=>M.value=K,onClick:p[6]||(p[6]=ee=>m.$emit("close")),"data-index":K},[_("div",null,[_("div",us,[p[12]||(p[12]=_("span",{class:"title-icon"},"#",-1)),(H(!0),Z(it,null,st(k.titles,(ee,we)=>(H(),Z("span",{key:we,class:"title"},[_("span",{class:"text",innerHTML:ee},null,8,ds),p[11]||(p[11]=_("span",{class:"vpi-chevron-right local-search-icon"},null,-1))]))),128)),_("span",fs,[_("span",{class:"text",innerHTML:k.title},null,8,hs)])]),L(b)?(H(),Z("div",ps,[k.text?(H(),Z("div",ms,[_("div",{class:"vp-doc",innerHTML:k.text},null,8,vs)])):Se("",!0),p[13]||(p[13]=_("div",{class:"excerpt-gradient-bottom"},null,-1)),p[14]||(p[14]=_("div",{class:"excerpt-gradient-top"},null,-1))])):Se("",!0)])],42,cs)],8,ls))),128)),L(h)&&!w.value.length&&C.value?(H(),Z("li",gs,[pe(me(L(d)("modal.noResultsText"))+' "',1),_("strong",null,me(L(h)),1),p[15]||(p[15]=pe('" '))])):Se("",!0)],40,os),_("div",bs,[_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.navigateUpKeyAriaLabel")},p[16]||(p[16]=[_("span",{class:"vpi-arrow-up navigate-icon"},null,-1)]),8,ys),_("kbd",{"aria-label":L(d)("modal.footer.navigateDownKeyAriaLabel")},p[17]||(p[17]=[_("span",{class:"vpi-arrow-down navigate-icon"},null,-1)]),8,ws),pe(" "+me(L(d)("modal.footer.navigateText")),1)]),_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.selectKeyAriaLabel")},p[18]||(p[18]=[_("span",{class:"vpi-corner-down-left navigate-icon"},null,-1)]),8,xs),pe(" "+me(L(d)("modal.footer.selectText")),1)]),_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.closeKeyAriaLabel")},"esc",8,_s),pe(" "+me(L(d)("modal.footer.closeText")),1)])])])],8,Zn)])}}}),Fs=tn(Ss,[["__scopeId","data-v-ce626c7c"]]);export{Fs as default}; diff --git a/docs/.vitepress/dist/assets/chunks/framework.C9SxlbOG.js b/docs/.vitepress/dist/assets/chunks/framework.C9SxlbOG.js new file mode 100644 index 000000000..e22bef7bd --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/framework.C9SxlbOG.js @@ -0,0 +1,18 @@ +/** +* @vue/shared v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function $s(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const ee={},Mt=[],Be=()=>{},zo=()=>!1,nn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),js=e=>e.startsWith("onUpdate:"),he=Object.assign,Vs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Qo=Object.prototype.hasOwnProperty,Q=(e,t)=>Qo.call(e,t),K=Array.isArray,Ot=e=>Hn(e)==="[object Map]",ai=e=>Hn(e)==="[object Set]",q=e=>typeof e=="function",oe=e=>typeof e=="string",ze=e=>typeof e=="symbol",se=e=>e!==null&&typeof e=="object",fi=e=>(se(e)||q(e))&&q(e.then)&&q(e.catch),ui=Object.prototype.toString,Hn=e=>ui.call(e),Zo=e=>Hn(e).slice(8,-1),di=e=>Hn(e)==="[object Object]",ks=e=>oe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Pt=$s(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Dn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},el=/-(\w)/g,Ne=Dn(e=>e.replace(el,(t,n)=>n?n.toUpperCase():"")),tl=/\B([A-Z])/g,ot=Dn(e=>e.replace(tl,"-$1").toLowerCase()),$n=Dn(e=>e.charAt(0).toUpperCase()+e.slice(1)),Sn=Dn(e=>e?`on${$n(e)}`:""),st=(e,t)=>!Object.is(e,t),Tn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Ss=e=>{const t=parseFloat(e);return isNaN(t)?e:t},nl=e=>{const t=oe(e)?Number(e):NaN;return isNaN(t)?e:t};let dr;const jn=()=>dr||(dr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Us(e){if(K(e)){const t={};for(let n=0;n{if(n){const s=n.split(rl);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function Ws(e){let t="";if(oe(e))t=e;else if(K(e))for(let n=0;n!!(e&&e.__v_isRef===!0),al=e=>oe(e)?e:e==null?"":K(e)||se(e)&&(e.toString===ui||!q(e.toString))?gi(e)?al(e.value):JSON.stringify(e,mi,2):String(e),mi=(e,t)=>gi(t)?mi(e,t.value):Ot(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Zn(s,i)+" =>"]=r,n),{})}:ai(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Zn(n))}:ze(t)?Zn(t):se(t)&&!K(t)&&!di(t)?String(t):t,Zn=(e,t="")=>{var n;return ze(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let we;class fl{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=we,!t&&we&&(this.index=(we.scopes||(we.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0)return;if(Ut){let t=Ut;for(Ut=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;kt;){let t=kt;for(kt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function wi(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Si(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),qs(s),dl(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Ts(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Ti(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Ti(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Xt))return;e.globalVersion=Xt;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&e.deps&&!Ts(e)){e.flags&=-3;return}const n=ne,s=He;ne=e,He=!0;try{wi(e);const r=e.fn(e._value);(t.version===0||st(r,e._value))&&(e._value=r,t.version++)}catch(r){throw t.version++,r}finally{ne=n,He=s,Si(e),e.flags&=-3}}function qs(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)qs(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function dl(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let He=!0;const xi=[];function lt(){xi.push(He),He=!1}function ct(){const e=xi.pop();He=e===void 0?!0:e}function hr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ne;ne=void 0;try{t()}finally{ne=n}}}let Xt=0;class hl{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Vn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0}track(t){if(!ne||!He||ne===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ne)n=this.activeLink=new hl(ne,this),ne.deps?(n.prevDep=ne.depsTail,ne.depsTail.nextDep=n,ne.depsTail=n):ne.deps=ne.depsTail=n,Ei(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ne.depsTail,n.nextDep=void 0,ne.depsTail.nextDep=n,ne.depsTail=n,ne.deps===n&&(ne.deps=s)}return n}trigger(t){this.version++,Xt++,this.notify(t)}notify(t){Bs();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Ks()}}}function Ei(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Ei(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Mn=new WeakMap,gt=Symbol(""),xs=Symbol(""),Yt=Symbol("");function me(e,t,n){if(He&&ne){let s=Mn.get(e);s||Mn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new Vn),r.map=s,r.key=n),r.track()}}function Xe(e,t,n,s,r,i){const o=Mn.get(e);if(!o){Xt++;return}const l=c=>{c&&c.trigger()};if(Bs(),t==="clear")o.forEach(l);else{const c=K(e),f=c&&ks(n);if(c&&n==="length"){const a=Number(s);o.forEach((d,m)=>{(m==="length"||m===Yt||!ze(m)&&m>=a)&&l(d)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),f&&l(o.get(Yt)),t){case"add":c?f&&l(o.get("length")):(l(o.get(gt)),Ot(e)&&l(o.get(xs)));break;case"delete":c||(l(o.get(gt)),Ot(e)&&l(o.get(xs)));break;case"set":Ot(e)&&l(o.get(gt));break}}Ks()}function pl(e,t){const n=Mn.get(e);return n&&n.get(t)}function xt(e){const t=z(e);return t===e?t:(me(t,"iterate",Yt),Le(e)?t:t.map(ve))}function kn(e){return me(e=z(e),"iterate",Yt),e}const gl={__proto__:null,[Symbol.iterator](){return ts(this,Symbol.iterator,ve)},concat(...e){return xt(this).concat(...e.map(t=>K(t)?xt(t):t))},entries(){return ts(this,"entries",e=>(e[1]=ve(e[1]),e))},every(e,t){return Ke(this,"every",e,t,void 0,arguments)},filter(e,t){return Ke(this,"filter",e,t,n=>n.map(ve),arguments)},find(e,t){return Ke(this,"find",e,t,ve,arguments)},findIndex(e,t){return Ke(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ke(this,"findLast",e,t,ve,arguments)},findLastIndex(e,t){return Ke(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ke(this,"forEach",e,t,void 0,arguments)},includes(...e){return ns(this,"includes",e)},indexOf(...e){return ns(this,"indexOf",e)},join(e){return xt(this).join(e)},lastIndexOf(...e){return ns(this,"lastIndexOf",e)},map(e,t){return Ke(this,"map",e,t,void 0,arguments)},pop(){return $t(this,"pop")},push(...e){return $t(this,"push",e)},reduce(e,...t){return pr(this,"reduce",e,t)},reduceRight(e,...t){return pr(this,"reduceRight",e,t)},shift(){return $t(this,"shift")},some(e,t){return Ke(this,"some",e,t,void 0,arguments)},splice(...e){return $t(this,"splice",e)},toReversed(){return xt(this).toReversed()},toSorted(e){return xt(this).toSorted(e)},toSpliced(...e){return xt(this).toSpliced(...e)},unshift(...e){return $t(this,"unshift",e)},values(){return ts(this,"values",ve)}};function ts(e,t,n){const s=kn(e),r=s[t]();return s!==e&&!Le(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const ml=Array.prototype;function Ke(e,t,n,s,r,i){const o=kn(e),l=o!==e&&!Le(e),c=o[t];if(c!==ml[t]){const d=c.apply(e,i);return l?ve(d):d}let f=n;o!==e&&(l?f=function(d,m){return n.call(this,ve(d),m,e)}:n.length>2&&(f=function(d,m){return n.call(this,d,m,e)}));const a=c.call(o,f,s);return l&&r?r(a):a}function pr(e,t,n,s){const r=kn(e);let i=n;return r!==e&&(Le(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,ve(l),c,e)}),r[t](i,...s)}function ns(e,t,n){const s=z(e);me(s,"iterate",Yt);const r=s[t](...n);return(r===-1||r===!1)&&Ys(n[0])?(n[0]=z(n[0]),s[t](...n)):r}function $t(e,t,n=[]){lt(),Bs();const s=z(e)[t].apply(e,n);return Ks(),ct(),s}const vl=$s("__proto__,__v_isRef,__isVue"),Ci=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ze));function yl(e){ze(e)||(e=String(e));const t=z(this);return me(t,"has",e),t.hasOwnProperty(e)}class Ai{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Rl:Pi:i?Oi:Mi).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=K(t);if(!r){let c;if(o&&(c=gl[n]))return c;if(n==="hasOwnProperty")return yl}const l=Reflect.get(t,n,fe(t)?t:s);return(ze(n)?Ci.has(n):vl(n))||(r||me(t,"get",n),i)?l:fe(l)?o&&ks(n)?l:l.value:se(l)?r?Un(l):It(l):l}}class Ri extends Ai{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const c=St(i);if(!Le(s)&&!St(s)&&(i=z(i),s=z(s)),!K(t)&&fe(i)&&!fe(s))return c?!1:(i.value=s,!0)}const o=K(t)&&ks(n)?Number(n)e,un=e=>Reflect.getPrototypeOf(e);function Tl(e,t,n){return function(...s){const r=this.__v_raw,i=z(r),o=Ot(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,f=r[e](...s),a=n?Es:t?Cs:ve;return!t&&me(i,"iterate",c?xs:gt),{next(){const{value:d,done:m}=f.next();return m?{value:d,done:m}:{value:l?[a(d[0]),a(d[1])]:a(d),done:m}},[Symbol.iterator](){return this}}}}function dn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function xl(e,t){const n={get(r){const i=this.__v_raw,o=z(i),l=z(r);e||(st(r,l)&&me(o,"get",r),me(o,"get",l));const{has:c}=un(o),f=t?Es:e?Cs:ve;if(c.call(o,r))return f(i.get(r));if(c.call(o,l))return f(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&me(z(r),"iterate",gt),Reflect.get(r,"size",r)},has(r){const i=this.__v_raw,o=z(i),l=z(r);return e||(st(r,l)&&me(o,"has",r),me(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=z(l),f=t?Es:e?Cs:ve;return!e&&me(c,"iterate",gt),l.forEach((a,d)=>r.call(i,f(a),f(d),o))}};return he(n,e?{add:dn("add"),set:dn("set"),delete:dn("delete"),clear:dn("clear")}:{add(r){!t&&!Le(r)&&!St(r)&&(r=z(r));const i=z(this);return un(i).has.call(i,r)||(i.add(r),Xe(i,"add",r,r)),this},set(r,i){!t&&!Le(i)&&!St(i)&&(i=z(i));const o=z(this),{has:l,get:c}=un(o);let f=l.call(o,r);f||(r=z(r),f=l.call(o,r));const a=c.call(o,r);return o.set(r,i),f?st(i,a)&&Xe(o,"set",r,i):Xe(o,"add",r,i),this},delete(r){const i=z(this),{has:o,get:l}=un(i);let c=o.call(i,r);c||(r=z(r),c=o.call(i,r)),l&&l.call(i,r);const f=i.delete(r);return c&&Xe(i,"delete",r,void 0),f},clear(){const r=z(this),i=r.size!==0,o=r.clear();return i&&Xe(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Tl(r,e,t)}),n}function Gs(e,t){const n=xl(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(Q(n,r)&&r in s?n:s,r,i)}const El={get:Gs(!1,!1)},Cl={get:Gs(!1,!0)},Al={get:Gs(!0,!1)};const Mi=new WeakMap,Oi=new WeakMap,Pi=new WeakMap,Rl=new WeakMap;function Ml(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ol(e){return e.__v_skip||!Object.isExtensible(e)?0:Ml(Zo(e))}function It(e){return St(e)?e:Xs(e,!1,_l,El,Mi)}function Pl(e){return Xs(e,!1,Sl,Cl,Oi)}function Un(e){return Xs(e,!0,wl,Al,Pi)}function Xs(e,t,n,s,r){if(!se(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=Ol(e);if(o===0)return e;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function mt(e){return St(e)?mt(e.__v_raw):!!(e&&e.__v_isReactive)}function St(e){return!!(e&&e.__v_isReadonly)}function Le(e){return!!(e&&e.__v_isShallow)}function Ys(e){return e?!!e.__v_raw:!1}function z(e){const t=e&&e.__v_raw;return t?z(t):e}function xn(e){return!Q(e,"__v_skip")&&Object.isExtensible(e)&&hi(e,"__v_skip",!0),e}const ve=e=>se(e)?It(e):e,Cs=e=>se(e)?Un(e):e;function fe(e){return e?e.__v_isRef===!0:!1}function De(e){return Li(e,!1)}function xe(e){return Li(e,!0)}function Li(e,t){return fe(e)?e:new Ll(e,t)}class Ll{constructor(t,n){this.dep=new Vn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:z(t),this._value=n?t:ve(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Le(t)||St(t);t=s?t:z(t),st(t,n)&&(this._rawValue=t,this._value=s?t:ve(t),this.dep.trigger())}}function Js(e){return fe(e)?e.value:e}function le(e){return q(e)?e():Js(e)}const Il={get:(e,t,n)=>t==="__v_raw"?e:Js(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return fe(r)&&!fe(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Ii(e){return mt(e)?e:new Proxy(e,Il)}class Nl{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new Vn,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Fl(e){return new Nl(e)}class Hl{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return pl(z(this._object),this._key)}}class Dl{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function $l(e,t,n){return fe(e)?e:q(e)?new Dl(e):se(e)&&arguments.length>1?jl(e,t,n):De(e)}function jl(e,t,n){const s=e[t];return fe(s)?s:new Hl(e,t,n)}class Vl{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Vn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Xt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ne!==this)return _i(this,!0),!0}get value(){const t=this.dep.track();return Ti(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function kl(e,t,n=!1){let s,r;return q(e)?s=e:(s=e.get,r=e.set),new Vl(s,r,n)}const hn={},On=new WeakMap;let ht;function Ul(e,t=!1,n=ht){if(n){let s=On.get(n);s||On.set(n,s=[]),s.push(e)}}function Wl(e,t,n=ee){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,f=g=>r?g:Le(g)||r===!1||r===0?Ye(g,1):Ye(g);let a,d,m,v,_=!1,b=!1;if(fe(e)?(d=()=>e.value,_=Le(e)):mt(e)?(d=()=>f(e),_=!0):K(e)?(b=!0,_=e.some(g=>mt(g)||Le(g)),d=()=>e.map(g=>{if(fe(g))return g.value;if(mt(g))return f(g);if(q(g))return c?c(g,2):g()})):q(e)?t?d=c?()=>c(e,2):e:d=()=>{if(m){lt();try{m()}finally{ct()}}const g=ht;ht=a;try{return c?c(e,3,[v]):e(v)}finally{ht=g}}:d=Be,t&&r){const g=d,O=r===!0?1/0:r;d=()=>Ye(g(),O)}const k=vi(),P=()=>{a.stop(),k&&k.active&&Vs(k.effects,a)};if(i&&t){const g=t;t=(...O)=>{g(...O),P()}}let D=b?new Array(e.length).fill(hn):hn;const p=g=>{if(!(!(a.flags&1)||!a.dirty&&!g))if(t){const O=a.run();if(r||_||(b?O.some(($,R)=>st($,D[R])):st(O,D))){m&&m();const $=ht;ht=a;try{const R=[O,D===hn?void 0:b&&D[0]===hn?[]:D,v];c?c(t,3,R):t(...R),D=O}finally{ht=$}}}else a.run()};return l&&l(p),a=new yi(d),a.scheduler=o?()=>o(p,!1):p,v=g=>Ul(g,!1,a),m=a.onStop=()=>{const g=On.get(a);if(g){if(c)c(g,4);else for(const O of g)O();On.delete(a)}},t?s?p(!0):D=a.run():o?o(p.bind(null,!0),!0):a.run(),P.pause=a.pause.bind(a),P.resume=a.resume.bind(a),P.stop=P,P}function Ye(e,t=1/0,n){if(t<=0||!se(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,fe(e))Ye(e.value,t,n);else if(K(e))for(let s=0;s{Ye(s,t,n)});else if(di(e)){for(const s in e)Ye(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&Ye(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function sn(e,t,n,s){try{return s?e(...s):e()}catch(r){rn(r,t,n)}}function $e(e,t,n,s){if(q(e)){const r=sn(e,t,n,s);return r&&fi(r)&&r.catch(i=>{rn(i,t,n)}),r}if(K(e)){const r=[];for(let i=0;i>>1,r=Se[s],i=Jt(r);i=Jt(n)?Se.push(e):Se.splice(Kl(t),0,e),e.flags|=1,Fi()}}function Fi(){Pn||(Pn=Ni.then(Hi))}function ql(e){K(e)?Lt.push(...e):et&&e.id===-1?et.splice(Ct+1,0,e):e.flags&1||(Lt.push(e),e.flags|=1),Fi()}function gr(e,t,n=Ue+1){for(;nJt(n)-Jt(s));if(Lt.length=0,et){et.push(...t);return}for(et=t,Ct=0;Cte.id==null?e.flags&2?-1:1/0:e.id;function Hi(e){try{for(Ue=0;Ue{s._d&&Or(-1);const i=In(t);let o;try{o=e(...r)}finally{In(i),s._d&&Or(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Pf(e,t){if(de===null)return e;const n=Xn(de),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,Wt=e=>e&&(e.disabled||e.disabled===""),mr=e=>e&&(e.defer||e.defer===""),vr=e=>typeof SVGElement<"u"&&e instanceof SVGElement,yr=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,As=(e,t)=>{const n=e&&e.to;return oe(n)?t?t(n):null:n},Vi={name:"Teleport",__isTeleport:!0,process(e,t,n,s,r,i,o,l,c,f){const{mc:a,pc:d,pbc:m,o:{insert:v,querySelector:_,createText:b,createComment:k}}=f,P=Wt(t.props);let{shapeFlag:D,children:p,dynamicChildren:g}=t;if(e==null){const O=t.el=b(""),$=t.anchor=b("");v(O,n,s),v($,n,s);const R=(T,M)=>{D&16&&(r&&r.isCE&&(r.ce._teleportTarget=T),a(p,T,M,r,i,o,l,c))},j=()=>{const T=t.target=As(t.props,_),M=ki(T,t,b,v);T&&(o!=="svg"&&vr(T)?o="svg":o!=="mathml"&&yr(T)&&(o="mathml"),P||(R(T,M),En(t,!1)))};P&&(R(n,$),En(t,!0)),mr(t.props)?_e(()=>{j(),t.el.__isMounted=!0},i):j()}else{if(mr(t.props)&&!e.el.__isMounted){_e(()=>{Vi.process(e,t,n,s,r,i,o,l,c,f),delete e.el.__isMounted},i);return}t.el=e.el,t.targetStart=e.targetStart;const O=t.anchor=e.anchor,$=t.target=e.target,R=t.targetAnchor=e.targetAnchor,j=Wt(e.props),T=j?n:$,M=j?O:R;if(o==="svg"||vr($)?o="svg":(o==="mathml"||yr($))&&(o="mathml"),g?(m(e.dynamicChildren,g,T,r,i,o,l),tr(e,t,!0)):c||d(e,t,T,M,r,i,o,l,!1),P)j?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):pn(t,n,O,f,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const A=t.target=As(t.props,_);A&&pn(t,A,null,f,0)}else j&&pn(t,$,R,f,1);En(t,P)}},remove(e,t,n,{um:s,o:{remove:r}},i){const{shapeFlag:o,children:l,anchor:c,targetStart:f,targetAnchor:a,target:d,props:m}=e;if(d&&(r(f),r(a)),i&&r(c),o&16){const v=i||!Wt(m);for(let _=0;_{e.isMounted=!0}),Xi(()=>{e.isUnmounting=!0}),e}const Me=[Function,Array],Ui={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Me,onEnter:Me,onAfterEnter:Me,onEnterCancelled:Me,onBeforeLeave:Me,onLeave:Me,onAfterLeave:Me,onLeaveCancelled:Me,onBeforeAppear:Me,onAppear:Me,onAfterAppear:Me,onAppearCancelled:Me},Wi=e=>{const t=e.subTree;return t.component?Wi(t.component):t},Jl={name:"BaseTransition",props:Ui,setup(e,{slots:t}){const n=ln(),s=Yl();return()=>{const r=t.default&&qi(t.default(),!0);if(!r||!r.length)return;const i=Bi(r),o=z(e),{mode:l}=o;if(s.isLeaving)return ss(i);const c=br(i);if(!c)return ss(i);let f=Rs(c,o,s,n,d=>f=d);c.type!==ye&&zt(c,f);let a=n.subTree&&br(n.subTree);if(a&&a.type!==ye&&!pt(c,a)&&Wi(n).type!==ye){let d=Rs(a,o,s,n);if(zt(a,d),l==="out-in"&&c.type!==ye)return s.isLeaving=!0,d.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete d.afterLeave,a=void 0},ss(i);l==="in-out"&&c.type!==ye?d.delayLeave=(m,v,_)=>{const b=Ki(s,a);b[String(a.key)]=a,m[tt]=()=>{v(),m[tt]=void 0,delete f.delayedLeave,a=void 0},f.delayedLeave=()=>{_(),delete f.delayedLeave,a=void 0}}:a=void 0}else a&&(a=void 0);return i}}};function Bi(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==ye){t=n;break}}return t}const zl=Jl;function Ki(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Rs(e,t,n,s,r){const{appear:i,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:f,onAfterEnter:a,onEnterCancelled:d,onBeforeLeave:m,onLeave:v,onAfterLeave:_,onLeaveCancelled:b,onBeforeAppear:k,onAppear:P,onAfterAppear:D,onAppearCancelled:p}=t,g=String(e.key),O=Ki(n,e),$=(T,M)=>{T&&$e(T,s,9,M)},R=(T,M)=>{const A=M[1];$(T,M),K(T)?T.every(w=>w.length<=1)&&A():T.length<=1&&A()},j={mode:o,persisted:l,beforeEnter(T){let M=c;if(!n.isMounted)if(i)M=k||c;else return;T[tt]&&T[tt](!0);const A=O[g];A&&pt(e,A)&&A.el[tt]&&A.el[tt](),$(M,[T])},enter(T){let M=f,A=a,w=d;if(!n.isMounted)if(i)M=P||f,A=D||a,w=p||d;else return;let F=!1;const Y=T[gn]=ie=>{F||(F=!0,ie?$(w,[T]):$(A,[T]),j.delayedLeave&&j.delayedLeave(),T[gn]=void 0)};M?R(M,[T,Y]):Y()},leave(T,M){const A=String(e.key);if(T[gn]&&T[gn](!0),n.isUnmounting)return M();$(m,[T]);let w=!1;const F=T[tt]=Y=>{w||(w=!0,M(),Y?$(b,[T]):$(_,[T]),T[tt]=void 0,O[A]===e&&delete O[A])};O[A]=e,v?R(v,[T,F]):F()},clone(T){const M=Rs(T,t,n,s,r);return r&&r(M),M}};return j}function ss(e){if(on(e))return e=rt(e),e.children=null,e}function br(e){if(!on(e))return ji(e.type)&&e.children?Bi(e.children):e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&q(n.default))return n.default()}}function zt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,zt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function qi(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iQt(_,t&&(K(t)?t[b]:t),n,s,r));return}if(vt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Qt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Xn(s.component):s.el,o=r?null:i,{i:l,r:c}=e,f=t&&t.r,a=l.refs===ee?l.refs={}:l.refs,d=l.setupState,m=z(d),v=d===ee?()=>!1:_=>Q(m,_);if(f!=null&&f!==c&&(oe(f)?(a[f]=null,v(f)&&(d[f]=null)):fe(f)&&(f.value=null)),q(c))sn(c,l,12,[o,a]);else{const _=oe(c),b=fe(c);if(_||b){const k=()=>{if(e.f){const P=_?v(c)?d[c]:a[c]:c.value;r?K(P)&&Vs(P,i):K(P)?P.includes(i)||P.push(i):_?(a[c]=[i],v(c)&&(d[c]=a[c])):(c.value=[i],e.k&&(a[e.k]=c.value))}else _?(a[c]=o,v(c)&&(d[c]=o)):b&&(c.value=o,e.k&&(a[e.k]=o))};o?(k.id=-1,_e(k,n)):k()}}}let _r=!1;const Et=()=>{_r||(console.error("Hydration completed but contains mismatches."),_r=!0)},Ql=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Zl=e=>e.namespaceURI.includes("MathML"),mn=e=>{if(e.nodeType===1){if(Ql(e))return"svg";if(Zl(e))return"mathml"}},Rt=e=>e.nodeType===8;function ec(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:o,remove:l,insert:c,createComment:f}}=e,a=(p,g)=>{if(!g.hasChildNodes()){n(null,p,g),Ln(),g._vnode=p;return}d(g.firstChild,p,null,null,null),Ln(),g._vnode=p},d=(p,g,O,$,R,j=!1)=>{j=j||!!g.dynamicChildren;const T=Rt(p)&&p.data==="[",M=()=>b(p,g,O,$,R,T),{type:A,ref:w,shapeFlag:F,patchFlag:Y}=g;let ie=p.nodeType;g.el=p,Y===-2&&(j=!1,g.dynamicChildren=null);let U=null;switch(A){case _t:ie!==3?g.children===""?(c(g.el=r(""),o(p),p),U=p):U=M():(p.data!==g.children&&(Et(),p.data=g.children),U=i(p));break;case ye:D(p)?(U=i(p),P(g.el=p.content.firstChild,p,O)):ie!==8||T?U=M():U=i(p);break;case Kt:if(T&&(p=i(p),ie=p.nodeType),ie===1||ie===3){U=p;const X=!g.children.length;for(let V=0;V{j=j||!!g.dynamicChildren;const{type:T,props:M,patchFlag:A,shapeFlag:w,dirs:F,transition:Y}=g,ie=T==="input"||T==="option";if(ie||A!==-1){F&&We(g,null,O,"created");let U=!1;if(D(p)){U=po(null,Y)&&O&&O.vnode.props&&O.vnode.props.appear;const V=p.content.firstChild;U&&Y.beforeEnter(V),P(V,p,O),g.el=p=V}if(w&16&&!(M&&(M.innerHTML||M.textContent))){let V=v(p.firstChild,g,p,O,$,R,j);for(;V;){vn(p,1)||Et();const ae=V;V=V.nextSibling,l(ae)}}else if(w&8){let V=g.children;V[0]===` +`&&(p.tagName==="PRE"||p.tagName==="TEXTAREA")&&(V=V.slice(1)),p.textContent!==V&&(vn(p,0)||Et(),p.textContent=g.children)}if(M){if(ie||!j||A&48){const V=p.tagName.includes("-");for(const ae in M)(ie&&(ae.endsWith("value")||ae==="indeterminate")||nn(ae)&&!Pt(ae)||ae[0]==="."||V)&&s(p,ae,null,M[ae],void 0,O)}else if(M.onClick)s(p,"onClick",null,M.onClick,void 0,O);else if(A&4&&mt(M.style))for(const V in M.style)M.style[V]}let X;(X=M&&M.onVnodeBeforeMount)&&Oe(X,O,g),F&&We(g,null,O,"beforeMount"),((X=M&&M.onVnodeMounted)||F||U)&&_o(()=>{X&&Oe(X,O,g),U&&Y.enter(p),F&&We(g,null,O,"mounted")},$)}return p.nextSibling},v=(p,g,O,$,R,j,T)=>{T=T||!!g.dynamicChildren;const M=g.children,A=M.length;for(let w=0;w{const{slotScopeIds:T}=g;T&&(R=R?R.concat(T):T);const M=o(p),A=v(i(p),g,M,O,$,R,j);return A&&Rt(A)&&A.data==="]"?i(g.anchor=A):(Et(),c(g.anchor=f("]"),M,A),A)},b=(p,g,O,$,R,j)=>{if(vn(p.parentElement,1)||Et(),g.el=null,j){const A=k(p);for(;;){const w=i(p);if(w&&w!==A)l(w);else break}}const T=i(p),M=o(p);return l(p),n(null,g,M,T,O,$,mn(M),R),O&&(O.vnode.el=g.el,yo(O,g.el)),T},k=(p,g="[",O="]")=>{let $=0;for(;p;)if(p=i(p),p&&Rt(p)&&(p.data===g&&$++,p.data===O)){if($===0)return i(p);$--}return p},P=(p,g,O)=>{const $=g.parentNode;$&&$.replaceChild(p,g);let R=O;for(;R;)R.vnode.el===g&&(R.vnode.el=R.subTree.el=p),R=R.parent},D=p=>p.nodeType===1&&p.tagName==="TEMPLATE";return[a,d]}const wr="data-allow-mismatch",tc={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function vn(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(wr);)e=e.parentElement;const n=e&&e.getAttribute(wr);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes(tc[t])}}jn().requestIdleCallback;jn().cancelIdleCallback;function nc(e,t){if(Rt(e)&&e.data==="["){let n=1,s=e.nextSibling;for(;s;){if(s.nodeType===1){if(t(s)===!1)break}else if(Rt(s))if(s.data==="]"){if(--n===0)break}else s.data==="["&&n++;s=s.nextSibling}}else t(e)}const vt=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function If(e){q(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:s,delay:r=200,hydrate:i,timeout:o,suspensible:l=!0,onError:c}=e;let f=null,a,d=0;const m=()=>(d++,f=null,v()),v=()=>{let _;return f||(_=f=t().catch(b=>{if(b=b instanceof Error?b:new Error(String(b)),c)return new Promise((k,P)=>{c(b,()=>k(m()),()=>P(b),d+1)});throw b}).then(b=>_!==f&&f?f:(b&&(b.__esModule||b[Symbol.toStringTag]==="Module")&&(b=b.default),a=b,b)))};return Qs({name:"AsyncComponentWrapper",__asyncLoader:v,__asyncHydrate(_,b,k){const P=i?()=>{const D=i(k,p=>nc(_,p));D&&(b.bum||(b.bum=[])).push(D)}:k;a?P():v().then(()=>!b.isUnmounted&&P())},get __asyncResolved(){return a},setup(){const _=ue;if(Zs(_),a)return()=>rs(a,_);const b=p=>{f=null,rn(p,_,13,!s)};if(l&&_.suspense||Nt)return v().then(p=>()=>rs(p,_)).catch(p=>(b(p),()=>s?ce(s,{error:p}):null));const k=De(!1),P=De(),D=De(!!r);return r&&setTimeout(()=>{D.value=!1},r),o!=null&&setTimeout(()=>{if(!k.value&&!P.value){const p=new Error(`Async component timed out after ${o}ms.`);b(p),P.value=p}},o),v().then(()=>{k.value=!0,_.parent&&on(_.parent.vnode)&&_.parent.update()}).catch(p=>{b(p),P.value=p}),()=>{if(k.value&&a)return rs(a,_);if(P.value&&s)return ce(s,{error:P.value});if(n&&!D.value)return ce(n)}}})}function rs(e,t){const{ref:n,props:s,children:r,ce:i}=t.vnode,o=ce(e,s,r);return o.ref=n,o.ce=i,delete t.vnode.ce,o}const on=e=>e.type.__isKeepAlive;function sc(e,t){Gi(e,"a",t)}function rc(e,t){Gi(e,"da",t)}function Gi(e,t,n=ue){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Bn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)on(r.parent.vnode)&&ic(s,t,n,r),r=r.parent}}function ic(e,t,n,s){const r=Bn(t,e,s,!0);Kn(()=>{Vs(s[t],r)},n)}function Bn(e,t,n=ue,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{lt();const l=cn(n),c=$e(t,n,e,o);return l(),ct(),c});return s?r.unshift(i):r.push(i),i}}const Qe=e=>(t,n=ue)=>{(!Nt||e==="sp")&&Bn(e,(...s)=>t(...s),n)},oc=Qe("bm"),Ft=Qe("m"),lc=Qe("bu"),cc=Qe("u"),Xi=Qe("bum"),Kn=Qe("um"),ac=Qe("sp"),fc=Qe("rtg"),uc=Qe("rtc");function dc(e,t=ue){Bn("ec",e,t)}const Yi="components";function Nf(e,t){return zi(Yi,e,!0,t)||e}const Ji=Symbol.for("v-ndc");function Ff(e){return oe(e)?zi(Yi,e,!1)||e:e||Ji}function zi(e,t,n=!0,s=!1){const r=de||ue;if(r){const i=r.type;{const l=Jc(i,!1);if(l&&(l===t||l===Ne(t)||l===$n(Ne(t))))return i}const o=Sr(r[e]||i[e],t)||Sr(r.appContext[e],t);return!o&&s?i:o}}function Sr(e,t){return e&&(e[t]||e[Ne(t)]||e[$n(Ne(t))])}function Hf(e,t,n,s){let r;const i=n,o=K(e);if(o||oe(e)){const l=o&&mt(e);let c=!1;l&&(c=!Le(e),e=kn(e)),r=new Array(e.length);for(let f=0,a=e.length;ft(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,f=l.length;cen(t)?!(t.type===ye||t.type===Te&&!Qi(t.children)):!0)?e:null}function $f(e,t){const n={};for(const s in e)n[/[A-Z]/.test(s)?`on:${s}`:Sn(s)]=e[s];return n}const Ms=e=>e?Eo(e)?Xn(e):Ms(e.parent):null,Bt=he(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Ms(e.parent),$root:e=>Ms(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>eo(e),$forceUpdate:e=>e.f||(e.f=()=>{zs(e.update)}),$nextTick:e=>e.n||(e.n=Wn.bind(e.proxy)),$watch:e=>Nc.bind(e)}),is=(e,t)=>e!==ee&&!e.__isScriptSetup&&Q(e,t),hc={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let f;if(t[0]!=="$"){const v=o[t];if(v!==void 0)switch(v){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(is(s,t))return o[t]=1,s[t];if(r!==ee&&Q(r,t))return o[t]=2,r[t];if((f=e.propsOptions[0])&&Q(f,t))return o[t]=3,i[t];if(n!==ee&&Q(n,t))return o[t]=4,n[t];Os&&(o[t]=0)}}const a=Bt[t];let d,m;if(a)return t==="$attrs"&&me(e.attrs,"get",""),a(e);if((d=l.__cssModules)&&(d=d[t]))return d;if(n!==ee&&Q(n,t))return o[t]=4,n[t];if(m=c.config.globalProperties,Q(m,t))return m[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return is(r,t)?(r[t]=n,!0):s!==ee&&Q(s,t)?(s[t]=n,!0):Q(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==ee&&Q(e,o)||is(t,o)||(l=i[0])&&Q(l,o)||Q(s,o)||Q(Bt,o)||Q(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Q(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function jf(){return pc().slots}function pc(){const e=ln();return e.setupContext||(e.setupContext=Ao(e))}function Tr(e){return K(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Os=!0;function gc(e){const t=eo(e),n=e.proxy,s=e.ctx;Os=!1,t.beforeCreate&&xr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:f,created:a,beforeMount:d,mounted:m,beforeUpdate:v,updated:_,activated:b,deactivated:k,beforeDestroy:P,beforeUnmount:D,destroyed:p,unmounted:g,render:O,renderTracked:$,renderTriggered:R,errorCaptured:j,serverPrefetch:T,expose:M,inheritAttrs:A,components:w,directives:F,filters:Y}=t;if(f&&mc(f,s,null),o)for(const X in o){const V=o[X];q(V)&&(s[X]=V.bind(n))}if(r){const X=r.call(n,n);se(X)&&(e.data=It(X))}if(Os=!0,i)for(const X in i){const V=i[X],ae=q(V)?V.bind(n,n):q(V.get)?V.get.bind(n,n):Be,an=!q(V)&&q(V.set)?V.set.bind(n):Be,at=re({get:ae,set:an});Object.defineProperty(s,X,{enumerable:!0,configurable:!0,get:()=>at.value,set:Ve=>at.value=Ve})}if(l)for(const X in l)Zi(l[X],s,n,X);if(c){const X=q(c)?c.call(n):c;Reflect.ownKeys(X).forEach(V=>{Sc(V,X[V])})}a&&xr(a,e,"c");function U(X,V){K(V)?V.forEach(ae=>X(ae.bind(n))):V&&X(V.bind(n))}if(U(oc,d),U(Ft,m),U(lc,v),U(cc,_),U(sc,b),U(rc,k),U(dc,j),U(uc,$),U(fc,R),U(Xi,D),U(Kn,g),U(ac,T),K(M))if(M.length){const X=e.exposed||(e.exposed={});M.forEach(V=>{Object.defineProperty(X,V,{get:()=>n[V],set:ae=>n[V]=ae})})}else e.exposed||(e.exposed={});O&&e.render===Be&&(e.render=O),A!=null&&(e.inheritAttrs=A),w&&(e.components=w),F&&(e.directives=F),T&&Zs(e)}function mc(e,t,n=Be){K(e)&&(e=Ps(e));for(const s in e){const r=e[s];let i;se(r)?"default"in r?i=bt(r.from||s,r.default,!0):i=bt(r.from||s):i=bt(r),fe(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function xr(e,t,n){$e(K(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Zi(e,t,n,s){let r=s.includes(".")?mo(n,s):()=>n[s];if(oe(e)){const i=t[e];q(i)&&Ie(r,i)}else if(q(e))Ie(r,e.bind(n));else if(se(e))if(K(e))e.forEach(i=>Zi(i,t,n,s));else{const i=q(e.handler)?e.handler.bind(n):t[e.handler];q(i)&&Ie(r,i,e)}}function eo(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(f=>Nn(c,f,o,!0)),Nn(c,t,o)),se(t)&&i.set(t,c),c}function Nn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Nn(e,i,n,!0),r&&r.forEach(o=>Nn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=vc[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const vc={data:Er,props:Cr,emits:Cr,methods:Vt,computed:Vt,beforeCreate:be,created:be,beforeMount:be,mounted:be,beforeUpdate:be,updated:be,beforeDestroy:be,beforeUnmount:be,destroyed:be,unmounted:be,activated:be,deactivated:be,errorCaptured:be,serverPrefetch:be,components:Vt,directives:Vt,watch:bc,provide:Er,inject:yc};function Er(e,t){return t?e?function(){return he(q(e)?e.call(this,this):e,q(t)?t.call(this,this):t)}:t:e}function yc(e,t){return Vt(Ps(e),Ps(t))}function Ps(e){if(K(e)){const t={};for(let n=0;n1)return n&&q(t)?t.call(s&&s.proxy):t}}function no(){return!!(ue||de||yt)}const so={},ro=()=>Object.create(so),io=e=>Object.getPrototypeOf(e)===so;function Tc(e,t,n,s=!1){const r={},i=ro();e.propsDefaults=Object.create(null),oo(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:Pl(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function xc(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=z(r),[c]=e.propsOptions;let f=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let d=0;d{c=!0;const[m,v]=lo(d,t,!0);he(o,m),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return se(e)&&s.set(e,Mt),Mt;if(K(i))for(let a=0;ae[0]==="_"||e==="$stable",er=e=>K(e)?e.map(Pe):[Pe(e)],Cc=(e,t,n)=>{if(t._n)return t;const s=Gl((...r)=>er(t(...r)),n);return s._c=!1,s},ao=(e,t,n)=>{const s=e._ctx;for(const r in e){if(co(r))continue;const i=e[r];if(q(i))t[r]=Cc(r,i,s);else if(i!=null){const o=er(i);t[r]=()=>o}}},fo=(e,t)=>{const n=er(t);e.slots.default=()=>n},uo=(e,t,n)=>{for(const s in t)(n||s!=="_")&&(e[s]=t[s])},Ac=(e,t,n)=>{const s=e.slots=ro();if(e.vnode.shapeFlag&32){const r=t._;r?(uo(s,t,n),n&&hi(s,"_",r,!0)):ao(t,s)}else t&&fo(e,t)},Rc=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=ee;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:uo(r,t,n):(i=!t.$stable,ao(t,r)),o=t}else t&&(fo(e,t),o={default:1});if(i)for(const l in r)!co(l)&&o[l]==null&&delete r[l]},_e=_o;function Mc(e){return ho(e)}function Oc(e){return ho(e,ec)}function ho(e,t){const n=jn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:f,setElementText:a,parentNode:d,nextSibling:m,setScopeId:v=Be,insertStaticContent:_}=e,b=(u,h,y,E=null,S=null,x=null,N=void 0,I=null,L=!!h.dynamicChildren)=>{if(u===h)return;u&&!pt(u,h)&&(E=fn(u),Ve(u,S,x,!0),u=null),h.patchFlag===-2&&(L=!1,h.dynamicChildren=null);const{type:C,ref:B,shapeFlag:H}=h;switch(C){case _t:k(u,h,y,E);break;case ye:P(u,h,y,E);break;case Kt:u==null&&D(h,y,E,N);break;case Te:w(u,h,y,E,S,x,N,I,L);break;default:H&1?O(u,h,y,E,S,x,N,I,L):H&6?F(u,h,y,E,S,x,N,I,L):(H&64||H&128)&&C.process(u,h,y,E,S,x,N,I,L,Tt)}B!=null&&S&&Qt(B,u&&u.ref,x,h||u,!h)},k=(u,h,y,E)=>{if(u==null)s(h.el=l(h.children),y,E);else{const S=h.el=u.el;h.children!==u.children&&f(S,h.children)}},P=(u,h,y,E)=>{u==null?s(h.el=c(h.children||""),y,E):h.el=u.el},D=(u,h,y,E)=>{[u.el,u.anchor]=_(u.children,h,y,E,u.el,u.anchor)},p=({el:u,anchor:h},y,E)=>{let S;for(;u&&u!==h;)S=m(u),s(u,y,E),u=S;s(h,y,E)},g=({el:u,anchor:h})=>{let y;for(;u&&u!==h;)y=m(u),r(u),u=y;r(h)},O=(u,h,y,E,S,x,N,I,L)=>{h.type==="svg"?N="svg":h.type==="math"&&(N="mathml"),u==null?$(h,y,E,S,x,N,I,L):T(u,h,S,x,N,I,L)},$=(u,h,y,E,S,x,N,I)=>{let L,C;const{props:B,shapeFlag:H,transition:W,dirs:G}=u;if(L=u.el=o(u.type,x,B&&B.is,B),H&8?a(L,u.children):H&16&&j(u.children,L,null,E,S,os(u,x),N,I),G&&We(u,null,E,"created"),R(L,u,u.scopeId,N,E),B){for(const te in B)te!=="value"&&!Pt(te)&&i(L,te,null,B[te],x,E);"value"in B&&i(L,"value",null,B.value,x),(C=B.onVnodeBeforeMount)&&Oe(C,E,u)}G&&We(u,null,E,"beforeMount");const J=po(S,W);J&&W.beforeEnter(L),s(L,h,y),((C=B&&B.onVnodeMounted)||J||G)&&_e(()=>{C&&Oe(C,E,u),J&&W.enter(L),G&&We(u,null,E,"mounted")},S)},R=(u,h,y,E,S)=>{if(y&&v(u,y),E)for(let x=0;x{for(let C=L;C{const I=h.el=u.el;let{patchFlag:L,dynamicChildren:C,dirs:B}=h;L|=u.patchFlag&16;const H=u.props||ee,W=h.props||ee;let G;if(y&&ft(y,!1),(G=W.onVnodeBeforeUpdate)&&Oe(G,y,h,u),B&&We(h,u,y,"beforeUpdate"),y&&ft(y,!0),(H.innerHTML&&W.innerHTML==null||H.textContent&&W.textContent==null)&&a(I,""),C?M(u.dynamicChildren,C,I,y,E,os(h,S),x):N||V(u,h,I,null,y,E,os(h,S),x,!1),L>0){if(L&16)A(I,H,W,y,S);else if(L&2&&H.class!==W.class&&i(I,"class",null,W.class,S),L&4&&i(I,"style",H.style,W.style,S),L&8){const J=h.dynamicProps;for(let te=0;te{G&&Oe(G,y,h,u),B&&We(h,u,y,"updated")},E)},M=(u,h,y,E,S,x,N)=>{for(let I=0;I{if(h!==y){if(h!==ee)for(const x in h)!Pt(x)&&!(x in y)&&i(u,x,h[x],null,S,E);for(const x in y){if(Pt(x))continue;const N=y[x],I=h[x];N!==I&&x!=="value"&&i(u,x,I,N,S,E)}"value"in y&&i(u,"value",h.value,y.value,S)}},w=(u,h,y,E,S,x,N,I,L)=>{const C=h.el=u?u.el:l(""),B=h.anchor=u?u.anchor:l("");let{patchFlag:H,dynamicChildren:W,slotScopeIds:G}=h;G&&(I=I?I.concat(G):G),u==null?(s(C,y,E),s(B,y,E),j(h.children||[],y,B,S,x,N,I,L)):H>0&&H&64&&W&&u.dynamicChildren?(M(u.dynamicChildren,W,y,S,x,N,I),(h.key!=null||S&&h===S.subTree)&&tr(u,h,!0)):V(u,h,y,B,S,x,N,I,L)},F=(u,h,y,E,S,x,N,I,L)=>{h.slotScopeIds=I,u==null?h.shapeFlag&512?S.ctx.activate(h,y,E,N,L):Y(h,y,E,S,x,N,L):ie(u,h,L)},Y=(u,h,y,E,S,x,N)=>{const I=u.component=qc(u,E,S);if(on(u)&&(I.ctx.renderer=Tt),Gc(I,!1,N),I.asyncDep){if(S&&S.registerDep(I,U,N),!u.el){const L=I.subTree=ce(ye);P(null,L,h,y)}}else U(I,u,h,y,S,x,N)},ie=(u,h,y)=>{const E=h.component=u.component;if(jc(u,h,y))if(E.asyncDep&&!E.asyncResolved){X(E,h,y);return}else E.next=h,E.update();else h.el=u.el,E.vnode=h},U=(u,h,y,E,S,x,N)=>{const I=()=>{if(u.isMounted){let{next:H,bu:W,u:G,parent:J,vnode:te}=u;{const Ce=go(u);if(Ce){H&&(H.el=te.el,X(u,H,N)),Ce.asyncDep.then(()=>{u.isUnmounted||I()});return}}let Z=H,Ee;ft(u,!1),H?(H.el=te.el,X(u,H,N)):H=te,W&&Tn(W),(Ee=H.props&&H.props.onVnodeBeforeUpdate)&&Oe(Ee,J,H,te),ft(u,!0);const pe=ls(u),Fe=u.subTree;u.subTree=pe,b(Fe,pe,d(Fe.el),fn(Fe),u,S,x),H.el=pe.el,Z===null&&yo(u,pe.el),G&&_e(G,S),(Ee=H.props&&H.props.onVnodeUpdated)&&_e(()=>Oe(Ee,J,H,te),S)}else{let H;const{el:W,props:G}=h,{bm:J,m:te,parent:Z,root:Ee,type:pe}=u,Fe=vt(h);if(ft(u,!1),J&&Tn(J),!Fe&&(H=G&&G.onVnodeBeforeMount)&&Oe(H,Z,h),ft(u,!0),W&&Qn){const Ce=()=>{u.subTree=ls(u),Qn(W,u.subTree,u,S,null)};Fe&&pe.__asyncHydrate?pe.__asyncHydrate(W,u,Ce):Ce()}else{Ee.ce&&Ee.ce._injectChildStyle(pe);const Ce=u.subTree=ls(u);b(null,Ce,y,E,u,S,x),h.el=Ce.el}if(te&&_e(te,S),!Fe&&(H=G&&G.onVnodeMounted)){const Ce=h;_e(()=>Oe(H,Z,Ce),S)}(h.shapeFlag&256||Z&&vt(Z.vnode)&&Z.vnode.shapeFlag&256)&&u.a&&_e(u.a,S),u.isMounted=!0,h=y=E=null}};u.scope.on();const L=u.effect=new yi(I);u.scope.off();const C=u.update=L.run.bind(L),B=u.job=L.runIfDirty.bind(L);B.i=u,B.id=u.uid,L.scheduler=()=>zs(B),ft(u,!0),C()},X=(u,h,y)=>{h.component=u;const E=u.vnode.props;u.vnode=h,u.next=null,xc(u,h.props,E,y),Rc(u,h.children,y),lt(),gr(u),ct()},V=(u,h,y,E,S,x,N,I,L=!1)=>{const C=u&&u.children,B=u?u.shapeFlag:0,H=h.children,{patchFlag:W,shapeFlag:G}=h;if(W>0){if(W&128){an(C,H,y,E,S,x,N,I,L);return}else if(W&256){ae(C,H,y,E,S,x,N,I,L);return}}G&8?(B&16&&Ht(C,S,x),H!==C&&a(y,H)):B&16?G&16?an(C,H,y,E,S,x,N,I,L):Ht(C,S,x,!0):(B&8&&a(y,""),G&16&&j(H,y,E,S,x,N,I,L))},ae=(u,h,y,E,S,x,N,I,L)=>{u=u||Mt,h=h||Mt;const C=u.length,B=h.length,H=Math.min(C,B);let W;for(W=0;WB?Ht(u,S,x,!0,!1,H):j(h,y,E,S,x,N,I,L,H)},an=(u,h,y,E,S,x,N,I,L)=>{let C=0;const B=h.length;let H=u.length-1,W=B-1;for(;C<=H&&C<=W;){const G=u[C],J=h[C]=L?nt(h[C]):Pe(h[C]);if(pt(G,J))b(G,J,y,null,S,x,N,I,L);else break;C++}for(;C<=H&&C<=W;){const G=u[H],J=h[W]=L?nt(h[W]):Pe(h[W]);if(pt(G,J))b(G,J,y,null,S,x,N,I,L);else break;H--,W--}if(C>H){if(C<=W){const G=W+1,J=GW)for(;C<=H;)Ve(u[C],S,x,!0),C++;else{const G=C,J=C,te=new Map;for(C=J;C<=W;C++){const Ae=h[C]=L?nt(h[C]):Pe(h[C]);Ae.key!=null&&te.set(Ae.key,C)}let Z,Ee=0;const pe=W-J+1;let Fe=!1,Ce=0;const Dt=new Array(pe);for(C=0;C=pe){Ve(Ae,S,x,!0);continue}let ke;if(Ae.key!=null)ke=te.get(Ae.key);else for(Z=J;Z<=W;Z++)if(Dt[Z-J]===0&&pt(Ae,h[Z])){ke=Z;break}ke===void 0?Ve(Ae,S,x,!0):(Dt[ke-J]=C+1,ke>=Ce?Ce=ke:Fe=!0,b(Ae,h[ke],y,null,S,x,N,I,L),Ee++)}const fr=Fe?Pc(Dt):Mt;for(Z=fr.length-1,C=pe-1;C>=0;C--){const Ae=J+C,ke=h[Ae],ur=Ae+1{const{el:x,type:N,transition:I,children:L,shapeFlag:C}=u;if(C&6){at(u.component.subTree,h,y,E);return}if(C&128){u.suspense.move(h,y,E);return}if(C&64){N.move(u,h,y,Tt);return}if(N===Te){s(x,h,y);for(let H=0;HI.enter(x),S);else{const{leave:H,delayLeave:W,afterLeave:G}=I,J=()=>s(x,h,y),te=()=>{H(x,()=>{J(),G&&G()})};W?W(x,J,te):te()}else s(x,h,y)},Ve=(u,h,y,E=!1,S=!1)=>{const{type:x,props:N,ref:I,children:L,dynamicChildren:C,shapeFlag:B,patchFlag:H,dirs:W,cacheIndex:G}=u;if(H===-2&&(S=!1),I!=null&&Qt(I,null,y,u,!0),G!=null&&(h.renderCache[G]=void 0),B&256){h.ctx.deactivate(u);return}const J=B&1&&W,te=!vt(u);let Z;if(te&&(Z=N&&N.onVnodeBeforeUnmount)&&Oe(Z,h,u),B&6)Jo(u.component,y,E);else{if(B&128){u.suspense.unmount(y,E);return}J&&We(u,null,h,"beforeUnmount"),B&64?u.type.remove(u,h,y,Tt,E):C&&!C.hasOnce&&(x!==Te||H>0&&H&64)?Ht(C,h,y,!1,!0):(x===Te&&H&384||!S&&B&16)&&Ht(L,h,y),E&&cr(u)}(te&&(Z=N&&N.onVnodeUnmounted)||J)&&_e(()=>{Z&&Oe(Z,h,u),J&&We(u,null,h,"unmounted")},y)},cr=u=>{const{type:h,el:y,anchor:E,transition:S}=u;if(h===Te){Yo(y,E);return}if(h===Kt){g(u);return}const x=()=>{r(y),S&&!S.persisted&&S.afterLeave&&S.afterLeave()};if(u.shapeFlag&1&&S&&!S.persisted){const{leave:N,delayLeave:I}=S,L=()=>N(y,x);I?I(u.el,x,L):L()}else x()},Yo=(u,h)=>{let y;for(;u!==h;)y=m(u),r(u),u=y;r(h)},Jo=(u,h,y)=>{const{bum:E,scope:S,job:x,subTree:N,um:I,m:L,a:C}=u;Rr(L),Rr(C),E&&Tn(E),S.stop(),x&&(x.flags|=8,Ve(N,u,h,y)),I&&_e(I,h),_e(()=>{u.isUnmounted=!0},h),h&&h.pendingBranch&&!h.isUnmounted&&u.asyncDep&&!u.asyncResolved&&u.suspenseId===h.pendingId&&(h.deps--,h.deps===0&&h.resolve())},Ht=(u,h,y,E=!1,S=!1,x=0)=>{for(let N=x;N{if(u.shapeFlag&6)return fn(u.component.subTree);if(u.shapeFlag&128)return u.suspense.next();const h=m(u.anchor||u.el),y=h&&h[$i];return y?m(y):h};let Jn=!1;const ar=(u,h,y)=>{u==null?h._vnode&&Ve(h._vnode,null,null,!0):b(h._vnode||null,u,h,null,null,null,y),h._vnode=u,Jn||(Jn=!0,gr(),Ln(),Jn=!1)},Tt={p:b,um:Ve,m:at,r:cr,mt:Y,mc:j,pc:V,pbc:M,n:fn,o:e};let zn,Qn;return t&&([zn,Qn]=t(Tt)),{render:ar,hydrate:zn,createApp:wc(ar,zn)}}function os({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function ft({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function po(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function tr(e,t,n=!1){const s=e.children,r=t.children;if(K(s)&&K(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function go(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:go(t)}function Rr(e){if(e)for(let t=0;tbt(Lc);function nr(e,t){return qn(e,null,t)}function Vf(e,t){return qn(e,null,{flush:"post"})}function Ie(e,t,n){return qn(e,t,n)}function qn(e,t,n=ee){const{immediate:s,deep:r,flush:i,once:o}=n,l=he({},n),c=t&&s||!t&&i!=="post";let f;if(Nt){if(i==="sync"){const v=Ic();f=v.__watcherHandles||(v.__watcherHandles=[])}else if(!c){const v=()=>{};return v.stop=Be,v.resume=Be,v.pause=Be,v}}const a=ue;l.call=(v,_,b)=>$e(v,a,_,b);let d=!1;i==="post"?l.scheduler=v=>{_e(v,a&&a.suspense)}:i!=="sync"&&(d=!0,l.scheduler=(v,_)=>{_?v():zs(v)}),l.augmentJob=v=>{t&&(v.flags|=4),d&&(v.flags|=2,a&&(v.id=a.uid,v.i=a))};const m=Wl(e,t,l);return Nt&&(f?f.push(m):c&&m()),m}function Nc(e,t,n){const s=this.proxy,r=oe(e)?e.includes(".")?mo(s,e):()=>s[e]:e.bind(s,s);let i;q(t)?i=t:(i=t.handler,n=t);const o=cn(this),l=qn(r,i.bind(s),n);return o(),l}function mo(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ne(t)}Modifiers`]||e[`${ot(t)}Modifiers`];function Hc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||ee;let r=n;const i=t.startsWith("update:"),o=i&&Fc(s,t.slice(7));o&&(o.trim&&(r=n.map(a=>oe(a)?a.trim():a)),o.number&&(r=n.map(Ss)));let l,c=s[l=Sn(t)]||s[l=Sn(Ne(t))];!c&&i&&(c=s[l=Sn(ot(t))]),c&&$e(c,e,6,r);const f=s[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,$e(f,e,6,r)}}function vo(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!q(e)){const c=f=>{const a=vo(f,t,!0);a&&(l=!0,he(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(se(e)&&s.set(e,null),null):(K(i)?i.forEach(c=>o[c]=null):he(o,i),se(e)&&s.set(e,o),o)}function Gn(e,t){return!e||!nn(t)?!1:(t=t.slice(2).replace(/Once$/,""),Q(e,t[0].toLowerCase()+t.slice(1))||Q(e,ot(t))||Q(e,t))}function ls(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:f,renderCache:a,props:d,data:m,setupState:v,ctx:_,inheritAttrs:b}=e,k=In(e);let P,D;try{if(n.shapeFlag&4){const g=r||s,O=g;P=Pe(f.call(O,g,a,d,v,m,_)),D=l}else{const g=t;P=Pe(g.length>1?g(d,{attrs:l,slots:o,emit:c}):g(d,null)),D=t.props?l:Dc(l)}}catch(g){qt.length=0,rn(g,e,1),P=ce(ye)}let p=P;if(D&&b!==!1){const g=Object.keys(D),{shapeFlag:O}=p;g.length&&O&7&&(i&&g.some(js)&&(D=$c(D,i)),p=rt(p,D,!1,!0))}return n.dirs&&(p=rt(p,null,!1,!0),p.dirs=p.dirs?p.dirs.concat(n.dirs):n.dirs),n.transition&&zt(p,n.transition),P=p,In(k),P}const Dc=e=>{let t;for(const n in e)(n==="class"||n==="style"||nn(n))&&((t||(t={}))[n]=e[n]);return t},$c=(e,t)=>{const n={};for(const s in e)(!js(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function jc(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,f=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Mr(s,o,f):!!o;if(c&8){const a=t.dynamicProps;for(let d=0;de.__isSuspense;function _o(e,t){t&&t.pendingBranch?K(e)?t.effects.push(...e):t.effects.push(e):ql(e)}const Te=Symbol.for("v-fgt"),_t=Symbol.for("v-txt"),ye=Symbol.for("v-cmt"),Kt=Symbol.for("v-stc"),qt=[];let Re=null;function Is(e=!1){qt.push(Re=e?null:[])}function Vc(){qt.pop(),Re=qt[qt.length-1]||null}let Zt=1;function Or(e,t=!1){Zt+=e,e<0&&Re&&t&&(Re.hasOnce=!0)}function wo(e){return e.dynamicChildren=Zt>0?Re||Mt:null,Vc(),Zt>0&&Re&&Re.push(e),e}function kf(e,t,n,s,r,i){return wo(To(e,t,n,s,r,i,!0))}function Ns(e,t,n,s,r){return wo(ce(e,t,n,s,r,!0))}function en(e){return e?e.__v_isVNode===!0:!1}function pt(e,t){return e.type===t.type&&e.key===t.key}const So=({key:e})=>e??null,Cn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?oe(e)||fe(e)||q(e)?{i:de,r:e,k:t,f:!!n}:e:null);function To(e,t=null,n=null,s=0,r=null,i=e===Te?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&So(t),ref:t&&Cn(t),scopeId:Di,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:de};return l?(sr(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=oe(n)?8:16),Zt>0&&!o&&Re&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Re.push(c),c}const ce=kc;function kc(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Ji)&&(e=ye),en(e)){const l=rt(e,t,!0);return n&&sr(l,n),Zt>0&&!i&&Re&&(l.shapeFlag&6?Re[Re.indexOf(e)]=l:Re.push(l)),l.patchFlag=-2,l}if(zc(e)&&(e=e.__vccOpts),t){t=Uc(t);let{class:l,style:c}=t;l&&!oe(l)&&(t.class=Ws(l)),se(c)&&(Ys(c)&&!K(c)&&(c=he({},c)),t.style=Us(c))}const o=oe(e)?1:bo(e)?128:ji(e)?64:se(e)?4:q(e)?2:0;return To(e,t,n,s,r,o,i,!0)}function Uc(e){return e?Ys(e)||io(e)?he({},e):e:null}function rt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,f=t?Wc(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&So(f),ref:t&&t.ref?n&&i?K(i)?i.concat(Cn(t)):[i,Cn(t)]:Cn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Te?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&rt(e.ssContent),ssFallback:e.ssFallback&&rt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&zt(a,c.clone(a)),a}function xo(e=" ",t=0){return ce(_t,null,e,t)}function Uf(e,t){const n=ce(Kt,null,e);return n.staticCount=t,n}function Wf(e="",t=!1){return t?(Is(),Ns(ye,null,e)):ce(ye,null,e)}function Pe(e){return e==null||typeof e=="boolean"?ce(ye):K(e)?ce(Te,null,e.slice()):en(e)?nt(e):ce(_t,null,String(e))}function nt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:rt(e)}function sr(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(K(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),sr(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!io(t)?t._ctx=de:r===3&&de&&(de.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else q(t)?(t={default:t,_ctx:de},n=32):(t=String(t),s&64?(n=16,t=[xo(t)]):n=8);e.children=t,e.shapeFlag|=n}function Wc(...e){const t={};for(let n=0;nue||de;let Fn,Fs;{const e=jn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};Fn=t("__VUE_INSTANCE_SETTERS__",n=>ue=n),Fs=t("__VUE_SSR_SETTERS__",n=>Nt=n)}const cn=e=>{const t=ue;return Fn(e),e.scope.on(),()=>{e.scope.off(),Fn(t)}},Pr=()=>{ue&&ue.scope.off(),Fn(null)};function Eo(e){return e.vnode.shapeFlag&4}let Nt=!1;function Gc(e,t=!1,n=!1){t&&Fs(t);const{props:s,children:r}=e.vnode,i=Eo(e);Tc(e,s,i,t),Ac(e,r,n);const o=i?Xc(e,t):void 0;return t&&Fs(!1),o}function Xc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,hc);const{setup:s}=n;if(s){lt();const r=e.setupContext=s.length>1?Ao(e):null,i=cn(e),o=sn(s,e,0,[e.props,r]),l=fi(o);if(ct(),i(),(l||e.sp)&&!vt(e)&&Zs(e),l){if(o.then(Pr,Pr),t)return o.then(c=>{Lr(e,c)}).catch(c=>{rn(c,e,0)});e.asyncDep=o}else Lr(e,o)}else Co(e)}function Lr(e,t,n){q(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:se(t)&&(e.setupState=Ii(t)),Co(e)}function Co(e,t,n){const s=e.type;e.render||(e.render=s.render||Be);{const r=cn(e);lt();try{gc(e)}finally{ct(),r()}}}const Yc={get(e,t){return me(e,"get",""),e[t]}};function Ao(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Yc),slots:e.slots,emit:e.emit,expose:t}}function Xn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Ii(xn(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Bt)return Bt[n](e)},has(t,n){return n in t||n in Bt}})):e.proxy}function Jc(e,t=!0){return q(e)?e.displayName||e.name:e.name||t&&e.__name}function zc(e){return q(e)&&"__vccOpts"in e}const re=(e,t)=>kl(e,t,Nt);function Hs(e,t,n){const s=arguments.length;return s===2?se(t)&&!K(t)?en(t)?ce(e,null,[t]):ce(e,t):ce(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&en(n)&&(n=[n]),ce(e,t,n))}const Qc="3.5.13";/** +* @vue/runtime-dom v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ds;const Ir=typeof window<"u"&&window.trustedTypes;if(Ir)try{Ds=Ir.createPolicy("vue",{createHTML:e=>e})}catch{}const Ro=Ds?e=>Ds.createHTML(e):e=>e,Zc="http://www.w3.org/2000/svg",ea="http://www.w3.org/1998/Math/MathML",Ge=typeof document<"u"?document:null,Nr=Ge&&Ge.createElement("template"),ta={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?Ge.createElementNS(Zc,e):t==="mathml"?Ge.createElementNS(ea,e):n?Ge.createElement(e,{is:n}):Ge.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>Ge.createTextNode(e),createComment:e=>Ge.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ge.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Nr.innerHTML=Ro(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Nr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Ze="transition",jt="animation",tn=Symbol("_vtc"),Mo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},na=he({},Ui,Mo),sa=e=>(e.displayName="Transition",e.props=na,e),Bf=sa((e,{slots:t})=>Hs(zl,ra(e),t)),ut=(e,t=[])=>{K(e)?e.forEach(n=>n(...t)):e&&e(...t)},Fr=e=>e?K(e)?e.some(t=>t.length>1):e.length>1:!1;function ra(e){const t={};for(const w in e)w in Mo||(t[w]=e[w]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:f=o,appearToClass:a=l,leaveFromClass:d=`${n}-leave-from`,leaveActiveClass:m=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,_=ia(r),b=_&&_[0],k=_&&_[1],{onBeforeEnter:P,onEnter:D,onEnterCancelled:p,onLeave:g,onLeaveCancelled:O,onBeforeAppear:$=P,onAppear:R=D,onAppearCancelled:j=p}=t,T=(w,F,Y,ie)=>{w._enterCancelled=ie,dt(w,F?a:l),dt(w,F?f:o),Y&&Y()},M=(w,F)=>{w._isLeaving=!1,dt(w,d),dt(w,v),dt(w,m),F&&F()},A=w=>(F,Y)=>{const ie=w?R:D,U=()=>T(F,w,Y);ut(ie,[F,U]),Hr(()=>{dt(F,w?c:i),qe(F,w?a:l),Fr(ie)||Dr(F,s,b,U)})};return he(t,{onBeforeEnter(w){ut(P,[w]),qe(w,i),qe(w,o)},onBeforeAppear(w){ut($,[w]),qe(w,c),qe(w,f)},onEnter:A(!1),onAppear:A(!0),onLeave(w,F){w._isLeaving=!0;const Y=()=>M(w,F);qe(w,d),w._enterCancelled?(qe(w,m),Vr()):(Vr(),qe(w,m)),Hr(()=>{w._isLeaving&&(dt(w,d),qe(w,v),Fr(g)||Dr(w,s,k,Y))}),ut(g,[w,Y])},onEnterCancelled(w){T(w,!1,void 0,!0),ut(p,[w])},onAppearCancelled(w){T(w,!0,void 0,!0),ut(j,[w])},onLeaveCancelled(w){M(w),ut(O,[w])}})}function ia(e){if(e==null)return null;if(se(e))return[cs(e.enter),cs(e.leave)];{const t=cs(e);return[t,t]}}function cs(e){return nl(e)}function qe(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[tn]||(e[tn]=new Set)).add(t)}function dt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[tn];n&&(n.delete(t),n.size||(e[tn]=void 0))}function Hr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let oa=0;function Dr(e,t,n,s){const r=e._endId=++oa,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=la(e,t);if(!o)return s();const f=o+"end";let a=0;const d=()=>{e.removeEventListener(f,m),i()},m=v=>{v.target===e&&++a>=c&&d()};setTimeout(()=>{a(n[_]||"").split(", "),r=s(`${Ze}Delay`),i=s(`${Ze}Duration`),o=$r(r,i),l=s(`${jt}Delay`),c=s(`${jt}Duration`),f=$r(l,c);let a=null,d=0,m=0;t===Ze?o>0&&(a=Ze,d=o,m=i.length):t===jt?f>0&&(a=jt,d=f,m=c.length):(d=Math.max(o,f),a=d>0?o>f?Ze:jt:null,m=a?a===Ze?i.length:c.length:0);const v=a===Ze&&/\b(transform|all)(,|$)/.test(s(`${Ze}Property`).toString());return{type:a,timeout:d,propCount:m,hasTransform:v}}function $r(e,t){for(;e.lengthjr(n)+jr(e[s])))}function jr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Vr(){return document.body.offsetHeight}function ca(e,t,n){const s=e[tn];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const kr=Symbol("_vod"),aa=Symbol("_vsh"),fa=Symbol(""),ua=/(^|;)\s*display\s*:/;function da(e,t,n){const s=e.style,r=oe(n);let i=!1;if(n&&!r){if(t)if(oe(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&An(s,l,"")}else for(const o in t)n[o]==null&&An(s,o,"");for(const o in n)o==="display"&&(i=!0),An(s,o,n[o])}else if(r){if(t!==n){const o=s[fa];o&&(n+=";"+o),s.cssText=n,i=ua.test(n)}}else t&&e.removeAttribute("style");kr in e&&(e[kr]=i?s.display:"",e[aa]&&(s.display="none"))}const Ur=/\s*!important$/;function An(e,t,n){if(K(n))n.forEach(s=>An(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=ha(e,t);Ur.test(n)?e.setProperty(ot(s),n.replace(Ur,""),"important"):e[s]=n}}const Wr=["Webkit","Moz","ms"],as={};function ha(e,t){const n=as[t];if(n)return n;let s=Ne(t);if(s!=="filter"&&s in e)return as[t]=s;s=$n(s);for(let r=0;rfs||(va.then(()=>fs=0),fs=Date.now());function ba(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;$e(_a(s,n.value),t,5,[s])};return n.value=e,n.attached=ya(),n}function _a(e,t){if(K(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Yr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,wa=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?ca(e,s,o):t==="style"?da(e,n,s):nn(t)?js(t)||ga(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Sa(e,t,s,o))?(qr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Kr(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!oe(s))?qr(e,Ne(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Kr(e,t,s,o))};function Sa(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Yr(t)&&q(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Yr(t)&&oe(n)?!1:t in e}const Jr=e=>{const t=e.props["onUpdate:modelValue"]||!1;return K(t)?n=>Tn(t,n):t};function Ta(e){e.target.composing=!0}function zr(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const us=Symbol("_assign"),Kf={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[us]=Jr(r);const i=s||r.props&&r.props.type==="number";At(e,t?"change":"input",o=>{if(o.target.composing)return;let l=e.value;n&&(l=l.trim()),i&&(l=Ss(l)),e[us](l)}),n&&At(e,"change",()=>{e.value=e.value.trim()}),t||(At(e,"compositionstart",Ta),At(e,"compositionend",zr),At(e,"change",zr))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[us]=Jr(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?Ss(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},xa=["ctrl","shift","alt","meta"],Ea={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>xa.some(n=>e[`${n}Key`]&&!t.includes(n))},qf=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=ot(r.key);if(t.some(o=>o===i||Ca[o]===i))return e(r)})},Oo=he({patchProp:wa},ta);let Gt,Qr=!1;function Aa(){return Gt||(Gt=Mc(Oo))}function Ra(){return Gt=Qr?Gt:Oc(Oo),Qr=!0,Gt}const Xf=(...e)=>{const t=Aa().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Lo(s);if(!r)return;const i=t._component;!q(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Po(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t},Yf=(...e)=>{const t=Ra().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Lo(s);if(r)return n(r,!0,Po(r))},t};function Po(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Lo(e){return oe(e)?document.querySelector(e):e}const Ma=window.__VP_SITE_DATA__;function Io(e){return vi()?(ul(e),!0):!1}const ds=new WeakMap,Oa=(...e)=>{var t;const n=e[0],s=(t=ln())==null?void 0:t.proxy;if(s==null&&!no())throw new Error("injectLocal must be called in setup");return s&&ds.has(s)&&n in ds.get(s)?ds.get(s)[n]:bt(...e)},No=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const Jf=e=>e!=null,Pa=Object.prototype.toString,La=e=>Pa.call(e)==="[object Object]",it=()=>{},Zr=Ia();function Ia(){var e,t;return No&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function rr(e,t){function n(...s){return new Promise((r,i)=>{Promise.resolve(e(()=>t.apply(this,s),{fn:t,thisArg:this,args:s})).then(r).catch(i)})}return n}const Fo=e=>e();function Ho(e,t={}){let n,s,r=it;const i=c=>{clearTimeout(c),r(),r=it};let o;return c=>{const f=le(e),a=le(t.maxWait);return n&&i(n),f<=0||a!==void 0&&a<=0?(s&&(i(s),s=null),Promise.resolve(c())):new Promise((d,m)=>{r=t.rejectOnCancel?m:d,o=c,a&&!s&&(s=setTimeout(()=>{n&&i(n),s=null,d(o())},a)),n=setTimeout(()=>{s&&i(s),s=null,d(c())},f)})}}function Na(...e){let t=0,n,s=!0,r=it,i,o,l,c,f;!fe(e[0])&&typeof e[0]=="object"?{delay:o,trailing:l=!0,leading:c=!0,rejectOnCancel:f=!1}=e[0]:[o,l=!0,c=!0,f=!1]=e;const a=()=>{n&&(clearTimeout(n),n=void 0,r(),r=it)};return m=>{const v=le(o),_=Date.now()-t,b=()=>i=m();return a(),v<=0?(t=Date.now(),b()):(_>v&&(c||!s)?(t=Date.now(),b()):l&&(i=new Promise((k,P)=>{r=f?P:k,n=setTimeout(()=>{t=Date.now(),s=!0,k(b()),a()},Math.max(0,v-_))})),!c&&!n&&(n=setTimeout(()=>s=!0,v)),s=!1,i)}}function Fa(e=Fo,t={}){const{initialState:n="active"}=t,s=ir(n==="active");function r(){s.value=!1}function i(){s.value=!0}const o=(...l)=>{s.value&&e(...l)};return{isActive:Un(s),pause:r,resume:i,eventFilter:o}}function ei(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function Ha(e){return ln()}function hs(e){return Array.isArray(e)?e:[e]}function ir(...e){if(e.length!==1)return $l(...e);const t=e[0];return typeof t=="function"?Un(Fl(()=>({get:t,set:it}))):De(t)}function Da(e,t=200,n={}){return rr(Ho(t,n),e)}function $a(e,t=200,n=!1,s=!0,r=!1){return rr(Na(t,n,s,r),e)}function Do(e,t,n={}){const{eventFilter:s=Fo,...r}=n;return Ie(e,rr(s,t),r)}function ja(e,t,n={}){const{eventFilter:s,initialState:r="active",...i}=n,{eventFilter:o,pause:l,resume:c,isActive:f}=Fa(s,{initialState:r});return{stop:Do(e,t,{...i,eventFilter:o}),pause:l,resume:c,isActive:f}}function Yn(e,t=!0,n){Ha()?Ft(e,n):t?e():Wn(e)}function zf(e,t,n={}){const{debounce:s=0,maxWait:r=void 0,...i}=n;return Do(e,t,{...i,eventFilter:Ho(s,{maxWait:r})})}function Va(e,t,n){return Ie(e,t,{...n,immediate:!0})}function Qf(e,t,n){let s;fe(n)?s={evaluating:n}:s={};const{lazy:r=!1,evaluating:i=void 0,shallow:o=!0,onError:l=it}=s,c=xe(!r),f=o?xe(t):De(t);let a=0;return nr(async d=>{if(!c.value)return;a++;const m=a;let v=!1;i&&Promise.resolve().then(()=>{i.value=!0});try{const _=await e(b=>{d(()=>{i&&(i.value=!1),v||b()})});m===a&&(f.value=_)}catch(_){l(_)}finally{i&&m===a&&(i.value=!1),v=!0}}),r?re(()=>(c.value=!0,f.value)):f}const je=No?window:void 0;function or(e){var t;const n=le(e);return(t=n==null?void 0:n.$el)!=null?t:n}function Je(...e){const t=[],n=()=>{t.forEach(l=>l()),t.length=0},s=(l,c,f,a)=>(l.addEventListener(c,f,a),()=>l.removeEventListener(c,f,a)),r=re(()=>{const l=hs(le(e[0])).filter(c=>c!=null);return l.every(c=>typeof c!="string")?l:void 0}),i=Va(()=>{var l,c;return[(c=(l=r.value)==null?void 0:l.map(f=>or(f)))!=null?c:[je].filter(f=>f!=null),hs(le(r.value?e[1]:e[0])),hs(Js(r.value?e[2]:e[1])),le(r.value?e[3]:e[2])]},([l,c,f,a])=>{if(n(),!(l!=null&&l.length)||!(c!=null&&c.length)||!(f!=null&&f.length))return;const d=La(a)?{...a}:a;t.push(...l.flatMap(m=>c.flatMap(v=>f.map(_=>s(m,v,_,d)))))},{flush:"post"}),o=()=>{i(),n()};return Io(n),o}function ka(){const e=xe(!1),t=ln();return t&&Ft(()=>{e.value=!0},t),e}function Ua(e){const t=ka();return re(()=>(t.value,!!e()))}function Wa(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function Zf(...e){let t,n,s={};e.length===3?(t=e[0],n=e[1],s=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,n=e[0],s=e[1]):(t=e[0],n=e[1]):(t=!0,n=e[0]);const{target:r=je,eventName:i="keydown",passive:o=!1,dedupe:l=!1}=s,c=Wa(t);return Je(r,i,a=>{a.repeat&&le(l)||c(a)&&n(a)},o)}const Ba=Symbol("vueuse-ssr-width");function Ka(){const e=no()?Oa(Ba,null):null;return typeof e=="number"?e:void 0}function $o(e,t={}){const{window:n=je,ssrWidth:s=Ka()}=t,r=Ua(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function"),i=xe(typeof s=="number"),o=xe(),l=xe(!1),c=f=>{l.value=f.matches};return nr(()=>{if(i.value){i.value=!r.value;const f=le(e).split(",");l.value=f.some(a=>{const d=a.includes("not all"),m=a.match(/\(\s*min-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/),v=a.match(/\(\s*max-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/);let _=!!(m||v);return m&&_&&(_=s>=ei(m[1])),v&&_&&(_=s<=ei(v[1])),d?!_:_});return}r.value&&(o.value=n.matchMedia(le(e)),l.value=o.value.matches)}),Je(o,"change",c,{passive:!0}),re(()=>l.value)}const yn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},bn="__vueuse_ssr_handlers__",qa=Ga();function Ga(){return bn in yn||(yn[bn]=yn[bn]||{}),yn[bn]}function jo(e,t){return qa[e]||t}function Vo(e){return $o("(prefers-color-scheme: dark)",e)}function Xa(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Ya={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},ti="vueuse-storage";function lr(e,t,n,s={}){var r;const{flush:i="pre",deep:o=!0,listenToStorageChanges:l=!0,writeDefaults:c=!0,mergeDefaults:f=!1,shallow:a,window:d=je,eventFilter:m,onError:v=A=>{console.error(A)},initOnMounted:_}=s,b=(a?xe:De)(typeof t=="function"?t():t),k=re(()=>le(e));if(!n)try{n=jo("getDefaultStorage",()=>{var A;return(A=je)==null?void 0:A.localStorage})()}catch(A){v(A)}if(!n)return b;const P=le(t),D=Xa(P),p=(r=s.serializer)!=null?r:Ya[D],{pause:g,resume:O}=ja(b,()=>R(b.value),{flush:i,deep:o,eventFilter:m});Ie(k,()=>T(),{flush:i}),d&&l&&Yn(()=>{n instanceof Storage?Je(d,"storage",T,{passive:!0}):Je(d,ti,M),_&&T()}),_||T();function $(A,w){if(d){const F={key:k.value,oldValue:A,newValue:w,storageArea:n};d.dispatchEvent(n instanceof Storage?new StorageEvent("storage",F):new CustomEvent(ti,{detail:F}))}}function R(A){try{const w=n.getItem(k.value);if(A==null)$(w,null),n.removeItem(k.value);else{const F=p.write(A);w!==F&&(n.setItem(k.value,F),$(w,F))}}catch(w){v(w)}}function j(A){const w=A?A.newValue:n.getItem(k.value);if(w==null)return c&&P!=null&&n.setItem(k.value,p.write(P)),P;if(!A&&f){const F=p.read(w);return typeof f=="function"?f(F,P):D==="object"&&!Array.isArray(F)?{...P,...F}:F}else return typeof w!="string"?w:p.read(w)}function T(A){if(!(A&&A.storageArea!==n)){if(A&&A.key==null){b.value=P;return}if(!(A&&A.key!==k.value)){g();try{(A==null?void 0:A.newValue)!==p.write(b.value)&&(b.value=j(A))}catch(w){v(w)}finally{A?Wn(O):O()}}}}function M(A){T(A.detail)}return b}const Ja="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function za(e={}){const{selector:t="html",attribute:n="class",initialValue:s="auto",window:r=je,storage:i,storageKey:o="vueuse-color-scheme",listenToStorageChanges:l=!0,storageRef:c,emitAuto:f,disableTransition:a=!0}=e,d={auto:"",light:"light",dark:"dark",...e.modes||{}},m=Vo({window:r}),v=re(()=>m.value?"dark":"light"),_=c||(o==null?ir(s):lr(o,s,i,{window:r,listenToStorageChanges:l})),b=re(()=>_.value==="auto"?v.value:_.value),k=jo("updateHTMLAttrs",(g,O,$)=>{const R=typeof g=="string"?r==null?void 0:r.document.querySelector(g):or(g);if(!R)return;const j=new Set,T=new Set;let M=null;if(O==="class"){const w=$.split(/\s/g);Object.values(d).flatMap(F=>(F||"").split(/\s/g)).filter(Boolean).forEach(F=>{w.includes(F)?j.add(F):T.add(F)})}else M={key:O,value:$};if(j.size===0&&T.size===0&&M===null)return;let A;a&&(A=r.document.createElement("style"),A.appendChild(document.createTextNode(Ja)),r.document.head.appendChild(A));for(const w of j)R.classList.add(w);for(const w of T)R.classList.remove(w);M&&R.setAttribute(M.key,M.value),a&&(r.getComputedStyle(A).opacity,document.head.removeChild(A))});function P(g){var O;k(t,n,(O=d[g])!=null?O:g)}function D(g){e.onChanged?e.onChanged(g,P):P(g)}Ie(b,D,{flush:"post",immediate:!0}),Yn(()=>D(b.value));const p=re({get(){return f?_.value:b.value},set(g){_.value=g}});return Object.assign(p,{store:_,system:v,state:b})}function Qa(e={}){const{valueDark:t="dark",valueLight:n=""}=e,s=za({...e,onChanged:(o,l)=>{var c;e.onChanged?(c=e.onChanged)==null||c.call(e,o==="dark",l,o):l(o)},modes:{dark:t,light:n}}),r=re(()=>s.system.value);return re({get(){return s.value==="dark"},set(o){const l=o?"dark":"light";r.value===l?s.value="auto":s.value=l}})}function ps(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}const ni=1;function Za(e,t={}){const{throttle:n=0,idle:s=200,onStop:r=it,onScroll:i=it,offset:o={left:0,right:0,top:0,bottom:0},eventListenerOptions:l={capture:!1,passive:!0},behavior:c="auto",window:f=je,onError:a=R=>{console.error(R)}}=t,d=xe(0),m=xe(0),v=re({get(){return d.value},set(R){b(R,void 0)}}),_=re({get(){return m.value},set(R){b(void 0,R)}});function b(R,j){var T,M,A,w;if(!f)return;const F=le(e);if(!F)return;(A=F instanceof Document?f.document.body:F)==null||A.scrollTo({top:(T=le(j))!=null?T:_.value,left:(M=le(R))!=null?M:v.value,behavior:le(c)});const Y=((w=F==null?void 0:F.document)==null?void 0:w.documentElement)||(F==null?void 0:F.documentElement)||F;v!=null&&(d.value=Y.scrollLeft),_!=null&&(m.value=Y.scrollTop)}const k=xe(!1),P=It({left:!0,right:!1,top:!0,bottom:!1}),D=It({left:!1,right:!1,top:!1,bottom:!1}),p=R=>{k.value&&(k.value=!1,D.left=!1,D.right=!1,D.top=!1,D.bottom=!1,r(R))},g=Da(p,n+s),O=R=>{var j;if(!f)return;const T=((j=R==null?void 0:R.document)==null?void 0:j.documentElement)||(R==null?void 0:R.documentElement)||or(R),{display:M,flexDirection:A,direction:w}=getComputedStyle(T),F=w==="rtl"?-1:1,Y=T.scrollLeft;D.left=Yd.value;const ie=Math.abs(Y*F)<=(o.left||0),U=Math.abs(Y*F)+T.clientWidth>=T.scrollWidth-(o.right||0)-ni;M==="flex"&&A==="row-reverse"?(P.left=U,P.right=ie):(P.left=ie,P.right=U),d.value=Y;let X=T.scrollTop;R===f.document&&!X&&(X=f.document.body.scrollTop),D.top=Xm.value;const V=Math.abs(X)<=(o.top||0),ae=Math.abs(X)+T.clientHeight>=T.scrollHeight-(o.bottom||0)-ni;M==="flex"&&A==="column-reverse"?(P.top=ae,P.bottom=V):(P.top=V,P.bottom=ae),m.value=X},$=R=>{var j;if(!f)return;const T=(j=R.target.documentElement)!=null?j:R.target;O(T),k.value=!0,g(R),i(R)};return Je(e,"scroll",n?$a($,n,!0,!1):$,l),Yn(()=>{try{const R=le(e);if(!R)return;O(R)}catch(R){a(R)}}),Je(e,"scrollend",p,l),{x:v,y:_,isScrolling:k,arrivedState:P,directions:D,measure(){const R=le(e);f&&R&&O(R)}}}function eu(e,t,n={}){const{window:s=je}=n;return lr(e,t,s==null?void 0:s.localStorage,n)}function ko(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const gs=new WeakMap;function tu(e,t=!1){const n=xe(t);let s=null,r="";Ie(ir(e),l=>{const c=ps(le(l));if(c){const f=c;if(gs.get(f)||gs.set(f,f.style.overflow),f.style.overflow!=="hidden"&&(r=f.style.overflow),f.style.overflow==="hidden")return n.value=!0;if(n.value)return f.style.overflow="hidden"}},{immediate:!0});const i=()=>{const l=ps(le(e));!l||n.value||(Zr&&(s=Je(l,"touchmove",c=>{ef(c)},{passive:!1})),l.style.overflow="hidden",n.value=!0)},o=()=>{const l=ps(le(e));!l||!n.value||(Zr&&(s==null||s()),l.style.overflow=r,gs.delete(l),n.value=!1)};return Io(o),re({get(){return n.value},set(l){l?i():o()}})}function nu(e,t,n={}){const{window:s=je}=n;return lr(e,t,s==null?void 0:s.sessionStorage,n)}function su(e={}){const{window:t=je,...n}=e;return Za(t,n)}function ru(e={}){const{window:t=je,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:s=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:i=!0,type:o="inner"}=e,l=xe(n),c=xe(s),f=()=>{if(t)if(o==="outer")l.value=t.outerWidth,c.value=t.outerHeight;else if(o==="visual"&&t.visualViewport){const{width:d,height:m,scale:v}=t.visualViewport;l.value=Math.round(d*v),c.value=Math.round(m*v)}else i?(l.value=t.innerWidth,c.value=t.innerHeight):(l.value=t.document.documentElement.clientWidth,c.value=t.document.documentElement.clientHeight)};f(),Yn(f);const a={passive:!0};if(Je("resize",f,a),t&&o==="visual"&&t.visualViewport&&Je(t.visualViewport,"resize",f,a),r){const d=$o("(orientation: portrait)");Ie(d,()=>f())}return{width:l,height:c}}const ms={};var vs={};const Uo=/^(?:[a-z]+:|\/\/)/i,tf="vitepress-theme-appearance",nf=/#.*$/,sf=/[?#].*$/,rf=/(?:(^|\/)index)?\.(?:md|html)$/,ge=typeof document<"u",Wo={relativePath:"404.md",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function of(e,t,n=!1){if(t===void 0)return!1;if(e=si(`/${e}`),n)return new RegExp(t).test(e);if(si(t)!==e)return!1;const s=t.match(nf);return s?(ge?location.hash:"")===s[0]:!0}function si(e){return decodeURI(e).replace(sf,"").replace(rf,"$1")}function lf(e){return Uo.test(e)}function cf(e,t){return Object.keys((e==null?void 0:e.locales)||{}).find(n=>n!=="root"&&!lf(n)&&of(t,`/${n}/`,!0))||"root"}function af(e,t){var s,r,i,o,l,c,f;const n=cf(e,t);return Object.assign({},e,{localeIndex:n,lang:((s=e.locales[n])==null?void 0:s.lang)??e.lang,dir:((r=e.locales[n])==null?void 0:r.dir)??e.dir,title:((i=e.locales[n])==null?void 0:i.title)??e.title,titleTemplate:((o=e.locales[n])==null?void 0:o.titleTemplate)??e.titleTemplate,description:((l=e.locales[n])==null?void 0:l.description)??e.description,head:Ko(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(f=e.locales[n])==null?void 0:f.themeConfig}})}function Bo(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const r=ff(e.title,s);return n===r.slice(3)?n:`${n}${r}`}function ff(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function uf(e,t){const[n,s]=t;if(n!=="meta")return!1;const r=Object.entries(s)[0];return r==null?!1:e.some(([i,o])=>i===n&&o[r[0]]===r[1])}function Ko(e,t){return[...e.filter(n=>!uf(t,n)),...t]}const df=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,hf=/^[a-z]:/i;function ri(e){const t=hf.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(df,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const ys=new Set;function pf(e){if(ys.size===0){const n=typeof process=="object"&&(vs==null?void 0:vs.VITE_EXTRA_EXTENSIONS)||(ms==null?void 0:ms.VITE_EXTRA_EXTENSIONS)||"";("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip"+(n&&typeof n=="string"?","+n:"")).split(",").forEach(s=>ys.add(s))}const t=e.split(".").pop();return t==null||!ys.has(t.toLowerCase())}function iu(e){return e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d")}const gf=Symbol(),wt=xe(Ma);function ou(e){const t=re(()=>af(wt.value,e.data.relativePath)),n=t.value.appearance,s=n==="force-dark"?De(!0):n==="force-auto"?Vo():n?Qa({storageKey:tf,initialValue:()=>n==="dark"?"dark":"auto",...typeof n=="object"?n:{}}):De(!1),r=De(ge?location.hash:"");return ge&&window.addEventListener("hashchange",()=>{r.value=location.hash}),Ie(()=>e.data,()=>{r.value=ge?location.hash:""}),{site:t,theme:re(()=>t.value.themeConfig),page:re(()=>e.data),frontmatter:re(()=>e.data.frontmatter),params:re(()=>e.data.params),lang:re(()=>t.value.lang),dir:re(()=>e.data.frontmatter.dir||t.value.dir),localeIndex:re(()=>t.value.localeIndex||"root"),title:re(()=>Bo(t.value,e.data)),description:re(()=>e.data.description||t.value.description),isDark:s,hash:re(()=>r.value)}}function mf(){const e=bt(gf);if(!e)throw new Error("vitepress data not properly injected in app");return e}function vf(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function ii(e){return Uo.test(e)||!e.startsWith("/")?e:vf(wt.value.base,e)}function yf(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),ge){const n="/bioloop/docs/";t=ri(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let s=__VP_HASH_MAP__[t.toLowerCase()];if(s||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",s=__VP_HASH_MAP__[t.toLowerCase()]),!s)return null;t=`${n}assets/${t}.${s}.js`}else t=`./${ri(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let Rn=[];function lu(e){Rn.push(e),Kn(()=>{Rn=Rn.filter(t=>t!==e)})}function bf(){let e=wt.value.scrollOffset,t=0,n=24;if(typeof e=="object"&&"padding"in e&&(n=e.padding,e=e.selector),typeof e=="number")t=e;else if(typeof e=="string")t=oi(e,n);else if(Array.isArray(e))for(const s of e){const r=oi(s,n);if(r){t=r;break}}return t}function oi(e,t){const n=document.querySelector(e);if(!n)return 0;const s=n.getBoundingClientRect().bottom;return s<0?0:s+t}const _f=Symbol(),qo="http://a.com",wf=()=>({path:"/",component:null,data:Wo});function cu(e,t){const n=It(wf()),s={route:n,go:r};async function r(l=ge?location.href:"/"){var c,f;l=bs(l),await((c=s.onBeforeRouteChange)==null?void 0:c.call(s,l))!==!1&&(ge&&l!==bs(location.href)&&(history.replaceState({scrollPosition:window.scrollY},""),history.pushState({},"",l)),await o(l),await((f=s.onAfterRouteChange??s.onAfterRouteChanged)==null?void 0:f(l)))}let i=null;async function o(l,c=0,f=!1){var m,v;if(await((m=s.onBeforePageLoad)==null?void 0:m.call(s,l))===!1)return;const a=new URL(l,qo),d=i=a.pathname;try{let _=await e(d);if(!_)throw new Error(`Page not found: ${d}`);if(i===d){i=null;const{default:b,__pageData:k}=_;if(!b)throw new Error(`Invalid route component: ${b}`);await((v=s.onAfterPageLoad)==null?void 0:v.call(s,l)),n.path=ge?d:ii(d),n.component=xn(b),n.data=xn(k),ge&&Wn(()=>{let P=wt.value.base+k.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!wt.value.cleanUrls&&!P.endsWith("/")&&(P+=".html"),P!==a.pathname&&(a.pathname=P,l=P+a.search+a.hash,history.replaceState({},"",l)),a.hash&&!c){let D=null;try{D=document.getElementById(decodeURIComponent(a.hash).slice(1))}catch(p){console.warn(p)}if(D){li(D,a.hash);return}}window.scrollTo(0,c)})}}catch(_){if(!/fetch|Page not found/.test(_.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(_),!f)try{const b=await fetch(wt.value.base+"hashmap.json");window.__VP_HASH_MAP__=await b.json(),await o(l,c,!0);return}catch{}if(i===d){i=null,n.path=ge?d:ii(d),n.component=t?xn(t):null;const b=ge?d.replace(/(^|\/)$/,"$1index").replace(/(\.html)?$/,".md").replace(/^\//,""):"404.md";n.data={...Wo,relativePath:b}}}}return ge&&(history.state===null&&history.replaceState({},""),window.addEventListener("click",l=>{if(l.defaultPrevented||!(l.target instanceof Element)||l.target.closest("button")||l.button!==0||l.ctrlKey||l.shiftKey||l.altKey||l.metaKey)return;const c=l.target.closest("a");if(!c||c.closest(".vp-raw")||c.hasAttribute("download")||c.hasAttribute("target"))return;const f=c.getAttribute("href")??(c instanceof SVGAElement?c.getAttribute("xlink:href"):null);if(f==null)return;const{href:a,origin:d,pathname:m,hash:v,search:_}=new URL(f,c.baseURI),b=new URL(location.href);d===b.origin&&pf(m)&&(l.preventDefault(),m===b.pathname&&_===b.search?(v!==b.hash&&(history.pushState({},"",a),window.dispatchEvent(new HashChangeEvent("hashchange",{oldURL:b.href,newURL:a}))),v?li(c,v,c.classList.contains("header-anchor")):window.scrollTo(0,0)):r(a))},{capture:!0}),window.addEventListener("popstate",async l=>{var f;if(l.state===null)return;const c=bs(location.href);await o(c,l.state&&l.state.scrollPosition||0),await((f=s.onAfterRouteChange??s.onAfterRouteChanged)==null?void 0:f(c))}),window.addEventListener("hashchange",l=>{l.preventDefault()})),s}function Sf(){const e=bt(_f);if(!e)throw new Error("useRouter() is called without provider.");return e}function Go(){return Sf().route}function li(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(r){console.warn(r)}if(s){let r=function(){!n||Math.abs(o-window.scrollY)>window.innerHeight?window.scrollTo(0,o):window.scrollTo({left:0,top:o,behavior:"smooth"})};const i=parseInt(window.getComputedStyle(s).paddingTop,10),o=window.scrollY+s.getBoundingClientRect().top-bf()+i;requestAnimationFrame(r)}}function bs(e){const t=new URL(e,qo);return t.pathname=t.pathname.replace(/(^|\/)index(\.html)?$/,"$1"),wt.value.cleanUrls?t.pathname=t.pathname.replace(/\.html$/,""):!t.pathname.endsWith("/")&&!t.pathname.endsWith(".html")&&(t.pathname+=".html"),t.pathname+t.search+t.hash}const _n=()=>Rn.forEach(e=>e()),au=Qs({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=Go(),{frontmatter:n,site:s}=mf();return Ie(n,_n,{deep:!0,flush:"post"}),()=>Hs(e.as,s.value.contentProps??{style:{position:"relative"}},[t.component?Hs(t.component,{onVnodeMounted:_n,onVnodeUpdated:_n,onVnodeUnmounted:_n}):"404 Page Not Found"])}}),fu=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Tf="modulepreload",xf=function(e){return"/bioloop/docs/"+e},ci={},uu=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(n.map(c=>{if(c=xf(c),c in ci)return;ci[c]=!0;const f=c.endsWith(".css"),a=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${a}`))return;const d=document.createElement("link");if(d.rel=f?"stylesheet":Tf,f||(d.as="script"),d.crossOrigin="",d.href=c,l&&d.setAttribute("nonce",l),document.head.appendChild(d),f)return new Promise((m,v)=>{d.addEventListener("load",m),d.addEventListener("error",()=>v(new Error(`Unable to preload CSS for ${c}`)))})}))}function i(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return r.then(o=>{for(const l of o||[])l.status==="rejected"&&i(l.reason);return t().catch(i)})},du=Qs({setup(e,{slots:t}){const n=De(!1);return Ft(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function hu(){ge&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const s=(n=t.parentElement)==null?void 0:n.parentElement;if(!s)return;const r=Array.from(s.querySelectorAll("input")).indexOf(t);if(r<0)return;const i=s.querySelector(".blocks");if(!i)return;const o=Array.from(i.children).find(f=>f.classList.contains("active"));if(!o)return;const l=i.children[r];if(!l||o===l)return;o.classList.remove("active"),l.classList.add("active");const c=s==null?void 0:s.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function pu(){if(ge){const e=new WeakMap;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,i=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!r||!i)return;const o=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className),l=[".vp-copy-ignore",".diff.remove"],c=i.cloneNode(!0);c.querySelectorAll(l.join(",")).forEach(a=>a.remove());let f=c.textContent||"";o&&(f=f.replace(/^ *(\$|>) /gm,"").trim()),Ef(f).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const a=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,a)})}})}}async function Ef(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),r=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),r&&(s.removeAllRanges(),s.addRange(r)),n&&n.focus()}}function gu(e,t){let n=!0,s=[];const r=i=>{if(n){n=!1,i.forEach(l=>{const c=_s(l);for(const f of document.head.children)if(f.isEqualNode(c)){s.push(f);return}});return}const o=i.map(_s);s.forEach((l,c)=>{const f=o.findIndex(a=>a==null?void 0:a.isEqualNode(l??null));f!==-1?delete o[f]:(l==null||l.remove(),delete s[c])}),o.forEach(l=>l&&document.head.appendChild(l)),s=[...s,...o].filter(Boolean)};nr(()=>{const i=e.data,o=t.value,l=i&&i.description,c=i&&i.frontmatter.head||[],f=Bo(o,i);f!==document.title&&(document.title=f);const a=l||o.description;let d=document.querySelector("meta[name=description]");d?d.getAttribute("content")!==a&&d.setAttribute("content",a):_s(["meta",{name:"description",content:a}]),r(Ko(o.head,Af(c)))})}function _s([e,t,n]){const s=document.createElement(e);for(const r in t)s.setAttribute(r,t[r]);return n&&(s.innerHTML=n),e==="script"&&t.async==null&&(s.async=!1),s}function Cf(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function Af(e){return e.filter(t=>!Cf(t))}const ws=new Set,Xo=()=>document.createElement("link"),Rf=e=>{const t=Xo();t.rel="prefetch",t.href=e,document.head.appendChild(t)},Mf=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let wn;const Of=ge&&(wn=Xo())&&wn.relList&&wn.relList.supports&&wn.relList.supports("prefetch")?Rf:Mf;function mu(){if(!ge||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(o=>{if(o.isIntersecting){const l=o.target;n.unobserve(l);const{pathname:c}=l;if(!ws.has(c)){ws.add(c);const f=yf(c);f&&Of(f)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{hostname:o,pathname:l}=new URL(i.href instanceof SVGAnimatedString?i.href.animVal:i.href,i.baseURI),c=l.match(/\.\w+$/);c&&c[0]!==".html"||i.target!=="_blank"&&o===location.hostname&&(l!==location.pathname?n.observe(i):ws.add(l))})})};Ft(s);const r=Go();Ie(()=>r.path,s),Kn(()=>{n&&n.disconnect()})}export{Xi as $,bf as A,Hf as B,Nf as C,xe as D,lu as E,Te as F,ce as G,Ff as H,Uo as I,Go as J,Wc as K,bt as L,ru as M,Us as N,Zf as O,Wn as P,su as Q,ge as R,Un as S,Bf as T,If as U,uu as V,tu as W,Sc as X,$f as Y,Gf as Z,fu as _,xo as a,qf as a0,jf as a1,gu as a2,_f as a3,ou as a4,gf as a5,au as a6,du as a7,wt as a8,cu as a9,yf as aa,Yf as ab,mu as ac,pu as ad,hu as ae,Hs as af,Uf as ag,le as ah,hs as ai,or as aj,Jf as ak,Io as al,Qf as am,nu as an,eu as ao,zf as ap,Sf as aq,Je as ar,Pf as as,Kf as at,fe as au,Lf as av,xn as aw,Xf as ax,iu as ay,Ns as b,kf as c,Qs as d,Wf as e,pf as f,ii as g,re as h,lf as i,To as j,Js as k,of as l,$o as m,Ws as n,Is as o,De as p,Ie as q,Df as r,nr as s,al as t,mf as u,Ft as v,Gl as w,Kn as x,Vf as y,cc as z}; diff --git a/docs/.vitepress/dist/assets/chunks/framework.MXVb71fM.js b/docs/.vitepress/dist/assets/chunks/framework.MXVb71fM.js deleted file mode 100644 index eea208b7c..000000000 --- a/docs/.vitepress/dist/assets/chunks/framework.MXVb71fM.js +++ /dev/null @@ -1,17 +0,0 @@ -/** -* @vue/shared v3.4.15 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/function gs(e,t){const n=new Set(e.split(","));return t?s=>n.has(s.toLowerCase()):s=>n.has(s)}const te={},mt=[],xe=()=>{},ii=()=>!1,kt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ms=e=>e.startsWith("onUpdate:"),oe=Object.assign,_s=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},li=Object.prototype.hasOwnProperty,X=(e,t)=>li.call(e,t),k=Array.isArray,_t=e=>bn(e)==="[object Map]",Nr=e=>bn(e)==="[object Set]",U=e=>typeof e=="function",ne=e=>typeof e=="string",xt=e=>typeof e=="symbol",Z=e=>e!==null&&typeof e=="object",Fr=e=>(Z(e)||U(e))&&U(e.then)&&U(e.catch),$r=Object.prototype.toString,bn=e=>$r.call(e),ci=e=>bn(e).slice(8,-1),Hr=e=>bn(e)==="[object Object]",ys=e=>ne(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ot=gs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),vn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ai=/-(\w)/g,Me=vn(e=>e.replace(ai,(t,n)=>n?n.toUpperCase():"")),ui=/\B([A-Z])/g,at=vn(e=>e.replace(ui,"-$1").toLowerCase()),wn=vn(e=>e.charAt(0).toUpperCase()+e.slice(1)),rn=vn(e=>e?`on${wn(e)}`:""),Qe=(e,t)=>!Object.is(e,t),Vn=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},fi=e=>{const t=parseFloat(e);return isNaN(t)?e:t},di=e=>{const t=ne(e)?Number(e):NaN;return isNaN(t)?e:t};let Bs;const jr=()=>Bs||(Bs=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function bs(e){if(k(e)){const t={};for(let n=0;n{if(n){const s=n.split(pi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function vs(e){let t="";if(ne(e))t=e;else if(k(e))for(let n=0;nne(e)?e:e==null?"":k(e)||Z(e)&&(e.toString===$r||!U(e.toString))?JSON.stringify(e,Dr,2):String(e),Dr=(e,t)=>t&&t.__v_isRef?Dr(e,t.value):_t(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],o)=>(n[Dn(s,o)+" =>"]=r,n),{})}:Nr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Dn(n))}:xt(t)?Dn(t):Z(t)&&!k(t)&&!Hr(t)?String(t):t,Dn=(e,t="")=>{var n;return xt(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** -* @vue/reactivity v3.4.15 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/let be;class bi{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=be,!t&&be&&(this.index=(be.scopes||(be.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=be;try{return be=this,t()}finally{be=n}}}on(){be=this}off(){be=this.parent}stop(t){if(this._active){let n,s;for(n=0,s=this.effects.length;n=2))break}this._dirtyLevel<2&&(this._dirtyLevel=0),ft()}return this._dirtyLevel>=2}set dirty(t){this._dirtyLevel=t?2:0}run(){if(this._dirtyLevel=0,!this.active)return this.fn();let t=Ge,n=it;try{return Ge=!0,it=this,this._runnings++,Us(this),this.fn()}finally{Ks(this),this._runnings--,it=n,Ge=t}}stop(){var t;this.active&&(Us(this),Ks(this),(t=this.onStop)==null||t.call(this),this.active=!1)}}function Ei(e){return e.value}function Us(e){e._trackId++,e._depsLength=0}function Ks(e){if(e.deps&&e.deps.length>e._depsLength){for(let t=e._depsLength;t{const n=new Map;return n.cleanup=e,n.computed=t,n},an=new WeakMap,lt=Symbol(""),ss=Symbol("");function _e(e,t,n){if(Ge&&it){let s=an.get(e);s||an.set(e,s=new Map);let r=s.get(n);r||s.set(n,r=Gr(()=>s.delete(n))),Kr(it,r)}}function $e(e,t,n,s,r,o){const i=an.get(e);if(!i)return;let l=[];if(t==="clear")l=[...i.values()];else if(n==="length"&&k(e)){const c=Number(s);i.forEach((u,d)=>{(d==="length"||!xt(d)&&d>=c)&&l.push(u)})}else switch(n!==void 0&&l.push(i.get(n)),t){case"add":k(e)?ys(n)&&l.push(i.get("length")):(l.push(i.get(lt)),_t(e)&&l.push(i.get(ss)));break;case"delete":k(e)||(l.push(i.get(lt)),_t(e)&&l.push(i.get(ss)));break;case"set":_t(e)&&l.push(i.get(lt));break}Es();for(const c of l)c&&Wr(c,2);Cs()}function Ci(e,t){var n;return(n=an.get(e))==null?void 0:n.get(t)}const xi=gs("__proto__,__v_isRef,__isVue"),zr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(xt)),Ws=Si();function Si(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=Y(this);for(let o=0,i=this.length;o{e[t]=function(...n){ut(),Es();const s=Y(this)[t].apply(this,n);return Cs(),ft(),s}}),e}function Ti(e){const t=Y(this);return _e(t,"has",e),t.hasOwnProperty(e)}class Xr{constructor(t=!1,n=!1){this._isReadonly=t,this._shallow=n}get(t,n,s){const r=this._isReadonly,o=this._shallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return s===(r?o?Vi:Zr:o?Qr:Jr).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const i=k(t);if(!r){if(i&&X(Ws,n))return Reflect.get(Ws,n,s);if(n==="hasOwnProperty")return Ti}const l=Reflect.get(t,n,s);return(xt(n)?zr.has(n):xi(n))||(r||_e(t,"get",n),o)?l:de(l)?i&&ys(n)?l:l.value:Z(l)?r?xn(l):Cn(l):l}}class Yr extends Xr{constructor(t=!1){super(!1,t)}set(t,n,s,r){let o=t[n];if(!this._shallow){const c=Et(o);if(!un(s)&&!Et(s)&&(o=Y(o),s=Y(s)),!k(t)&&de(o)&&!de(s))return c?!1:(o.value=s,!0)}const i=k(t)&&ys(n)?Number(n)e,En=e=>Reflect.getPrototypeOf(e);function Wt(e,t,n=!1,s=!1){e=e.__v_raw;const r=Y(e),o=Y(t);n||(Qe(t,o)&&_e(r,"get",t),_e(r,"get",o));const{has:i}=En(r),l=s?xs:n?As:$t;if(i.call(r,t))return l(e.get(t));if(i.call(r,o))return l(e.get(o));e!==r&&e.get(t)}function qt(e,t=!1){const n=this.__v_raw,s=Y(n),r=Y(e);return t||(Qe(e,r)&&_e(s,"has",e),_e(s,"has",r)),e===r?n.has(e):n.has(e)||n.has(r)}function Gt(e,t=!1){return e=e.__v_raw,!t&&_e(Y(e),"iterate",lt),Reflect.get(e,"size",e)}function qs(e){e=Y(e);const t=Y(this);return En(t).has.call(t,e)||(t.add(e),$e(t,"add",e,e)),this}function Gs(e,t){t=Y(t);const n=Y(this),{has:s,get:r}=En(n);let o=s.call(n,e);o||(e=Y(e),o=s.call(n,e));const i=r.call(n,e);return n.set(e,t),o?Qe(t,i)&&$e(n,"set",e,t):$e(n,"add",e,t),this}function zs(e){const t=Y(this),{has:n,get:s}=En(t);let r=n.call(t,e);r||(e=Y(e),r=n.call(t,e)),s&&s.call(t,e);const o=t.delete(e);return r&&$e(t,"delete",e,void 0),o}function Xs(){const e=Y(this),t=e.size!==0,n=e.clear();return t&&$e(e,"clear",void 0,void 0),n}function zt(e,t){return function(s,r){const o=this,i=o.__v_raw,l=Y(i),c=t?xs:e?As:$t;return!e&&_e(l,"iterate",lt),i.forEach((u,d)=>s.call(r,c(u),c(d),o))}}function Xt(e,t,n){return function(...s){const r=this.__v_raw,o=Y(r),i=_t(o),l=e==="entries"||e===Symbol.iterator&&i,c=e==="keys"&&i,u=r[e](...s),d=n?xs:t?As:$t;return!t&&_e(o,"iterate",c?ss:lt),{next(){const{value:h,done:m}=u.next();return m?{value:h,done:m}:{value:l?[d(h[0]),d(h[1])]:d(h),done:m}},[Symbol.iterator](){return this}}}}function Ve(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Ii(){const e={get(o){return Wt(this,o)},get size(){return Gt(this)},has:qt,add:qs,set:Gs,delete:zs,clear:Xs,forEach:zt(!1,!1)},t={get(o){return Wt(this,o,!1,!0)},get size(){return Gt(this)},has:qt,add:qs,set:Gs,delete:zs,clear:Xs,forEach:zt(!1,!0)},n={get(o){return Wt(this,o,!0)},get size(){return Gt(this,!0)},has(o){return qt.call(this,o,!0)},add:Ve("add"),set:Ve("set"),delete:Ve("delete"),clear:Ve("clear"),forEach:zt(!0,!1)},s={get(o){return Wt(this,o,!0,!0)},get size(){return Gt(this,!0)},has(o){return qt.call(this,o,!0)},add:Ve("add"),set:Ve("set"),delete:Ve("delete"),clear:Ve("clear"),forEach:zt(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Xt(o,!1,!1),n[o]=Xt(o,!0,!1),t[o]=Xt(o,!1,!0),s[o]=Xt(o,!0,!0)}),[e,n,t,s]}const[Pi,Mi,Ni,Fi]=Ii();function Ss(e,t){const n=t?e?Fi:Ni:e?Mi:Pi;return(s,r,o)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(X(n,r)&&r in s?n:s,r,o)}const $i={get:Ss(!1,!1)},Hi={get:Ss(!1,!0)},ji={get:Ss(!0,!1)},Jr=new WeakMap,Qr=new WeakMap,Zr=new WeakMap,Vi=new WeakMap;function Di(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function ki(e){return e.__v_skip||!Object.isExtensible(e)?0:Di(ci(e))}function Cn(e){return Et(e)?e:Ts(e,!1,Ri,$i,Jr)}function Bi(e){return Ts(e,!1,Li,Hi,Qr)}function xn(e){return Ts(e,!0,Oi,ji,Zr)}function Ts(e,t,n,s,r){if(!Z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const o=r.get(e);if(o)return o;const i=ki(e);if(i===0)return e;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function yt(e){return Et(e)?yt(e.__v_raw):!!(e&&e.__v_isReactive)}function Et(e){return!!(e&&e.__v_isReadonly)}function un(e){return!!(e&&e.__v_isShallow)}function eo(e){return yt(e)||Et(e)}function Y(e){const t=e&&e.__v_raw;return t?Y(t):e}function Lt(e){return cn(e,"__v_skip",!0),e}const $t=e=>Z(e)?Cn(e):e,As=e=>Z(e)?xn(e):e;class to{constructor(t,n,s,r){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this.effect=new ws(()=>t(this._value),()=>It(this,1),()=>this.dep&&qr(this.dep)),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=s}get value(){const t=Y(this);return(!t._cacheable||t.effect.dirty)&&Qe(t._value,t._value=t.effect.run())&&It(t,2),Rs(t),t.effect._dirtyLevel>=1&&It(t,1),t._value}set value(t){this._setter(t)}get _dirty(){return this.effect.dirty}set _dirty(t){this.effect.dirty=t}}function Ui(e,t,n=!1){let s,r;const o=U(e);return o?(s=e,r=xe):(s=e.get,r=e.set),new to(s,r,o||!r,n)}function Rs(e){Ge&&it&&(e=Y(e),Kr(it,e.dep||(e.dep=Gr(()=>e.dep=void 0,e instanceof to?e:void 0))))}function It(e,t=2,n){e=Y(e);const s=e.dep;s&&Wr(s,t)}function de(e){return!!(e&&e.__v_isRef===!0)}function me(e){return so(e,!1)}function no(e){return so(e,!0)}function so(e,t){return de(e)?e:new Ki(e,t)}class Ki{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:Y(t),this._value=n?t:$t(t)}get value(){return Rs(this),this._value}set value(t){const n=this.__v_isShallow||un(t)||Et(t);t=n?t:Y(t),Qe(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:$t(t),It(this,2))}}function ro(e){return de(e)?e.value:e}const Wi={get:(e,t,n)=>ro(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return de(r)&&!de(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function oo(e){return yt(e)?e:new Proxy(e,Wi)}class qi{constructor(t){this.dep=void 0,this.__v_isRef=!0;const{get:n,set:s}=t(()=>Rs(this),()=>It(this));this._get=n,this._set=s}get value(){return this._get()}set value(t){this._set(t)}}function Gi(e){return new qi(e)}class zi{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return Ci(Y(this._object),this._key)}}class Xi{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function Yi(e,t,n){return de(e)?e:U(e)?new Xi(e):Z(e)&&arguments.length>1?Ji(e,t,n):me(e)}function Ji(e,t,n){const s=e[t];return de(s)?s:new zi(e,t,n)}/** -* @vue/runtime-core v3.4.15 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/function ze(e,t,n,s){let r;try{r=s?e(...s):e()}catch(o){Sn(o,t,n)}return r}function Se(e,t,n,s){if(U(e)){const o=ze(e,t,n,s);return o&&Fr(o)&&o.catch(i=>{Sn(i,t,n)}),o}const r=[];for(let o=0;o>>1,r=ue[s],o=jt(r);oPe&&ue.splice(t,1)}function tl(e){k(e)?bt.push(...e):(!Ue||!Ue.includes(e,e.allowRecurse?rt+1:rt))&&bt.push(e),lo()}function Ys(e,t,n=Ht?Pe+1:0){for(;njt(n)-jt(s));if(bt.length=0,Ue){Ue.push(...t);return}for(Ue=t,rt=0;rte.id==null?1/0:e.id,nl=(e,t)=>{const n=jt(e)-jt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function co(e){rs=!1,Ht=!0,ue.sort(nl);try{for(Pe=0;Pene(w)?w.trim():w)),h&&(r=n.map(fi))}let l,c=s[l=rn(t)]||s[l=rn(Me(t))];!c&&o&&(c=s[l=rn(at(t))]),c&&Se(c,e,6,r);const u=s[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Se(u,e,6,r)}}function ao(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const o=e.emits;let i={},l=!1;if(!U(e)){const c=u=>{const d=ao(u,t,!0);d&&(l=!0,oe(i,d))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!o&&!l?(Z(e)&&s.set(e,null),null):(k(o)?o.forEach(c=>i[c]=null):oe(i,o),Z(e)&&s.set(e,i),i)}function An(e,t){return!e||!kt(t)?!1:(t=t.slice(2).replace(/Once$/,""),X(e,t[0].toLowerCase()+t.slice(1))||X(e,at(t))||X(e,t))}let fe=null,Rn=null;function dn(e){const t=fe;return fe=e,Rn=e&&e.type.__scopeId||null,t}function $a(e){Rn=e}function Ha(){Rn=null}function rl(e,t=fe,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&cr(-1);const o=dn(t);let i;try{i=e(...r)}finally{dn(o),s._d&&cr(1)}return i};return s._n=!0,s._c=!0,s._d=!0,s}function kn(e){const{type:t,vnode:n,proxy:s,withProxy:r,props:o,propsOptions:[i],slots:l,attrs:c,emit:u,render:d,renderCache:h,data:m,setupState:w,ctx:L,inheritAttrs:M}=e;let F,W;const J=dn(e);try{if(n.shapeFlag&4){const _=r||s,N=_;F=Ae(d.call(N,_,h,o,w,m,L)),W=c}else{const _=t;F=Ae(_.length>1?_(o,{attrs:c,slots:l,emit:u}):_(o,null)),W=t.props?c:ol(c)}}catch(_){Nt.length=0,Sn(_,e,1),F=ae(ve)}let p=F;if(W&&M!==!1){const _=Object.keys(W),{shapeFlag:N}=p;_.length&&N&7&&(i&&_.some(ms)&&(W=il(W,i)),p=Ze(p,W))}return n.dirs&&(p=Ze(p),p.dirs=p.dirs?p.dirs.concat(n.dirs):n.dirs),n.transition&&(p.transition=n.transition),F=p,dn(J),F}const ol=e=>{let t;for(const n in e)(n==="class"||n==="style"||kt(n))&&((t||(t={}))[n]=e[n]);return t},il=(e,t)=>{const n={};for(const s in e)(!ms(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function ll(e,t,n){const{props:s,children:r,component:o}=e,{props:i,children:l,patchFlag:c}=t,u=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Js(s,i,u):!!i;if(c&8){const d=t.dynamicProps;for(let h=0;he.__isSuspense;function ho(e,t){t&&t.pendingBranch?k(e)?t.effects.push(...e):t.effects.push(e):tl(e)}const ul=Symbol.for("v-scx"),fl=()=>wt(ul);function po(e,t){return On(e,null,t)}function Da(e,t){return On(e,null,{flush:"post"})}const Yt={};function Xe(e,t,n){return On(e,t,n)}function On(e,t,{immediate:n,deep:s,flush:r,once:o,onTrack:i,onTrigger:l}=te){if(t&&o){const I=t;t=(...D)=>{I(...D),N()}}const c=ce,u=I=>s===!0?I:pt(I,s===!1?1:void 0);let d,h=!1,m=!1;if(de(e)?(d=()=>e.value,h=un(e)):yt(e)?(d=()=>u(e),h=!0):k(e)?(m=!0,h=e.some(I=>yt(I)||un(I)),d=()=>e.map(I=>{if(de(I))return I.value;if(yt(I))return u(I);if(U(I))return ze(I,c,2)})):U(e)?t?d=()=>ze(e,c,2):d=()=>(w&&w(),Se(e,c,3,[L])):d=xe,t&&s){const I=d;d=()=>pt(I())}let w,L=I=>{w=p.onStop=()=>{ze(I,c,4),w=p.onStop=void 0}},M;if(Fn)if(L=xe,t?n&&Se(t,c,3,[d(),m?[]:void 0,L]):d(),r==="sync"){const I=fl();M=I.__watcherHandles||(I.__watcherHandles=[])}else return xe;let F=m?new Array(e.length).fill(Yt):Yt;const W=()=>{if(!(!p.active||!p.dirty))if(t){const I=p.run();(s||h||(m?I.some((D,R)=>Qe(D,F[R])):Qe(I,F)))&&(w&&w(),Se(t,c,3,[I,F===Yt?void 0:m&&F[0]===Yt?[]:F,L]),F=I)}else p.run()};W.allowRecurse=!!t;let J;r==="sync"?J=W:r==="post"?J=()=>pe(W,c&&c.suspense):(W.pre=!0,c&&(W.id=c.uid),J=()=>Ls(W));const p=new ws(d,xe,J),_=kr(),N=()=>{p.stop(),_&&_s(_.effects,p)};return t?n?W():F=p.run():r==="post"?pe(p.run.bind(p),c&&c.suspense):p.run(),M&&M.push(N),N}function dl(e,t,n){const s=this.proxy,r=ne(e)?e.includes(".")?go(s,e):()=>s[e]:e.bind(s,s);let o;U(t)?o=t:(o=t.handler,n=t);const i=Bt(this),l=On(r,o.bind(s),n);return i(),l}function go(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r0){if(n>=t)return e;n++}if(s=s||new Set,s.has(e))return e;if(s.add(e),de(e))pt(e.value,t,n,s);else if(k(e))for(let r=0;r{pt(r,t,n,s)});else if(Hr(e))for(const r in e)pt(e[r],t,n,s);return e}function Ie(e,t,n,s){const r=e.dirs,o=t&&t.dirs;for(let i=0;i{e.isMounted=!0}),wo(()=>{e.isUnmounting=!0}),e}const we=[Function,Array],mo={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:we,onEnter:we,onAfterEnter:we,onEnterCancelled:we,onBeforeLeave:we,onLeave:we,onAfterLeave:we,onLeaveCancelled:we,onBeforeAppear:we,onAppear:we,onAfterAppear:we,onAppearCancelled:we},pl={name:"BaseTransition",props:mo,setup(e,{slots:t}){const n=Nn(),s=hl();let r;return()=>{const o=t.default&&yo(t.default(),!0);if(!o||!o.length)return;let i=o[0];if(o.length>1){for(const M of o)if(M.type!==ve){i=M;break}}const l=Y(e),{mode:c}=l;if(s.isLeaving)return Bn(i);const u=Zs(i);if(!u)return Bn(i);const d=os(u,l,s,n);is(u,d);const h=n.subTree,m=h&&Zs(h);let w=!1;const{getTransitionKey:L}=u.type;if(L){const M=L();r===void 0?r=M:M!==r&&(r=M,w=!0)}if(m&&m.type!==ve&&(!ot(u,m)||w)){const M=os(m,l,s,n);if(is(m,M),c==="out-in")return s.isLeaving=!0,M.afterLeave=()=>{s.isLeaving=!1,n.update.active!==!1&&(n.effect.dirty=!0,n.update())},Bn(i);c==="in-out"&&u.type!==ve&&(M.delayLeave=(F,W,J)=>{const p=_o(s,m);p[String(m.key)]=m,F[Ke]=()=>{W(),F[Ke]=void 0,delete d.delayedLeave},d.delayedLeave=J})}return i}}},gl=pl;function _o(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function os(e,t,n,s){const{appear:r,mode:o,persisted:i=!1,onBeforeEnter:l,onEnter:c,onAfterEnter:u,onEnterCancelled:d,onBeforeLeave:h,onLeave:m,onAfterLeave:w,onLeaveCancelled:L,onBeforeAppear:M,onAppear:F,onAfterAppear:W,onAppearCancelled:J}=t,p=String(e.key),_=_o(n,e),N=(R,T)=>{R&&Se(R,s,9,T)},I=(R,T)=>{const S=T[1];N(R,T),k(R)?R.every(K=>K.length<=1)&&S():R.length<=1&&S()},D={mode:o,persisted:i,beforeEnter(R){let T=l;if(!n.isMounted)if(r)T=M||l;else return;R[Ke]&&R[Ke](!0);const S=_[p];S&&ot(e,S)&&S.el[Ke]&&S.el[Ke](),N(T,[R])},enter(R){let T=c,S=u,K=d;if(!n.isMounted)if(r)T=F||c,S=W||u,K=J||d;else return;let O=!1;const q=R[Jt]=re=>{O||(O=!0,re?N(K,[R]):N(S,[R]),D.delayedLeave&&D.delayedLeave(),R[Jt]=void 0)};T?I(T,[R,q]):q()},leave(R,T){const S=String(e.key);if(R[Jt]&&R[Jt](!0),n.isUnmounting)return T();N(h,[R]);let K=!1;const O=R[Ke]=q=>{K||(K=!0,T(),q?N(L,[R]):N(w,[R]),R[Ke]=void 0,_[S]===e&&delete _[S])};_[S]=e,m?I(m,[R,O]):O()},clone(R){return os(R,t,n,s)}};return D}function Bn(e){if(Ln(e))return e=Ze(e),e.children=null,e}function Zs(e){return Ln(e)?e.children?e.children[0]:void 0:e}function is(e,t){e.shapeFlag&6&&e.component?is(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function yo(e,t=!1,n){let s=[],r=0;for(let o=0;o1)for(let o=0;o!!e.type.__asyncLoader,Ln=e=>e.type.__isKeepAlive;function ml(e,t){vo(e,"a",t)}function _l(e,t){vo(e,"da",t)}function vo(e,t,n=ce){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(In(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Ln(r.parent.vnode)&&yl(s,t,n,r),r=r.parent}}function yl(e,t,n,s){const r=In(t,e,s,!0);Pn(()=>{_s(s[t],r)},n)}function In(e,t,n=ce,s=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{if(n.isUnmounted)return;ut();const l=Bt(n),c=Se(t,n,e,i);return l(),ft(),c});return s?r.unshift(o):r.push(o),o}}const je=e=>(t,n=ce)=>(!Fn||e==="sp")&&In(e,(...s)=>t(...s),n),bl=je("bm"),St=je("m"),vl=je("bu"),wl=je("u"),wo=je("bum"),Pn=je("um"),El=je("sp"),Cl=je("rtg"),xl=je("rtc");function Sl(e,t=ce){In("ec",e,t)}function ka(e,t,n,s){let r;const o=n&&n[s];if(k(e)||ne(e)){r=new Array(e.length);for(let i=0,l=e.length;it(i,l,void 0,o&&o[l]));else{const i=Object.keys(e);r=new Array(i.length);for(let l=0,c=i.length;lmn(t)?!(t.type===ve||t.type===ge&&!Eo(t.children)):!0)?e:null}function Ua(e,t){const n={};for(const s in e)n[t&&/[A-Z]/.test(s)?`on:${s}`:rn(s)]=e[s];return n}const ls=e=>e?Vo(e)?Fs(e)||e.proxy:ls(e.parent):null,Pt=oe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ls(e.parent),$root:e=>ls(e.root),$emit:e=>e.emit,$options:e=>Ps(e),$forceUpdate:e=>e.f||(e.f=()=>{e.effect.dirty=!0,Ls(e.update)}),$nextTick:e=>e.n||(e.n=Tn.bind(e.proxy)),$watch:e=>dl.bind(e)}),Un=(e,t)=>e!==te&&!e.__isScriptSetup&&X(e,t),Tl={get({_:e},t){const{ctx:n,setupState:s,data:r,props:o,accessCache:i,type:l,appContext:c}=e;let u;if(t[0]!=="$"){const w=i[t];if(w!==void 0)switch(w){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return o[t]}else{if(Un(s,t))return i[t]=1,s[t];if(r!==te&&X(r,t))return i[t]=2,r[t];if((u=e.propsOptions[0])&&X(u,t))return i[t]=3,o[t];if(n!==te&&X(n,t))return i[t]=4,n[t];cs&&(i[t]=0)}}const d=Pt[t];let h,m;if(d)return t==="$attrs"&&_e(e,"get",t),d(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==te&&X(n,t))return i[t]=4,n[t];if(m=c.config.globalProperties,X(m,t))return m[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:o}=e;return Un(r,t)?(r[t]=n,!0):s!==te&&X(s,t)?(s[t]=n,!0):X(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:o}},i){let l;return!!n[i]||e!==te&&X(e,i)||Un(t,i)||(l=o[0])&&X(l,i)||X(s,i)||X(Pt,i)||X(r.config.globalProperties,i)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:X(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ka(){return Al().slots}function Al(){const e=Nn();return e.setupContext||(e.setupContext=ko(e))}function er(e){return k(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let cs=!0;function Rl(e){const t=Ps(e),n=e.proxy,s=e.ctx;cs=!1,t.beforeCreate&&tr(t.beforeCreate,e,"bc");const{data:r,computed:o,methods:i,watch:l,provide:c,inject:u,created:d,beforeMount:h,mounted:m,beforeUpdate:w,updated:L,activated:M,deactivated:F,beforeDestroy:W,beforeUnmount:J,destroyed:p,unmounted:_,render:N,renderTracked:I,renderTriggered:D,errorCaptured:R,serverPrefetch:T,expose:S,inheritAttrs:K,components:O,directives:q,filters:re}=t;if(u&&Ol(u,s,null),i)for(const z in i){const H=i[z];U(H)&&(s[z]=H.bind(n))}if(r){const z=r.call(n,n);Z(z)&&(e.data=Cn(z))}if(cs=!0,o)for(const z in o){const H=o[z],Ne=U(H)?H.bind(n,n):U(H.get)?H.get.bind(n,n):xe,Ut=!U(H)&&U(H.set)?H.set.bind(n):xe,et=se({get:Ne,set:Ut});Object.defineProperty(s,z,{enumerable:!0,configurable:!0,get:()=>et.value,set:Oe=>et.value=Oe})}if(l)for(const z in l)Co(l[z],s,n,z);if(c){const z=U(c)?c.call(n):c;Reflect.ownKeys(z).forEach(H=>{Fl(H,z[H])})}d&&tr(d,e,"c");function j(z,H){k(H)?H.forEach(Ne=>z(Ne.bind(n))):H&&z(H.bind(n))}if(j(bl,h),j(St,m),j(vl,w),j(wl,L),j(ml,M),j(_l,F),j(Sl,R),j(xl,I),j(Cl,D),j(wo,J),j(Pn,_),j(El,T),k(S))if(S.length){const z=e.exposed||(e.exposed={});S.forEach(H=>{Object.defineProperty(z,H,{get:()=>n[H],set:Ne=>n[H]=Ne})})}else e.exposed||(e.exposed={});N&&e.render===xe&&(e.render=N),K!=null&&(e.inheritAttrs=K),O&&(e.components=O),q&&(e.directives=q)}function Ol(e,t,n=xe){k(e)&&(e=as(e));for(const s in e){const r=e[s];let o;Z(r)?"default"in r?o=wt(r.from||s,r.default,!0):o=wt(r.from||s):o=wt(r),de(o)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[s]=o}}function tr(e,t,n){Se(k(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Co(e,t,n,s){const r=s.includes(".")?go(n,s):()=>n[s];if(ne(e)){const o=t[e];U(o)&&Xe(r,o)}else if(U(e))Xe(r,e.bind(n));else if(Z(e))if(k(e))e.forEach(o=>Co(o,t,n,s));else{const o=U(e.handler)?e.handler.bind(n):t[e.handler];U(o)&&Xe(r,o,e)}}function Ps(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(u=>hn(c,u,i,!0)),hn(c,t,i)),Z(t)&&o.set(t,c),c}function hn(e,t,n,s=!1){const{mixins:r,extends:o}=t;o&&hn(e,o,n,!0),r&&r.forEach(i=>hn(e,i,n,!0));for(const i in t)if(!(s&&i==="expose")){const l=Ll[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const Ll={data:nr,props:sr,emits:sr,methods:Rt,computed:Rt,beforeCreate:he,created:he,beforeMount:he,mounted:he,beforeUpdate:he,updated:he,beforeDestroy:he,beforeUnmount:he,destroyed:he,unmounted:he,activated:he,deactivated:he,errorCaptured:he,serverPrefetch:he,components:Rt,directives:Rt,watch:Pl,provide:nr,inject:Il};function nr(e,t){return t?e?function(){return oe(U(e)?e.call(this,this):e,U(t)?t.call(this,this):t)}:t:e}function Il(e,t){return Rt(as(e),as(t))}function as(e){if(k(e)){const t={};for(let n=0;n1)return n&&U(t)?t.call(s&&s.proxy):t}}function $l(e,t,n,s=!1){const r={},o={};cn(o,Mn,1),e.propsDefaults=Object.create(null),So(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=s?r:Bi(r):e.type.props?e.props=r:e.props=o,e.attrs=o}function Hl(e,t,n,s){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=Y(r),[c]=e.propsOptions;let u=!1;if((s||i>0)&&!(i&16)){if(i&8){const d=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[m,w]=To(h,t,!0);oe(i,m),w&&l.push(...w)};!n&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!o&&!c)return Z(e)&&s.set(e,mt),mt;if(k(o))for(let d=0;d-1,w[1]=M<0||L-1||X(w,"default"))&&l.push(h)}}}const u=[i,l];return Z(e)&&s.set(e,u),u}function rr(e){return e[0]!=="$"}function or(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ir(e,t){return or(e)===or(t)}function lr(e,t){return k(t)?t.findIndex(n=>ir(n,e)):U(t)&&ir(t,e)?0:-1}const Ao=e=>e[0]==="_"||e==="$stable",Ms=e=>k(e)?e.map(Ae):[Ae(e)],jl=(e,t,n)=>{if(t._n)return t;const s=rl((...r)=>Ms(t(...r)),n);return s._c=!1,s},Ro=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Ao(r))continue;const o=e[r];if(U(o))t[r]=jl(r,o,s);else if(o!=null){const i=Ms(o);t[r]=()=>i}}},Oo=(e,t)=>{const n=Ms(t);e.slots.default=()=>n},Vl=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=Y(t),cn(t,"_",n)):Ro(t,e.slots={})}else e.slots={},t&&Oo(e,t);cn(e.slots,Mn,1)},Dl=(e,t,n)=>{const{vnode:s,slots:r}=e;let o=!0,i=te;if(s.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:(oe(r,t),!n&&l===1&&delete r._):(o=!t.$stable,Ro(t,r)),i=t}else t&&(Oo(e,t),i={default:1});if(o)for(const l in r)!Ao(l)&&i[l]==null&&delete r[l]};function gn(e,t,n,s,r=!1){if(k(e)){e.forEach((m,w)=>gn(m,t&&(k(t)?t[w]:t),n,s,r));return}if(vt(s)&&!r)return;const o=s.shapeFlag&4?Fs(s.component)||s.component.proxy:s.el,i=r?null:o,{i:l,r:c}=e,u=t&&t.r,d=l.refs===te?l.refs={}:l.refs,h=l.setupState;if(u!=null&&u!==c&&(ne(u)?(d[u]=null,X(h,u)&&(h[u]=null)):de(u)&&(u.value=null)),U(c))ze(c,l,12,[i,d]);else{const m=ne(c),w=de(c),L=e.f;if(m||w){const M=()=>{if(L){const F=m?X(h,c)?h[c]:d[c]:c.value;r?k(F)&&_s(F,o):k(F)?F.includes(o)||F.push(o):m?(d[c]=[o],X(h,c)&&(h[c]=d[c])):(c.value=[o],e.k&&(d[e.k]=c.value))}else m?(d[c]=i,X(h,c)&&(h[c]=i)):w&&(c.value=i,e.k&&(d[e.k]=i))};r||L?M():(M.id=-1,pe(M,n))}}}let De=!1;const kl=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Bl=e=>e.namespaceURI.includes("MathML"),Qt=e=>{if(kl(e))return"svg";if(Bl(e))return"mathml"},Zt=e=>e.nodeType===8;function Ul(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:o,parentNode:i,remove:l,insert:c,createComment:u}}=e,d=(p,_)=>{if(!_.hasChildNodes()){n(null,p,_),fn(),_._vnode=p;return}De=!1,h(_.firstChild,p,null,null,null),fn(),_._vnode=p,De&&console.error("Hydration completed but contains mismatches.")},h=(p,_,N,I,D,R=!1)=>{const T=Zt(p)&&p.data==="[",S=()=>M(p,_,N,I,D,T),{type:K,ref:O,shapeFlag:q,patchFlag:re}=_;let le=p.nodeType;_.el=p,re===-2&&(R=!1,_.dynamicChildren=null);let j=null;switch(K){case Ct:le!==3?_.children===""?(c(_.el=r(""),i(p),p),j=p):j=S():(p.data!==_.children&&(De=!0,p.data=_.children),j=o(p));break;case ve:J(p)?(j=o(p),W(_.el=p.content.firstChild,p,N)):le!==8||T?j=S():j=o(p);break;case Mt:if(T&&(p=o(p),le=p.nodeType),le===1||le===3){j=p;const z=!_.children.length;for(let H=0;H<_.staticCount;H++)z&&(_.children+=j.nodeType===1?j.outerHTML:j.data),H===_.staticCount-1&&(_.anchor=j),j=o(j);return T?o(j):j}else S();break;case ge:T?j=L(p,_,N,I,D,R):j=S();break;default:if(q&1)(le!==1||_.type.toLowerCase()!==p.tagName.toLowerCase())&&!J(p)?j=S():j=m(p,_,N,I,D,R);else if(q&6){_.slotScopeIds=D;const z=i(p);if(T?j=F(p):Zt(p)&&p.data==="teleport start"?j=F(p,p.data,"teleport end"):j=o(p),t(_,z,null,N,I,Qt(z),R),vt(_)){let H;T?(H=ae(ge),H.anchor=j?j.previousSibling:z.lastChild):H=p.nodeType===3?jo(""):ae("div"),H.el=p,_.component.subTree=H}}else q&64?le!==8?j=S():j=_.type.hydrate(p,_,N,I,D,R,e,w):q&128&&(j=_.type.hydrate(p,_,N,I,Qt(i(p)),D,R,e,h))}return O!=null&&gn(O,null,I,_),j},m=(p,_,N,I,D,R)=>{R=R||!!_.dynamicChildren;const{type:T,props:S,patchFlag:K,shapeFlag:O,dirs:q,transition:re}=_,le=T==="input"||T==="option";if(le||K!==-1){q&&Ie(_,null,N,"created");let j=!1;if(J(p)){j=Lo(I,re)&&N&&N.vnode.props&&N.vnode.props.appear;const H=p.content.firstChild;j&&re.beforeEnter(H),W(H,p,N),_.el=p=H}if(O&16&&!(S&&(S.innerHTML||S.textContent))){let H=w(p.firstChild,_,p,N,I,D,R);for(;H;){De=!0;const Ne=H;H=H.nextSibling,l(Ne)}}else O&8&&p.textContent!==_.children&&(De=!0,p.textContent=_.children);if(S)if(le||!R||K&48)for(const H in S)(le&&(H.endsWith("value")||H==="indeterminate")||kt(H)&&!Ot(H)||H[0]===".")&&s(p,H,null,S[H],void 0,void 0,N);else S.onClick&&s(p,"onClick",null,S.onClick,void 0,void 0,N);let z;(z=S&&S.onVnodeBeforeMount)&&Ee(z,N,_),q&&Ie(_,null,N,"beforeMount"),((z=S&&S.onVnodeMounted)||q||j)&&ho(()=>{z&&Ee(z,N,_),j&&re.enter(p),q&&Ie(_,null,N,"mounted")},I)}return p.nextSibling},w=(p,_,N,I,D,R,T)=>{T=T||!!_.dynamicChildren;const S=_.children,K=S.length;for(let O=0;O{const{slotScopeIds:T}=_;T&&(D=D?D.concat(T):T);const S=i(p),K=w(o(p),_,S,N,I,D,R);return K&&Zt(K)&&K.data==="]"?o(_.anchor=K):(De=!0,c(_.anchor=u("]"),S,K),K)},M=(p,_,N,I,D,R)=>{if(De=!0,_.el=null,R){const K=F(p);for(;;){const O=o(p);if(O&&O!==K)l(O);else break}}const T=o(p),S=i(p);return l(p),n(null,_,S,T,N,I,Qt(S),D),T},F=(p,_="[",N="]")=>{let I=0;for(;p;)if(p=o(p),p&&Zt(p)&&(p.data===_&&I++,p.data===N)){if(I===0)return o(p);I--}return p},W=(p,_,N)=>{const I=_.parentNode;I&&I.replaceChild(p,_);let D=N;for(;D;)D.vnode.el===_&&(D.vnode.el=D.subTree.el=p),D=D.parent},J=p=>p.nodeType===1&&p.tagName.toLowerCase()==="template";return[d,h]}const pe=ho;function Kl(e){return Wl(e,Ul)}function Wl(e,t){const n=jr();n.__VUE__=!0;const{insert:s,remove:r,patchProp:o,createElement:i,createText:l,createComment:c,setText:u,setElementText:d,parentNode:h,nextSibling:m,setScopeId:w=xe,insertStaticContent:L}=e,M=(a,f,g,y=null,b=null,C=null,A=void 0,E=null,x=!!f.dynamicChildren)=>{if(a===f)return;a&&!ot(a,f)&&(y=Kt(a),Oe(a,b,C,!0),a=null),f.patchFlag===-2&&(x=!1,f.dynamicChildren=null);const{type:v,ref:P,shapeFlag:V}=f;switch(v){case Ct:F(a,f,g,y);break;case ve:W(a,f,g,y);break;case Mt:a==null&&J(f,g,y,A);break;case ge:O(a,f,g,y,b,C,A,E,x);break;default:V&1?N(a,f,g,y,b,C,A,E,x):V&6?q(a,f,g,y,b,C,A,E,x):(V&64||V&128)&&v.process(a,f,g,y,b,C,A,E,x,dt)}P!=null&&b&&gn(P,a&&a.ref,C,f||a,!f)},F=(a,f,g,y)=>{if(a==null)s(f.el=l(f.children),g,y);else{const b=f.el=a.el;f.children!==a.children&&u(b,f.children)}},W=(a,f,g,y)=>{a==null?s(f.el=c(f.children||""),g,y):f.el=a.el},J=(a,f,g,y)=>{[a.el,a.anchor]=L(a.children,f,g,y,a.el,a.anchor)},p=({el:a,anchor:f},g,y)=>{let b;for(;a&&a!==f;)b=m(a),s(a,g,y),a=b;s(f,g,y)},_=({el:a,anchor:f})=>{let g;for(;a&&a!==f;)g=m(a),r(a),a=g;r(f)},N=(a,f,g,y,b,C,A,E,x)=>{f.type==="svg"?A="svg":f.type==="math"&&(A="mathml"),a==null?I(f,g,y,b,C,A,E,x):T(a,f,b,C,A,E,x)},I=(a,f,g,y,b,C,A,E)=>{let x,v;const{props:P,shapeFlag:V,transition:$,dirs:B}=a;if(x=a.el=i(a.type,C,P&&P.is,P),V&8?d(x,a.children):V&16&&R(a.children,x,null,y,b,Kn(a,C),A,E),B&&Ie(a,null,y,"created"),D(x,a,a.scopeId,A,y),P){for(const Q in P)Q!=="value"&&!Ot(Q)&&o(x,Q,null,P[Q],C,a.children,y,b,Fe);"value"in P&&o(x,"value",null,P.value,C),(v=P.onVnodeBeforeMount)&&Ee(v,y,a)}B&&Ie(a,null,y,"beforeMount");const G=Lo(b,$);G&&$.beforeEnter(x),s(x,f,g),((v=P&&P.onVnodeMounted)||G||B)&&pe(()=>{v&&Ee(v,y,a),G&&$.enter(x),B&&Ie(a,null,y,"mounted")},b)},D=(a,f,g,y,b)=>{if(g&&w(a,g),y)for(let C=0;C{for(let v=x;v{const E=f.el=a.el;let{patchFlag:x,dynamicChildren:v,dirs:P}=f;x|=a.patchFlag&16;const V=a.props||te,$=f.props||te;let B;if(g&&tt(g,!1),(B=$.onVnodeBeforeUpdate)&&Ee(B,g,f,a),P&&Ie(f,a,g,"beforeUpdate"),g&&tt(g,!0),v?S(a.dynamicChildren,v,E,g,y,Kn(f,b),C):A||H(a,f,E,null,g,y,Kn(f,b),C,!1),x>0){if(x&16)K(E,f,V,$,g,y,b);else if(x&2&&V.class!==$.class&&o(E,"class",null,$.class,b),x&4&&o(E,"style",V.style,$.style,b),x&8){const G=f.dynamicProps;for(let Q=0;Q{B&&Ee(B,g,f,a),P&&Ie(f,a,g,"updated")},y)},S=(a,f,g,y,b,C,A)=>{for(let E=0;E{if(g!==y){if(g!==te)for(const E in g)!Ot(E)&&!(E in y)&&o(a,E,g[E],null,A,f.children,b,C,Fe);for(const E in y){if(Ot(E))continue;const x=y[E],v=g[E];x!==v&&E!=="value"&&o(a,E,v,x,A,f.children,b,C,Fe)}"value"in y&&o(a,"value",g.value,y.value,A)}},O=(a,f,g,y,b,C,A,E,x)=>{const v=f.el=a?a.el:l(""),P=f.anchor=a?a.anchor:l("");let{patchFlag:V,dynamicChildren:$,slotScopeIds:B}=f;B&&(E=E?E.concat(B):B),a==null?(s(v,g,y),s(P,g,y),R(f.children||[],g,P,b,C,A,E,x)):V>0&&V&64&&$&&a.dynamicChildren?(S(a.dynamicChildren,$,g,b,C,A,E),(f.key!=null||b&&f===b.subTree)&&Io(a,f,!0)):H(a,f,g,P,b,C,A,E,x)},q=(a,f,g,y,b,C,A,E,x)=>{f.slotScopeIds=E,a==null?f.shapeFlag&512?b.ctx.activate(f,g,y,A,x):re(f,g,y,b,C,A,x):le(a,f,x)},re=(a,f,g,y,b,C,A)=>{const E=a.component=ec(a,y,b);if(Ln(a)&&(E.ctx.renderer=dt),tc(E),E.asyncDep){if(b&&b.registerDep(E,j),!a.el){const x=E.subTree=ae(ve);W(null,x,f,g)}}else j(E,a,f,g,b,C,A)},le=(a,f,g)=>{const y=f.component=a.component;if(ll(a,f,g))if(y.asyncDep&&!y.asyncResolved){z(y,f,g);return}else y.next=f,el(y.update),y.effect.dirty=!0,y.update();else f.el=a.el,y.vnode=f},j=(a,f,g,y,b,C,A)=>{const E=()=>{if(a.isMounted){let{next:P,bu:V,u:$,parent:B,vnode:G}=a;{const ht=Po(a);if(ht){P&&(P.el=G.el,z(a,P,A)),ht.asyncDep.then(()=>{a.isUnmounted||E()});return}}let Q=P,ee;tt(a,!1),P?(P.el=G.el,z(a,P,A)):P=G,V&&Vn(V),(ee=P.props&&P.props.onVnodeBeforeUpdate)&&Ee(ee,B,P,G),tt(a,!0);const ie=kn(a),Te=a.subTree;a.subTree=ie,M(Te,ie,h(Te.el),Kt(Te),a,b,C),P.el=ie.el,Q===null&&cl(a,ie.el),$&&pe($,b),(ee=P.props&&P.props.onVnodeUpdated)&&pe(()=>Ee(ee,B,P,G),b)}else{let P;const{el:V,props:$}=f,{bm:B,m:G,parent:Q}=a,ee=vt(f);if(tt(a,!1),B&&Vn(B),!ee&&(P=$&&$.onVnodeBeforeMount)&&Ee(P,Q,f),tt(a,!0),V&&jn){const ie=()=>{a.subTree=kn(a),jn(V,a.subTree,a,b,null)};ee?f.type.__asyncLoader().then(()=>!a.isUnmounted&&ie()):ie()}else{const ie=a.subTree=kn(a);M(null,ie,g,y,a,b,C),f.el=ie.el}if(G&&pe(G,b),!ee&&(P=$&&$.onVnodeMounted)){const ie=f;pe(()=>Ee(P,Q,ie),b)}(f.shapeFlag&256||Q&&vt(Q.vnode)&&Q.vnode.shapeFlag&256)&&a.a&&pe(a.a,b),a.isMounted=!0,f=g=y=null}},x=a.effect=new ws(E,xe,()=>Ls(v),a.scope),v=a.update=()=>{x.dirty&&x.run()};v.id=a.uid,tt(a,!0),v()},z=(a,f,g)=>{f.component=a;const y=a.vnode.props;a.vnode=f,a.next=null,Hl(a,f.props,y,g),Dl(a,f.children,g),ut(),Ys(a),ft()},H=(a,f,g,y,b,C,A,E,x=!1)=>{const v=a&&a.children,P=a?a.shapeFlag:0,V=f.children,{patchFlag:$,shapeFlag:B}=f;if($>0){if($&128){Ut(v,V,g,y,b,C,A,E,x);return}else if($&256){Ne(v,V,g,y,b,C,A,E,x);return}}B&8?(P&16&&Fe(v,b,C),V!==v&&d(g,V)):P&16?B&16?Ut(v,V,g,y,b,C,A,E,x):Fe(v,b,C,!0):(P&8&&d(g,""),B&16&&R(V,g,y,b,C,A,E,x))},Ne=(a,f,g,y,b,C,A,E,x)=>{a=a||mt,f=f||mt;const v=a.length,P=f.length,V=Math.min(v,P);let $;for($=0;$P?Fe(a,b,C,!0,!1,V):R(f,g,y,b,C,A,E,x,V)},Ut=(a,f,g,y,b,C,A,E,x)=>{let v=0;const P=f.length;let V=a.length-1,$=P-1;for(;v<=V&&v<=$;){const B=a[v],G=f[v]=x?We(f[v]):Ae(f[v]);if(ot(B,G))M(B,G,g,null,b,C,A,E,x);else break;v++}for(;v<=V&&v<=$;){const B=a[V],G=f[$]=x?We(f[$]):Ae(f[$]);if(ot(B,G))M(B,G,g,null,b,C,A,E,x);else break;V--,$--}if(v>V){if(v<=$){const B=$+1,G=B$)for(;v<=V;)Oe(a[v],b,C,!0),v++;else{const B=v,G=v,Q=new Map;for(v=G;v<=$;v++){const ye=f[v]=x?We(f[v]):Ae(f[v]);ye.key!=null&&Q.set(ye.key,v)}let ee,ie=0;const Te=$-G+1;let ht=!1,Vs=0;const Tt=new Array(Te);for(v=0;v=Te){Oe(ye,b,C,!0);continue}let Le;if(ye.key!=null)Le=Q.get(ye.key);else for(ee=G;ee<=$;ee++)if(Tt[ee-G]===0&&ot(ye,f[ee])){Le=ee;break}Le===void 0?Oe(ye,b,C,!0):(Tt[Le-G]=v+1,Le>=Vs?Vs=Le:ht=!0,M(ye,f[Le],g,null,b,C,A,E,x),ie++)}const Ds=ht?ql(Tt):mt;for(ee=Ds.length-1,v=Te-1;v>=0;v--){const ye=G+v,Le=f[ye],ks=ye+1{const{el:C,type:A,transition:E,children:x,shapeFlag:v}=a;if(v&6){et(a.component.subTree,f,g,y);return}if(v&128){a.suspense.move(f,g,y);return}if(v&64){A.move(a,f,g,dt);return}if(A===ge){s(C,f,g);for(let V=0;VE.enter(C),b);else{const{leave:V,delayLeave:$,afterLeave:B}=E,G=()=>s(C,f,g),Q=()=>{V(C,()=>{G(),B&&B()})};$?$(C,G,Q):Q()}else s(C,f,g)},Oe=(a,f,g,y=!1,b=!1)=>{const{type:C,props:A,ref:E,children:x,dynamicChildren:v,shapeFlag:P,patchFlag:V,dirs:$}=a;if(E!=null&&gn(E,null,g,a,!0),P&256){f.ctx.deactivate(a);return}const B=P&1&&$,G=!vt(a);let Q;if(G&&(Q=A&&A.onVnodeBeforeUnmount)&&Ee(Q,f,a),P&6)oi(a.component,g,y);else{if(P&128){a.suspense.unmount(g,y);return}B&&Ie(a,null,f,"beforeUnmount"),P&64?a.type.remove(a,f,g,b,dt,y):v&&(C!==ge||V>0&&V&64)?Fe(v,f,g,!1,!0):(C===ge&&V&384||!b&&P&16)&&Fe(x,f,g),y&&Hs(a)}(G&&(Q=A&&A.onVnodeUnmounted)||B)&&pe(()=>{Q&&Ee(Q,f,a),B&&Ie(a,null,f,"unmounted")},g)},Hs=a=>{const{type:f,el:g,anchor:y,transition:b}=a;if(f===ge){ri(g,y);return}if(f===Mt){_(a);return}const C=()=>{r(g),b&&!b.persisted&&b.afterLeave&&b.afterLeave()};if(a.shapeFlag&1&&b&&!b.persisted){const{leave:A,delayLeave:E}=b,x=()=>A(g,C);E?E(a.el,C,x):x()}else C()},ri=(a,f)=>{let g;for(;a!==f;)g=m(a),r(a),a=g;r(f)},oi=(a,f,g)=>{const{bum:y,scope:b,update:C,subTree:A,um:E}=a;y&&Vn(y),b.stop(),C&&(C.active=!1,Oe(A,a,f,g)),E&&pe(E,f),pe(()=>{a.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&a.asyncDep&&!a.asyncResolved&&a.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},Fe=(a,f,g,y=!1,b=!1,C=0)=>{for(let A=C;Aa.shapeFlag&6?Kt(a.component.subTree):a.shapeFlag&128?a.suspense.next():m(a.anchor||a.el);let $n=!1;const js=(a,f,g)=>{a==null?f._vnode&&Oe(f._vnode,null,null,!0):M(f._vnode||null,a,f,null,null,null,g),$n||($n=!0,Ys(),fn(),$n=!1),f._vnode=a},dt={p:M,um:Oe,m:et,r:Hs,mt:re,mc:R,pc:H,pbc:S,n:Kt,o:e};let Hn,jn;return t&&([Hn,jn]=t(dt)),{render:js,hydrate:Hn,createApp:Nl(js,Hn)}}function Kn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function tt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Lo(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Io(e,t,n=!1){const s=e.children,r=t.children;if(k(s)&&k(r))for(let o=0;o>1,e[n[l]]0&&(t[s]=n[o-1]),n[o]=s)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function Po(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Po(t)}const Gl=e=>e.__isTeleport,ge=Symbol.for("v-fgt"),Ct=Symbol.for("v-txt"),ve=Symbol.for("v-cmt"),Mt=Symbol.for("v-stc"),Nt=[];let Re=null;function Mo(e=!1){Nt.push(Re=e?null:[])}function zl(){Nt.pop(),Re=Nt[Nt.length-1]||null}let Vt=1;function cr(e){Vt+=e}function No(e){return e.dynamicChildren=Vt>0?Re||mt:null,zl(),Vt>0&&Re&&Re.push(e),e}function Wa(e,t,n,s,r,o){return No(Ho(e,t,n,s,r,o,!0))}function Fo(e,t,n,s,r){return No(ae(e,t,n,s,r,!0))}function mn(e){return e?e.__v_isVNode===!0:!1}function ot(e,t){return e.type===t.type&&e.key===t.key}const Mn="__vInternal",$o=({key:e})=>e??null,on=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ne(e)||de(e)||U(e)?{i:fe,r:e,k:t,f:!!n}:e:null);function Ho(e,t=null,n=null,s=0,r=null,o=e===ge?0:1,i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&$o(t),ref:t&&on(t),scopeId:Rn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:fe};return l?(Ns(c,n),o&128&&e.normalize(c)):n&&(c.shapeFlag|=ne(n)?8:16),Vt>0&&!i&&Re&&(c.patchFlag>0||o&6)&&c.patchFlag!==32&&Re.push(c),c}const ae=Xl;function Xl(e,t=null,n=null,s=0,r=null,o=!1){if((!e||e===uo)&&(e=ve),mn(e)){const l=Ze(e,t,!0);return n&&Ns(l,n),Vt>0&&!o&&Re&&(l.shapeFlag&6?Re[Re.indexOf(e)]=l:Re.push(l)),l.patchFlag|=-2,l}if(oc(e)&&(e=e.__vccOpts),t){t=Yl(t);let{class:l,style:c}=t;l&&!ne(l)&&(t.class=vs(l)),Z(c)&&(eo(c)&&!k(c)&&(c=oe({},c)),t.style=bs(c))}const i=ne(e)?1:al(e)?128:Gl(e)?64:Z(e)?4:U(e)?2:0;return Ho(e,t,n,s,r,i,o,!0)}function Yl(e){return e?eo(e)||Mn in e?oe({},e):e:null}function Ze(e,t,n=!1){const{props:s,ref:r,patchFlag:o,children:i}=e,l=t?Jl(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&$o(l),ref:t&&t.ref?n&&r?k(r)?r.concat(on(t)):[r,on(t)]:on(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:i,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ge?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ze(e.ssContent),ssFallback:e.ssFallback&&Ze(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function jo(e=" ",t=0){return ae(Ct,null,e,t)}function qa(e,t){const n=ae(Mt,null,e);return n.staticCount=t,n}function Ga(e="",t=!1){return t?(Mo(),Fo(ve,null,e)):ae(ve,null,e)}function Ae(e){return e==null||typeof e=="boolean"?ae(ve):k(e)?ae(ge,null,e.slice()):typeof e=="object"?We(e):ae(Ct,null,String(e))}function We(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ze(e)}function Ns(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(k(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Ns(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!(Mn in t)?t._ctx=fe:r===3&&fe&&(fe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else U(t)?(t={default:t,_ctx:fe},n=32):(t=String(t),s&64?(n=16,t=[jo(t)]):n=8);e.children=t,e.shapeFlag|=n}function Jl(...e){const t={};for(let n=0;nce||fe;let _n,fs;{const e=jr(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),o=>{r.length>1?r.forEach(i=>i(o)):r[0](o)}};_n=t("__VUE_INSTANCE_SETTERS__",n=>ce=n),fs=t("__VUE_SSR_SETTERS__",n=>Fn=n)}const Bt=e=>{const t=ce;return _n(e),e.scope.on(),()=>{e.scope.off(),_n(t)}},ar=()=>{ce&&ce.scope.off(),_n(null)};function Vo(e){return e.vnode.shapeFlag&4}let Fn=!1;function tc(e,t=!1){t&&fs(t);const{props:n,children:s}=e.vnode,r=Vo(e);$l(e,n,r,t),Vl(e,s);const o=r?nc(e,t):void 0;return t&&fs(!1),o}function nc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Lt(new Proxy(e.ctx,Tl));const{setup:s}=n;if(s){const r=e.setupContext=s.length>1?ko(e):null,o=Bt(e);ut();const i=ze(s,e,0,[e.props,r]);if(ft(),o(),Fr(i)){if(i.then(ar,ar),t)return i.then(l=>{ur(e,l,t)}).catch(l=>{Sn(l,e,0)});e.asyncDep=i}else ur(e,i,t)}else Do(e,t)}function ur(e,t,n){U(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Z(t)&&(e.setupState=oo(t)),Do(e,n)}let fr;function Do(e,t,n){const s=e.type;if(!e.render){if(!t&&fr&&!s.render){const r=s.template||Ps(e).template;if(r){const{isCustomElement:o,compilerOptions:i}=e.appContext.config,{delimiters:l,compilerOptions:c}=s,u=oe(oe({isCustomElement:o,delimiters:l},i),c);s.render=fr(r,u)}}e.render=s.render||xe}{const r=Bt(e);ut();try{Rl(e)}finally{ft(),r()}}}function sc(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return _e(e,"get","$attrs"),t[n]}}))}function ko(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return sc(e)},slots:e.slots,emit:e.emit,expose:t}}function Fs(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(oo(Lt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Pt)return Pt[n](e)},has(t,n){return n in t||n in Pt}}))}function rc(e,t=!0){return U(e)?e.displayName||e.name:e.name||t&&e.__name}function oc(e){return U(e)&&"__vccOpts"in e}const se=(e,t)=>Ui(e,t,Fn);function ds(e,t,n){const s=arguments.length;return s===2?Z(t)&&!k(t)?mn(t)?ae(e,null,[t]):ae(e,t):ae(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&mn(n)&&(n=[n]),ae(e,t,n))}const ic="3.4.15";/** -* @vue/runtime-dom v3.4.15 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/const lc="http://www.w3.org/2000/svg",cc="http://www.w3.org/1998/Math/MathML",qe=typeof document<"u"?document:null,dr=qe&&qe.createElement("template"),ac={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?qe.createElementNS(lc,e):t==="mathml"?qe.createElementNS(cc,e):qe.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>qe.createTextNode(e),createComment:e=>qe.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>qe.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{dr.innerHTML=s==="svg"?`${e}`:s==="mathml"?`${e}`:e;const l=dr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},ke="transition",At="animation",Dt=Symbol("_vtc"),Bo=(e,{slots:t})=>ds(gl,uc(e),t);Bo.displayName="Transition";const Uo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Bo.props=oe({},mo,Uo);const nt=(e,t=[])=>{k(e)?e.forEach(n=>n(...t)):e&&e(...t)},hr=e=>e?k(e)?e.some(t=>t.length>1):e.length>1:!1;function uc(e){const t={};for(const O in e)O in Uo||(t[O]=e[O]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=o,appearActiveClass:u=i,appearToClass:d=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:m=`${n}-leave-active`,leaveToClass:w=`${n}-leave-to`}=e,L=fc(r),M=L&&L[0],F=L&&L[1],{onBeforeEnter:W,onEnter:J,onEnterCancelled:p,onLeave:_,onLeaveCancelled:N,onBeforeAppear:I=W,onAppear:D=J,onAppearCancelled:R=p}=t,T=(O,q,re)=>{st(O,q?d:l),st(O,q?u:i),re&&re()},S=(O,q)=>{O._isLeaving=!1,st(O,h),st(O,w),st(O,m),q&&q()},K=O=>(q,re)=>{const le=O?D:J,j=()=>T(q,O,re);nt(le,[q,j]),pr(()=>{st(q,O?c:o),Be(q,O?d:l),hr(le)||gr(q,s,M,j)})};return oe(t,{onBeforeEnter(O){nt(W,[O]),Be(O,o),Be(O,i)},onBeforeAppear(O){nt(I,[O]),Be(O,c),Be(O,u)},onEnter:K(!1),onAppear:K(!0),onLeave(O,q){O._isLeaving=!0;const re=()=>S(O,q);Be(O,h),pc(),Be(O,m),pr(()=>{O._isLeaving&&(st(O,h),Be(O,w),hr(_)||gr(O,s,F,re))}),nt(_,[O,re])},onEnterCancelled(O){T(O,!1),nt(p,[O])},onAppearCancelled(O){T(O,!0),nt(R,[O])},onLeaveCancelled(O){S(O),nt(N,[O])}})}function fc(e){if(e==null)return null;if(Z(e))return[Wn(e.enter),Wn(e.leave)];{const t=Wn(e);return[t,t]}}function Wn(e){return di(e)}function Be(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Dt]||(e[Dt]=new Set)).add(t)}function st(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[Dt];n&&(n.delete(t),n.size||(e[Dt]=void 0))}function pr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let dc=0;function gr(e,t,n,s){const r=e._endId=++dc,o=()=>{r===e._endId&&s()};if(n)return setTimeout(o,n);const{type:i,timeout:l,propCount:c}=hc(e,t);if(!i)return s();const u=i+"end";let d=0;const h=()=>{e.removeEventListener(u,m),o()},m=w=>{w.target===e&&++d>=c&&h()};setTimeout(()=>{d(n[L]||"").split(", "),r=s(`${ke}Delay`),o=s(`${ke}Duration`),i=mr(r,o),l=s(`${At}Delay`),c=s(`${At}Duration`),u=mr(l,c);let d=null,h=0,m=0;t===ke?i>0&&(d=ke,h=i,m=o.length):t===At?u>0&&(d=At,h=u,m=c.length):(h=Math.max(i,u),d=h>0?i>u?ke:At:null,m=d?d===ke?o.length:c.length:0);const w=d===ke&&/\b(transform|all)(,|$)/.test(s(`${ke}Property`).toString());return{type:d,timeout:h,propCount:m,hasTransform:w}}function mr(e,t){for(;e.length_r(n)+_r(e[s])))}function _r(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function pc(){return document.body.offsetHeight}function gc(e,t,n){const s=e[Dt];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const mc=Symbol("_vod"),_c=Symbol("");function yc(e,t,n){const s=e.style,r=s.display,o=ne(n);if(n&&!o){if(t&&!ne(t))for(const i in t)n[i]==null&&hs(s,i,"");for(const i in n)hs(s,i,n[i])}else if(o){if(t!==n){const i=s[_c];i&&(n+=";"+i),s.cssText=n}}else t&&e.removeAttribute("style");mc in e&&(s.display=r)}const yr=/\s*!important$/;function hs(e,t,n){if(k(n))n.forEach(s=>hs(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=bc(e,t);yr.test(n)?e.setProperty(at(s),n.replace(yr,""),"important"):e[s]=n}}const br=["Webkit","Moz","ms"],qn={};function bc(e,t){const n=qn[t];if(n)return n;let s=Me(t);if(s!=="filter"&&s in e)return qn[t]=s;s=wn(s);for(let r=0;rGn||(Tc.then(()=>Gn=0),Gn=Date.now());function Rc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Se(Oc(s,n.value),t,5,[s])};return n.value=e,n.attached=Ac(),n}function Oc(e,t){if(k(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Cr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Lc=(e,t,n,s,r,o,i,l,c)=>{const u=r==="svg";t==="class"?gc(e,s,u):t==="style"?yc(e,n,s):kt(t)?ms(t)||xc(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ic(e,t,s,u))?wc(e,t,s,o,i,l,c):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),vc(e,t,s,u))};function Ic(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Cr(t)&&U(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Cr(t)&&ne(n)?!1:t in e}const Pc=["ctrl","shift","alt","meta"],Mc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Pc.some(n=>e[`${n}Key`]&&!t.includes(n))},za=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...o)=>{for(let i=0;i{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const o=at(r.key);if(t.some(i=>i===o||Nc[i]===o))return e(r)})},Fc=oe({patchProp:Lc},ac);let zn,xr=!1;function $c(){return zn=xr?zn:Kl(Fc),xr=!0,zn}const Ya=(...e)=>{const t=$c().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=jc(s);if(r)return n(r,!0,Hc(r))},t};function Hc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function jc(e){return ne(e)?document.querySelector(e):e}const Ja="/bioloop/docs/api_auth.png",Qa=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},Za="/bioloop/docs/architecture.png",eu="/bioloop/docs/app-celery-communication-diagram.png",tu="/bioloop/docs/secure-download-arch-diagram.png",nu="/bioloop/docs/ui/assets/auth-overview-2.png",su="/bioloop/docs/ui/assets/app-load.png",ru="/bioloop/docs/ui/assets/navigation-guard-logic.png",ou="/bioloop/docs/ui/assets/login-flow-before-cas.png",iu="/bioloop/docs/ui/assets/login-flow-after-cas-return.png",lu="/bioloop/docs/ui/assets/logged-in-flow.png",Vc="modulepreload",Dc=function(e){return"/bioloop/docs/"+e},Sr={},cu=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){const o=document.getElementsByTagName("link");r=Promise.all(n.map(i=>{if(i=Dc(i),i in Sr)return;Sr[i]=!0;const l=i.endsWith(".css"),c=l?'[rel="stylesheet"]':"";if(!!s)for(let h=o.length-1;h>=0;h--){const m=o[h];if(m.href===i&&(!l||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${i}"]${c}`))return;const d=document.createElement("link");if(d.rel=l?"stylesheet":Vc,l||(d.as="script",d.crossOrigin=""),d.href=i,document.head.appendChild(d),l)return new Promise((h,m)=>{d.addEventListener("load",h),d.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${i}`)))})}))}return r.then(()=>t()).catch(o=>{const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=o,window.dispatchEvent(i),!i.defaultPrevented)throw o})},kc=window.__VP_SITE_DATA__;function $s(e){return kr()?(wi(e),!0):!1}function Ye(e){return typeof e=="function"?e():ro(e)}const Ko=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const Bc=Object.prototype.toString,Uc=e=>Bc.call(e)==="[object Object]",Ft=()=>{},ps=Kc();function Kc(){var e,t;return Ko&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function Wc(e,t){function n(...s){return new Promise((r,o)=>{Promise.resolve(e(()=>t.apply(this,s),{fn:t,thisArg:this,args:s})).then(r).catch(o)})}return n}const Wo=e=>e();function qc(e=Wo){const t=me(!0);function n(){t.value=!1}function s(){t.value=!0}const r=(...o)=>{t.value&&e(...o)};return{isActive:xn(t),pause:n,resume:s,eventFilter:r}}function Gc(e){return e||Nn()}function qo(...e){if(e.length!==1)return Yi(...e);const t=e[0];return typeof t=="function"?xn(Gi(()=>({get:t,set:Ft}))):me(t)}function zc(e,t,n={}){const{eventFilter:s=Wo,...r}=n;return Xe(e,Wc(s,t),r)}function Xc(e,t,n={}){const{eventFilter:s,...r}=n,{eventFilter:o,pause:i,resume:l,isActive:c}=qc(s);return{stop:zc(e,t,{...r,eventFilter:o}),pause:i,resume:l,isActive:c}}function Go(e,t=!0,n){Gc()?St(e,n):t?e():Tn(e)}function gt(e){var t;const n=Ye(e);return(t=n==null?void 0:n.$el)!=null?t:n}const He=Ko?window:void 0;function Je(...e){let t,n,s,r;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,s,r]=e,t=He):[t,n,s,r]=e,!t)return Ft;Array.isArray(n)||(n=[n]),Array.isArray(s)||(s=[s]);const o=[],i=()=>{o.forEach(d=>d()),o.length=0},l=(d,h,m,w)=>(d.addEventListener(h,m,w),()=>d.removeEventListener(h,m,w)),c=Xe(()=>[gt(t),Ye(r)],([d,h])=>{if(i(),!d)return;const m=Uc(h)?{...h}:h;o.push(...n.flatMap(w=>s.map(L=>l(d,w,L,m))))},{immediate:!0,flush:"post"}),u=()=>{c(),i()};return $s(u),u}let Tr=!1;function au(e,t,n={}){const{window:s=He,ignore:r=[],capture:o=!0,detectIframe:i=!1}=n;if(!s)return Ft;ps&&!Tr&&(Tr=!0,Array.from(s.document.body.children).forEach(m=>m.addEventListener("click",Ft)),s.document.documentElement.addEventListener("click",Ft));let l=!0;const c=m=>r.some(w=>{if(typeof w=="string")return Array.from(s.document.querySelectorAll(w)).some(L=>L===m.target||m.composedPath().includes(L));{const L=gt(w);return L&&(m.target===L||m.composedPath().includes(L))}}),d=[Je(s,"click",m=>{const w=gt(e);if(!(!w||w===m.target||m.composedPath().includes(w))){if(m.detail===0&&(l=!c(m)),!l){l=!0;return}t(m)}},{passive:!0,capture:o}),Je(s,"pointerdown",m=>{const w=gt(e);l=!c(m)&&!!(w&&!m.composedPath().includes(w))},{passive:!0}),i&&Je(s,"blur",m=>{setTimeout(()=>{var w;const L=gt(e);((w=s.document.activeElement)==null?void 0:w.tagName)==="IFRAME"&&!(L!=null&&L.contains(s.document.activeElement))&&t(m)},0)})].filter(Boolean);return()=>d.forEach(m=>m())}function Yc(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function uu(...e){let t,n,s={};e.length===3?(t=e[0],n=e[1],s=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,n=e[0],s=e[1]):(t=e[0],n=e[1]):(t=!0,n=e[0]);const{target:r=He,eventName:o="keydown",passive:i=!1,dedupe:l=!1}=s,c=Yc(t);return Je(r,o,d=>{d.repeat&&Ye(l)||c(d)&&n(d)},i)}function Jc(){const e=me(!1);return Nn()&&St(()=>{e.value=!0}),e}function Qc(e){const t=Jc();return se(()=>(t.value,!!e()))}function Zc(e,t={}){const{window:n=He}=t,s=Qc(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let r;const o=me(!1),i=u=>{o.value=u.matches},l=()=>{r&&("removeEventListener"in r?r.removeEventListener("change",i):r.removeListener(i))},c=po(()=>{s.value&&(l(),r=n.matchMedia(Ye(e)),"addEventListener"in r?r.addEventListener("change",i):r.addListener(i),o.value=r.matches)});return $s(()=>{c(),l(),r=void 0}),o}const en=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},tn="__vueuse_ssr_handlers__",ea=ta();function ta(){return tn in en||(en[tn]=en[tn]||{}),en[tn]}function zo(e,t){return ea[e]||t}function na(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const sa={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Ar="vueuse-storage";function ra(e,t,n,s={}){var r;const{flush:o="pre",deep:i=!0,listenToStorageChanges:l=!0,writeDefaults:c=!0,mergeDefaults:u=!1,shallow:d,window:h=He,eventFilter:m,onError:w=T=>{console.error(T)},initOnMounted:L}=s,M=(d?no:me)(typeof t=="function"?t():t);if(!n)try{n=zo("getDefaultStorage",()=>{var T;return(T=He)==null?void 0:T.localStorage})()}catch(T){w(T)}if(!n)return M;const F=Ye(t),W=na(F),J=(r=s.serializer)!=null?r:sa[W],{pause:p,resume:_}=Xc(M,()=>N(M.value),{flush:o,deep:i,eventFilter:m});return h&&l&&Go(()=>{Je(h,"storage",R),Je(h,Ar,D),L&&R()}),L||R(),M;function N(T){try{if(T==null)n.removeItem(e);else{const S=J.write(T),K=n.getItem(e);K!==S&&(n.setItem(e,S),h&&h.dispatchEvent(new CustomEvent(Ar,{detail:{key:e,oldValue:K,newValue:S,storageArea:n}})))}}catch(S){w(S)}}function I(T){const S=T?T.newValue:n.getItem(e);if(S==null)return c&&F!=null&&n.setItem(e,J.write(F)),F;if(!T&&u){const K=J.read(S);return typeof u=="function"?u(K,F):W==="object"&&!Array.isArray(K)?{...F,...K}:K}else return typeof S!="string"?S:J.read(S)}function D(T){R(T.detail)}function R(T){if(!(T&&T.storageArea!==n)){if(T&&T.key==null){M.value=F;return}if(!(T&&T.key!==e)){p();try{(T==null?void 0:T.newValue)!==J.write(M.value)&&(M.value=I(T))}catch(S){w(S)}finally{T?Tn(_):_()}}}}}function Xo(e){return Zc("(prefers-color-scheme: dark)",e)}function oa(e={}){const{selector:t="html",attribute:n="class",initialValue:s="auto",window:r=He,storage:o,storageKey:i="vueuse-color-scheme",listenToStorageChanges:l=!0,storageRef:c,emitAuto:u,disableTransition:d=!0}=e,h={auto:"",light:"light",dark:"dark",...e.modes||{}},m=Xo({window:r}),w=se(()=>m.value?"dark":"light"),L=c||(i==null?qo(s):ra(i,s,o,{window:r,listenToStorageChanges:l})),M=se(()=>L.value==="auto"?w.value:L.value),F=zo("updateHTMLAttrs",(_,N,I)=>{const D=typeof _=="string"?r==null?void 0:r.document.querySelector(_):gt(_);if(!D)return;let R;if(d&&(R=r.document.createElement("style"),R.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),r.document.head.appendChild(R)),N==="class"){const T=I.split(/\s/g);Object.values(h).flatMap(S=>(S||"").split(/\s/g)).filter(Boolean).forEach(S=>{T.includes(S)?D.classList.add(S):D.classList.remove(S)})}else D.setAttribute(N,I);d&&(r.getComputedStyle(R).opacity,document.head.removeChild(R))});function W(_){var N;F(t,n,(N=h[_])!=null?N:_)}function J(_){e.onChanged?e.onChanged(_,W):W(_)}Xe(M,J,{flush:"post",immediate:!0}),Go(()=>J(M.value));const p=se({get(){return u?L.value:M.value},set(_){L.value=_}});try{return Object.assign(p,{store:L,system:w,state:M})}catch{return p}}function ia(e={}){const{valueDark:t="dark",valueLight:n="",window:s=He}=e,r=oa({...e,onChanged:(l,c)=>{var u;e.onChanged?(u=e.onChanged)==null||u.call(e,l==="dark",c,l):c(l)},modes:{dark:t,light:n}}),o=se(()=>r.system?r.system.value:Xo({window:s}).value?"dark":"light");return se({get(){return r.value==="dark"},set(l){const c=l?"dark":"light";o.value===c?r.value="auto":r.value=c}})}function Xn(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function Yo(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const nn=new WeakMap;function fu(e,t=!1){const n=me(t);let s=null,r;Xe(qo(e),l=>{const c=Xn(Ye(l));if(c){const u=c;nn.get(u)||nn.set(u,r),n.value&&(u.style.overflow="hidden")}},{immediate:!0});const o=()=>{const l=Xn(Ye(e));!l||n.value||(ps&&(s=Je(l,"touchmove",c=>{la(c)},{passive:!1})),l.style.overflow="hidden",n.value=!0)},i=()=>{var l;const c=Xn(Ye(e));!c||!n.value||(ps&&(s==null||s()),c.style.overflow=(l=nn.get(c))!=null?l:"",nn.delete(c),n.value=!1)};return $s(i),se({get(){return n.value},set(l){l?o():i()}})}function du(e={}){const{window:t=He,behavior:n="auto"}=e;if(!t)return{x:me(0),y:me(0)};const s=me(t.scrollX),r=me(t.scrollY),o=se({get(){return s.value},set(l){scrollTo({left:l,behavior:n})}}),i=se({get(){return r.value},set(l){scrollTo({top:l,behavior:n})}});return Je(t,"scroll",()=>{s.value=t.scrollX,r.value=t.scrollY},{capture:!1,passive:!0}),{x:o,y:i}}var Yn={BASE_URL:"/bioloop/docs/",MODE:"production",DEV:!1,PROD:!0,SSR:!1},ca={};const Jo=/^(?:[a-z]+:|\/\/)/i,aa="vitepress-theme-appearance",ua=/#.*$/,fa=/[?#].*$/,da=/(?:(^|\/)index)?\.(?:md|html)$/,Ce=typeof document<"u",Qo={relativePath:"",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function ha(e,t,n=!1){if(t===void 0)return!1;if(e=Rr(`/${e}`),n)return new RegExp(t).test(e);if(Rr(t)!==e)return!1;const s=t.match(ua);return s?(Ce?location.hash:"")===s[0]:!0}function Rr(e){return decodeURI(e).replace(fa,"").replace(da,"$1")}function pa(e){return Jo.test(e)}function ga(e,t){var s,r,o,i,l,c,u;const n=Object.keys(e.locales).find(d=>d!=="root"&&!pa(d)&&ha(t,`/${d}/`,!0))||"root";return Object.assign({},e,{localeIndex:n,lang:((s=e.locales[n])==null?void 0:s.lang)??e.lang,dir:((r=e.locales[n])==null?void 0:r.dir)??e.dir,title:((o=e.locales[n])==null?void 0:o.title)??e.title,titleTemplate:((i=e.locales[n])==null?void 0:i.titleTemplate)??e.titleTemplate,description:((l=e.locales[n])==null?void 0:l.description)??e.description,head:ei(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(u=e.locales[n])==null?void 0:u.themeConfig}})}function Zo(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const r=ma(e.title,s);return n===r.slice(3)?n:`${n}${r}`}function ma(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function _a(e,t){const[n,s]=t;if(n!=="meta")return!1;const r=Object.entries(s)[0];return r==null?!1:e.some(([o,i])=>o===n&&i[r[0]]===r[1])}function ei(e,t){return[...e.filter(n=>!_a(t,n)),...t]}const ya=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,ba=/^[a-z]:/i;function Or(e){const t=ba.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(ya,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const Jn=new Set;function va(e){if(Jn.size===0){const n=typeof process=="object"&&ca.VITE_EXTRA_EXTENSIONS||(Yn==null?void 0:Yn.VITE_EXTRA_EXTENSIONS)||"";("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip"+(n&&typeof n=="string"?","+n:"")).split(",").forEach(s=>Jn.add(s))}const t=e.split(".").pop();return t==null||!Jn.has(t.toLowerCase())}const wa=Symbol(),ct=no(kc);function hu(e){const t=se(()=>ga(ct.value,e.data.relativePath)),n=t.value.appearance,s=n==="force-dark"?me(!0):n?ia({storageKey:aa,initialValue:()=>typeof n=="string"?n:"auto",...typeof n=="object"?n:{}}):me(!1);return{site:t,theme:se(()=>t.value.themeConfig),page:se(()=>e.data),frontmatter:se(()=>e.data.frontmatter),params:se(()=>e.data.params),lang:se(()=>t.value.lang),dir:se(()=>e.data.frontmatter.dir||t.value.dir),localeIndex:se(()=>t.value.localeIndex||"root"),title:se(()=>Zo(t.value,e.data)),description:se(()=>e.data.description||t.value.description),isDark:s}}function Ea(){const e=wt(wa);if(!e)throw new Error("vitepress data not properly injected in app");return e}function Ca(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function Lr(e){return Jo.test(e)||!e.startsWith("/")?e:Ca(ct.value.base,e)}function xa(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),Ce){const n="/bioloop/docs/";t=Or(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let s=__VP_HASH_MAP__[t.toLowerCase()];if(s||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",s=__VP_HASH_MAP__[t.toLowerCase()]),!s)return null;t=`${n}assets/${t}.${s}.js`}else t=`./${Or(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let ln=[];function pu(e){ln.push(e),Pn(()=>{ln=ln.filter(t=>t!==e)})}function Sa(){let e=ct.value.scrollOffset,t=0,n=24;if(typeof e=="object"&&"padding"in e&&(n=e.padding,e=e.selector),typeof e=="number")t=e;else if(typeof e=="string")t=Ir(e,n);else if(Array.isArray(e))for(const s of e){const r=Ir(s,n);if(r){t=r;break}}return t}function Ir(e,t){const n=document.querySelector(e);if(!n)return 0;const s=n.getBoundingClientRect().bottom;return s<0?0:s+t}const Ta=Symbol(),ti="http://a.com",Aa=()=>({path:"/",component:null,data:Qo});function gu(e,t){const n=Cn(Aa()),s={route:n,go:r};async function r(l=Ce?location.href:"/"){var c,u;l=yn(l),await((c=s.onBeforeRouteChange)==null?void 0:c.call(s,l))!==!1&&(Mr(l),await i(l),await((u=s.onAfterRouteChanged)==null?void 0:u.call(s,l)))}let o=null;async function i(l,c=0,u=!1){var m;if(await((m=s.onBeforePageLoad)==null?void 0:m.call(s,l))===!1)return;const d=new URL(l,ti),h=o=d.pathname;try{let w=await e(h);if(!w)throw new Error(`Page not found: ${h}`);if(o===h){o=null;const{default:L,__pageData:M}=w;if(!L)throw new Error(`Invalid route component: ${L}`);n.path=Ce?h:Lr(h),n.component=Lt(L),n.data=Lt(M),Ce&&Tn(()=>{let F=ct.value.base+M.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!ct.value.cleanUrls&&!F.endsWith("/")&&(F+=".html"),F!==d.pathname&&(d.pathname=F,l=F+d.search+d.hash,history.replaceState(null,"",l)),d.hash&&!c){let W=null;try{W=document.getElementById(decodeURIComponent(d.hash).slice(1))}catch(J){console.warn(J)}if(W){Pr(W,d.hash);return}}window.scrollTo(0,c)})}}catch(w){if(!/fetch|Page not found/.test(w.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(w),!u)try{const L=await fetch(ct.value.base+"hashmap.json");window.__VP_HASH_MAP__=await L.json(),await i(l,c,!0);return}catch{}o===h&&(o=null,n.path=Ce?h:Lr(h),n.component=t?Lt(t):null,n.data=Qo)}}return Ce&&(window.addEventListener("click",l=>{if(l.target.closest("button"))return;const u=l.target.closest("a");if(u&&!u.closest(".vp-raw")&&(u instanceof SVGElement||!u.download)){const{target:d}=u,{href:h,origin:m,pathname:w,hash:L,search:M}=new URL(u.href instanceof SVGAnimatedString?u.href.animVal:u.href,u.baseURI),F=window.location;!l.ctrlKey&&!l.shiftKey&&!l.altKey&&!l.metaKey&&!d&&m===F.origin&&va(w)&&(l.preventDefault(),w===F.pathname&&M===F.search?(L!==F.hash&&(history.pushState(null,"",L),window.dispatchEvent(new Event("hashchange"))),L?Pr(u,L,u.classList.contains("header-anchor")):(Mr(h),window.scrollTo(0,0))):r(h))}},{capture:!0}),window.addEventListener("popstate",async l=>{var c;await i(yn(location.href),l.state&&l.state.scrollPosition||0),(c=s.onAfterRouteChanged)==null||c.call(s,location.href)}),window.addEventListener("hashchange",l=>{l.preventDefault()})),s}function Ra(){const e=wt(Ta);if(!e)throw new Error("useRouter() is called without provider.");return e}function ni(){return Ra().route}function Pr(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(r){console.warn(r)}if(s){let r=function(){!n||Math.abs(i-window.scrollY)>window.innerHeight?window.scrollTo(0,i):window.scrollTo({left:0,top:i,behavior:"smooth"})};const o=parseInt(window.getComputedStyle(s).paddingTop,10),i=window.scrollY+s.getBoundingClientRect().top-Sa()+o;requestAnimationFrame(r)}}function Mr(e){Ce&&yn(e)!==yn(location.href)&&(history.replaceState({scrollPosition:window.scrollY},document.title),history.pushState(null,"",e))}function yn(e){const t=new URL(e,ti);return t.pathname=t.pathname.replace(/(^|\/)index(\.html)?$/,"$1"),ct.value.cleanUrls?t.pathname=t.pathname.replace(/\.html$/,""):!t.pathname.endsWith("/")&&!t.pathname.endsWith(".html")&&(t.pathname+=".html"),t.pathname+t.search+t.hash}const Qn=()=>ln.forEach(e=>e()),mu=bo({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=ni(),{site:n}=Ea();return()=>ds(e.as,n.value.contentProps??{style:{position:"relative"}},[t.component?ds(t.component,{onVnodeMounted:Qn,onVnodeUpdated:Qn,onVnodeUnmounted:Qn}):"404 Page Not Found"])}}),_u=bo({setup(e,{slots:t}){const n=me(!1);return St(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function yu(){Ce&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const s=(n=t.parentElement)==null?void 0:n.parentElement;if(!s)return;const r=Array.from(s.querySelectorAll("input")).indexOf(t);if(r<0)return;const o=s.querySelector(".blocks");if(!o)return;const i=Array.from(o.children).find(u=>u.classList.contains("active"));if(!i)return;const l=o.children[r];if(!l||i===l)return;i.classList.remove("active"),l.classList.add("active");const c=s==null?void 0:s.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function bu(){if(Ce){const e=new WeakMap;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,o=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!r||!o)return;const i=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className),l=[".vp-copy-ignore",".diff.remove"],c=o.cloneNode(!0);c.querySelectorAll(l.join(",")).forEach(d=>d.remove());let u=c.textContent||"";i&&(u=u.replace(/^ *(\$|>) /gm,"").trim()),Oa(u).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const d=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,d)})}})}}async function Oa(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),r=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),r&&(s.removeAllRanges(),s.addRange(r)),n&&n.focus()}}function vu(e,t){let n=!0,s=[];const r=o=>{if(n){n=!1,o.forEach(l=>{const c=Zn(l);for(const u of document.head.children)if(u.isEqualNode(c)){s.push(u);return}});return}const i=o.map(Zn);s.forEach((l,c)=>{const u=i.findIndex(d=>d==null?void 0:d.isEqualNode(l??null));u!==-1?delete i[u]:(l==null||l.remove(),delete s[c])}),i.forEach(l=>l&&document.head.appendChild(l)),s=[...s,...i].filter(Boolean)};po(()=>{const o=e.data,i=t.value,l=o&&o.description,c=o&&o.frontmatter.head||[],u=Zo(i,o);u!==document.title&&(document.title=u);const d=l||i.description;let h=document.querySelector("meta[name=description]");h?h.getAttribute("content")!==d&&h.setAttribute("content",d):Zn(["meta",{name:"description",content:d}]),r(ei(i.head,Ia(c)))})}function Zn([e,t,n]){const s=document.createElement(e);for(const r in t)s.setAttribute(r,t[r]);return n&&(s.innerHTML=n),e==="script"&&!t.async&&(s.async=!1),s}function La(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function Ia(e){return e.filter(t=>!La(t))}const es=new Set,si=()=>document.createElement("link"),Pa=e=>{const t=si();t.rel="prefetch",t.href=e,document.head.appendChild(t)},Ma=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let sn;const Na=Ce&&(sn=si())&&sn.relList&&sn.relList.supports&&sn.relList.supports("prefetch")?Pa:Ma;function wu(){if(!Ce||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(o=>{o.forEach(i=>{if(i.isIntersecting){const l=i.target;n.unobserve(l);const{pathname:c}=l;if(!es.has(c)){es.add(c);const u=xa(c);u&&Na(u)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(o=>{const{hostname:i,pathname:l}=new URL(o.href instanceof SVGAnimatedString?o.href.animVal:o.href,o.baseURI),c=l.match(/\.\w+$/);c&&c[0]!==".html"||o.target!=="_blank"&&i===location.hostname&&(l!==location.pathname?n.observe(o):es.add(l))})})};St(s);const r=ni();Xe(()=>r.path,s),Pn(()=>{n&&n.disconnect()})}export{Ua as $,Pn as A,Da as B,wl as C,Sa as D,ja as E,ge as F,ka as G,no as H,pu as I,ae as J,Va as K,Jo as L,ni as M,Jl as N,wt as O,au as P,uu as Q,bs as R,Tn as S,Bo as T,du as U,qa as V,xn as W,fu as X,Fl as Y,Xa as Z,Qa as _,jo as a,za as a0,Ka as a1,Ja as a2,Za as a3,eu as a4,tu as a5,nu as a6,su as a7,ru as a8,ou as a9,iu as aa,lu as ab,vu as ac,Ta as ad,hu as ae,wa as af,mu as ag,_u as ah,ct as ai,Ya as aj,gu as ak,xa as al,cu as am,wu as an,bu as ao,yu as ap,ds as aq,Fo as b,Wa as c,bo as d,Ga as e,va as f,Lr as g,me as h,pa as i,Ce as j,se as k,St as l,Ho as m,vs as n,Mo as o,ro as p,$a as q,Ba as r,Ha as s,Fa as t,Ea as u,ha as v,rl as w,Zc as x,Xe as y,po as z}; diff --git a/docs/.vitepress/dist/assets/chunks/theme.BqY363-_.js b/docs/.vitepress/dist/assets/chunks/theme.BqY363-_.js new file mode 100644 index 000000000..35b18eda4 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/theme.BqY363-_.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/chunks/VPLocalSearchBox.Crn_Pjbv.js","assets/chunks/framework.C9SxlbOG.js"])))=>i.map(i=>d[i]); +import{d as m,c as u,r as c,n as M,o as a,a as z,t as I,b as k,w as f,T as ue,e as h,_ as g,u as He,i as Be,f as Ee,g as de,h as y,j as d,k as r,l as W,m as ae,p as T,q as D,s as Y,v as j,x as ve,y as pe,z as Fe,A as De,F as w,B as H,C as K,D as $e,E as Q,G as _,H as E,I as ye,J as Z,K as U,L as x,M as Oe,N as Pe,O as re,P as Le,Q as Ve,R as ee,S as Ge,U as Ue,V as je,W as Se,X as Te,Y as ze,Z as We,$ as Ke,a0 as qe,a1 as Re}from"./framework.C9SxlbOG.js";const Je=m({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(s){return(e,t)=>(a(),u("span",{class:M(["VPBadge",e.type])},[c(e.$slots,"default",{},()=>[z(I(e.text),1)])],2))}}),Xe={key:0,class:"VPBackdrop"},Ye=m({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(s){return(e,t)=>(a(),k(ue,{name:"fade"},{default:f(()=>[e.show?(a(),u("div",Xe)):h("",!0)]),_:1}))}}),Qe=g(Ye,[["__scopeId","data-v-c79a1216"]]),L=He;function Ze(s,e){let t,o=!1;return()=>{t&&clearTimeout(t),o?t=setTimeout(s,e):(s(),(o=!0)&&setTimeout(()=>o=!1,e))}}function ie(s){return s.startsWith("/")?s:`/${s}`}function fe(s){const{pathname:e,search:t,hash:o,protocol:n}=new URL(s,"http://a.com");if(Be(s)||s.startsWith("#")||!n.startsWith("http")||!Ee(e))return s;const{site:i}=L(),l=e.endsWith("/")||e.endsWith(".html")?s:s.replace(/(?:(^\.+)\/)?.*$/,`$1${e.replace(/(\.md)?$/,i.value.cleanUrls?"":".html")}${t}${o}`);return de(l)}function R({correspondingLink:s=!1}={}){const{site:e,localeIndex:t,page:o,theme:n,hash:i}=L(),l=y(()=>{var p,$;return{label:(p=e.value.locales[t.value])==null?void 0:p.label,link:(($=e.value.locales[t.value])==null?void 0:$.link)||(t.value==="root"?"/":`/${t.value}/`)}});return{localeLinks:y(()=>Object.entries(e.value.locales).flatMap(([p,$])=>l.value.label===$.label?[]:{text:$.label,link:xe($.link||(p==="root"?"/":`/${p}/`),n.value.i18nRouting!==!1&&s,o.value.relativePath.slice(l.value.link.length-1),!e.value.cleanUrls)+i.value})),currentLang:l}}function xe(s,e,t,o){return e?s.replace(/\/$/,"")+ie(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,o?".html":"")):s}const et={class:"NotFound"},tt={class:"code"},nt={class:"title"},ot={class:"quote"},st={class:"action"},at=["href","aria-label"],rt=m({__name:"NotFound",setup(s){const{theme:e}=L(),{currentLang:t}=R();return(o,n)=>{var i,l,v,p,$;return a(),u("div",et,[d("p",tt,I(((i=r(e).notFound)==null?void 0:i.code)??"404"),1),d("h1",nt,I(((l=r(e).notFound)==null?void 0:l.title)??"PAGE NOT FOUND"),1),n[0]||(n[0]=d("div",{class:"divider"},null,-1)),d("blockquote",ot,I(((v=r(e).notFound)==null?void 0:v.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),d("div",st,[d("a",{class:"link",href:r(de)(r(t).link),"aria-label":((p=r(e).notFound)==null?void 0:p.linkLabel)??"go to home"},I((($=r(e).notFound)==null?void 0:$.linkText)??"Take me home"),9,at)])])}}}),it=g(rt,[["__scopeId","data-v-d6be1790"]]);function Ne(s,e){if(Array.isArray(s))return J(s);if(s==null)return[];e=ie(e);const t=Object.keys(s).sort((n,i)=>i.split("/").length-n.split("/").length).find(n=>e.startsWith(ie(n))),o=t?s[t]:[];return Array.isArray(o)?J(o):J(o.items,o.base)}function lt(s){const e=[];let t=0;for(const o in s){const n=s[o];if(n.items){t=e.push(n);continue}e[t]||e.push({items:[]}),e[t].items.push(n)}return e}function ct(s){const e=[];function t(o){for(const n of o)n.text&&n.link&&e.push({text:n.text,link:n.link,docFooterText:n.docFooterText}),n.items&&t(n.items)}return t(s),e}function le(s,e){return Array.isArray(e)?e.some(t=>le(s,t)):W(s,e.link)?!0:e.items?le(s,e.items):!1}function J(s,e){return[...s].map(t=>{const o={...t},n=o.base||e;return n&&o.link&&(o.link=n+o.link),o.items&&(o.items=J(o.items,n)),o})}function O(){const{frontmatter:s,page:e,theme:t}=L(),o=ae("(min-width: 960px)"),n=T(!1),i=y(()=>{const C=t.value.sidebar,S=e.value.relativePath;return C?Ne(C,S):[]}),l=T(i.value);D(i,(C,S)=>{JSON.stringify(C)!==JSON.stringify(S)&&(l.value=i.value)});const v=y(()=>s.value.sidebar!==!1&&l.value.length>0&&s.value.layout!=="home"),p=y(()=>$?s.value.aside==null?t.value.aside==="left":s.value.aside==="left":!1),$=y(()=>s.value.layout==="home"?!1:s.value.aside!=null?!!s.value.aside:t.value.aside!==!1),V=y(()=>v.value&&o.value),b=y(()=>v.value?lt(l.value):[]);function P(){n.value=!0}function N(){n.value=!1}function A(){n.value?N():P()}return{isOpen:n,sidebar:l,sidebarGroups:b,hasSidebar:v,hasAside:$,leftAside:p,isSidebarEnabled:V,open:P,close:N,toggle:A}}function ut(s,e){let t;Y(()=>{t=s.value?document.activeElement:void 0}),j(()=>{window.addEventListener("keyup",o)}),ve(()=>{window.removeEventListener("keyup",o)});function o(n){n.key==="Escape"&&s.value&&(e(),t==null||t.focus())}}function dt(s){const{page:e,hash:t}=L(),o=T(!1),n=y(()=>s.value.collapsed!=null),i=y(()=>!!s.value.link),l=T(!1),v=()=>{l.value=W(e.value.relativePath,s.value.link)};D([e,s,t],v),j(v);const p=y(()=>l.value?!0:s.value.items?le(e.value.relativePath,s.value.items):!1),$=y(()=>!!(s.value.items&&s.value.items.length));Y(()=>{o.value=!!(n.value&&s.value.collapsed)}),pe(()=>{(l.value||p.value)&&(o.value=!1)});function V(){n.value&&(o.value=!o.value)}return{collapsed:o,collapsible:n,isLink:i,isActiveLink:l,hasActiveLink:p,hasChildren:$,toggle:V}}function vt(){const{hasSidebar:s}=O(),e=ae("(min-width: 960px)"),t=ae("(min-width: 1280px)");return{isAsideEnabled:y(()=>!t.value&&!e.value?!1:s.value?t.value:e.value)}}const pt=/\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/,ce=[];function Me(s){return typeof s.outline=="object"&&!Array.isArray(s.outline)&&s.outline.label||s.outlineTitle||"On this page"}function he(s){const e=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const o=Number(t.tagName[1]);return{element:t,title:ft(t),link:"#"+t.id,level:o}});return ht(e,s)}function ft(s){let e="";for(const t of s.childNodes)if(t.nodeType===1){if(pt.test(t.className))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function ht(s,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[o,n]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;return kt(s,o,n)}function mt(s,e){const{isAsideEnabled:t}=vt(),o=Ze(i,100);let n=null;j(()=>{requestAnimationFrame(i),window.addEventListener("scroll",o)}),Fe(()=>{l(location.hash)}),ve(()=>{window.removeEventListener("scroll",o)});function i(){if(!t.value)return;const v=window.scrollY,p=window.innerHeight,$=document.body.offsetHeight,V=Math.abs(v+p-$)<1,b=ce.map(({element:N,link:A})=>({link:A,top:_t(N)})).filter(({top:N})=>!Number.isNaN(N)).sort((N,A)=>N.top-A.top);if(!b.length){l(null);return}if(v<1){l(null);return}if(V){l(b[b.length-1].link);return}let P=null;for(const{link:N,top:A}of b){if(A>v+De()+4)break;P=N}l(P)}function l(v){n&&n.classList.remove("active"),v==null?n=null:n=s.value.querySelector(`a[href="${decodeURIComponent(v)}"]`);const p=n;p?(p.classList.add("active"),e.value.style.top=p.offsetTop+39+"px",e.value.style.opacity="1"):(e.value.style.top="33px",e.value.style.opacity="0")}}function _t(s){let e=0;for(;s!==document.body;){if(s===null)return NaN;e+=s.offsetTop,s=s.offsetParent}return e}function kt(s,e,t){ce.length=0;const o=[],n=[];return s.forEach(i=>{const l={...i,children:[]};let v=n[n.length-1];for(;v&&v.level>=l.level;)n.pop(),v=n[n.length-1];if(l.element.classList.contains("ignore-header")||v&&"shouldIgnore"in v){n.push({level:l.level,shouldIgnore:!0});return}l.level>t||l.level{const n=K("VPDocOutlineItem",!0);return a(),u("ul",{class:M(["VPDocOutlineItem",t.root?"root":"nested"])},[(a(!0),u(w,null,H(t.headers,({children:i,link:l,title:v})=>(a(),u("li",null,[d("a",{class:"outline-link",href:l,onClick:e,title:v},I(v),9,bt),i!=null&&i.length?(a(),k(n,{key:0,headers:i},null,8,["headers"])):h("",!0)]))),256))],2)}}}),Ie=g(gt,[["__scopeId","data-v-b933a997"]]),$t={class:"content"},yt={"aria-level":"2",class:"outline-title",id:"doc-outline-aria-label",role:"heading"},Pt=m({__name:"VPDocAsideOutline",setup(s){const{frontmatter:e,theme:t}=L(),o=$e([]);Q(()=>{o.value=he(e.value.outline??t.value.outline)});const n=T(),i=T();return mt(n,i),(l,v)=>(a(),u("nav",{"aria-labelledby":"doc-outline-aria-label",class:M(["VPDocAsideOutline",{"has-outline":o.value.length>0}]),ref_key:"container",ref:n},[d("div",$t,[d("div",{class:"outline-marker",ref_key:"marker",ref:i},null,512),d("div",yt,I(r(Me)(r(t))),1),_(Ie,{headers:o.value,root:!0},null,8,["headers"])])],2))}}),Lt=g(Pt,[["__scopeId","data-v-a5bbad30"]]),Vt={class:"VPDocAsideCarbonAds"},St=m({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(s){const e=()=>null;return(t,o)=>(a(),u("div",Vt,[_(r(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),Tt={class:"VPDocAside"},Nt=m({__name:"VPDocAside",setup(s){const{theme:e}=L();return(t,o)=>(a(),u("div",Tt,[c(t.$slots,"aside-top",{},void 0,!0),c(t.$slots,"aside-outline-before",{},void 0,!0),_(Lt),c(t.$slots,"aside-outline-after",{},void 0,!0),o[0]||(o[0]=d("div",{class:"spacer"},null,-1)),c(t.$slots,"aside-ads-before",{},void 0,!0),r(e).carbonAds?(a(),k(St,{key:0,"carbon-ads":r(e).carbonAds},null,8,["carbon-ads"])):h("",!0),c(t.$slots,"aside-ads-after",{},void 0,!0),c(t.$slots,"aside-bottom",{},void 0,!0)]))}}),Mt=g(Nt,[["__scopeId","data-v-3f215769"]]);function It(){const{theme:s,page:e}=L();return y(()=>{const{text:t="Edit this page",pattern:o=""}=s.value.editLink||{};let n;return typeof o=="function"?n=o(e.value):n=o.replace(/:path/g,e.value.filePath),{url:n,text:t}})}function wt(){const{page:s,theme:e,frontmatter:t}=L();return y(()=>{var $,V,b,P,N,A,C,S;const o=Ne(e.value.sidebar,s.value.relativePath),n=ct(o),i=At(n,B=>B.link.replace(/[?#].*$/,"")),l=i.findIndex(B=>W(s.value.relativePath,B.link)),v=(($=e.value.docFooter)==null?void 0:$.prev)===!1&&!t.value.prev||t.value.prev===!1,p=((V=e.value.docFooter)==null?void 0:V.next)===!1&&!t.value.next||t.value.next===!1;return{prev:v?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((b=i[l-1])==null?void 0:b.docFooterText)??((P=i[l-1])==null?void 0:P.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((N=i[l-1])==null?void 0:N.link)},next:p?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((A=i[l+1])==null?void 0:A.docFooterText)??((C=i[l+1])==null?void 0:C.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((S=i[l+1])==null?void 0:S.link)}}})}function At(s,e){const t=new Set;return s.filter(o=>{const n=e(o);return t.has(n)?!1:t.add(n)})}const F=m({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(s){const e=s,t=y(()=>e.tag??(e.href?"a":"span")),o=y(()=>e.href&&ye.test(e.href)||e.target==="_blank");return(n,i)=>(a(),k(E(t.value),{class:M(["VPLink",{link:n.href,"vp-external-link-icon":o.value,"no-icon":n.noIcon}]),href:n.href?r(fe)(n.href):void 0,target:n.target??(o.value?"_blank":void 0),rel:n.rel??(o.value?"noreferrer":void 0)},{default:f(()=>[c(n.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),Ct={class:"VPLastUpdated"},Ht=["datetime"],Bt=m({__name:"VPDocFooterLastUpdated",setup(s){const{theme:e,page:t,lang:o}=L(),n=y(()=>new Date(t.value.lastUpdated)),i=y(()=>n.value.toISOString()),l=T("");return j(()=>{Y(()=>{var v,p,$;l.value=new Intl.DateTimeFormat((p=(v=e.value.lastUpdated)==null?void 0:v.formatOptions)!=null&&p.forceLocale?o.value:void 0,(($=e.value.lastUpdated)==null?void 0:$.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(n.value)})}),(v,p)=>{var $;return a(),u("p",Ct,[z(I((($=r(e).lastUpdated)==null?void 0:$.text)||r(e).lastUpdatedText||"Last updated")+": ",1),d("time",{datetime:i.value},I(l.value),9,Ht)])}}}),Et=g(Bt,[["__scopeId","data-v-e98dd255"]]),Ft={key:0,class:"VPDocFooter"},Dt={key:0,class:"edit-info"},Ot={key:0,class:"edit-link"},Gt={key:1,class:"last-updated"},Ut={key:1,class:"prev-next","aria-labelledby":"doc-footer-aria-label"},jt={class:"pager"},zt=["innerHTML"],Wt=["innerHTML"],Kt={class:"pager"},qt=["innerHTML"],Rt=["innerHTML"],Jt=m({__name:"VPDocFooter",setup(s){const{theme:e,page:t,frontmatter:o}=L(),n=It(),i=wt(),l=y(()=>e.value.editLink&&o.value.editLink!==!1),v=y(()=>t.value.lastUpdated),p=y(()=>l.value||v.value||i.value.prev||i.value.next);return($,V)=>{var b,P,N,A;return p.value?(a(),u("footer",Ft,[c($.$slots,"doc-footer-before",{},void 0,!0),l.value||v.value?(a(),u("div",Dt,[l.value?(a(),u("div",Ot,[_(F,{class:"edit-link-button",href:r(n).url,"no-icon":!0},{default:f(()=>[V[0]||(V[0]=d("span",{class:"vpi-square-pen edit-link-icon"},null,-1)),z(" "+I(r(n).text),1)]),_:1},8,["href"])])):h("",!0),v.value?(a(),u("div",Gt,[_(Et)])):h("",!0)])):h("",!0),(b=r(i).prev)!=null&&b.link||(P=r(i).next)!=null&&P.link?(a(),u("nav",Ut,[V[1]||(V[1]=d("span",{class:"visually-hidden",id:"doc-footer-aria-label"},"Pager",-1)),d("div",jt,[(N=r(i).prev)!=null&&N.link?(a(),k(F,{key:0,class:"pager-link prev",href:r(i).prev.link},{default:f(()=>{var C;return[d("span",{class:"desc",innerHTML:((C=r(e).docFooter)==null?void 0:C.prev)||"Previous page"},null,8,zt),d("span",{class:"title",innerHTML:r(i).prev.text},null,8,Wt)]}),_:1},8,["href"])):h("",!0)]),d("div",Kt,[(A=r(i).next)!=null&&A.link?(a(),k(F,{key:0,class:"pager-link next",href:r(i).next.link},{default:f(()=>{var C;return[d("span",{class:"desc",innerHTML:((C=r(e).docFooter)==null?void 0:C.next)||"Next page"},null,8,qt),d("span",{class:"title",innerHTML:r(i).next.text},null,8,Rt)]}),_:1},8,["href"])):h("",!0)])])):h("",!0)])):h("",!0)}}}),Xt=g(Jt,[["__scopeId","data-v-e257564d"]]),Yt={class:"container"},Qt={class:"aside-container"},Zt={class:"aside-content"},xt={class:"content"},en={class:"content-container"},tn={class:"main"},nn=m({__name:"VPDoc",setup(s){const{theme:e}=L(),t=Z(),{hasSidebar:o,hasAside:n,leftAside:i}=O(),l=y(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(v,p)=>{const $=K("Content");return a(),u("div",{class:M(["VPDoc",{"has-sidebar":r(o),"has-aside":r(n)}])},[c(v.$slots,"doc-top",{},void 0,!0),d("div",Yt,[r(n)?(a(),u("div",{key:0,class:M(["aside",{"left-aside":r(i)}])},[p[0]||(p[0]=d("div",{class:"aside-curtain"},null,-1)),d("div",Qt,[d("div",Zt,[_(Mt,null,{"aside-top":f(()=>[c(v.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[c(v.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[c(v.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(v.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(v.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(v.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):h("",!0),d("div",xt,[d("div",en,[c(v.$slots,"doc-before",{},void 0,!0),d("main",tn,[_($,{class:M(["vp-doc",[l.value,r(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),_(Xt,null,{"doc-footer-before":f(()=>[c(v.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),c(v.$slots,"doc-after",{},void 0,!0)])])]),c(v.$slots,"doc-bottom",{},void 0,!0)],2)}}}),on=g(nn,[["__scopeId","data-v-39a288b8"]]),sn=m({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(s){const e=s,t=y(()=>e.href&&ye.test(e.href)),o=y(()=>e.tag||(e.href?"a":"button"));return(n,i)=>(a(),k(E(o.value),{class:M(["VPButton",[n.size,n.theme]]),href:n.href?r(fe)(n.href):void 0,target:e.target??(t.value?"_blank":void 0),rel:e.rel??(t.value?"noreferrer":void 0)},{default:f(()=>[z(I(n.text),1)]),_:1},8,["class","href","target","rel"]))}}),an=g(sn,[["__scopeId","data-v-fa7799d5"]]),rn=["src","alt"],ln=m({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(s){return(e,t)=>{const o=K("VPImage",!0);return e.image?(a(),u(w,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),u("img",U({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:r(de)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,rn)):(a(),u(w,{key:1},[_(o,U({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),_(o,U({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):h("",!0)}}}),X=g(ln,[["__scopeId","data-v-8426fc1a"]]),cn={class:"container"},un={class:"main"},dn={class:"heading"},vn=["innerHTML"],pn=["innerHTML"],fn=["innerHTML"],hn={key:0,class:"actions"},mn={key:0,class:"image"},_n={class:"image-container"},kn=m({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(s){const e=x("hero-image-slot-exists");return(t,o)=>(a(),u("div",{class:M(["VPHero",{"has-image":t.image||r(e)}])},[d("div",cn,[d("div",un,[c(t.$slots,"home-hero-info-before",{},void 0,!0),c(t.$slots,"home-hero-info",{},()=>[d("h1",dn,[t.name?(a(),u("span",{key:0,innerHTML:t.name,class:"name clip"},null,8,vn)):h("",!0),t.text?(a(),u("span",{key:1,innerHTML:t.text,class:"text"},null,8,pn)):h("",!0)]),t.tagline?(a(),u("p",{key:0,innerHTML:t.tagline,class:"tagline"},null,8,fn)):h("",!0)],!0),c(t.$slots,"home-hero-info-after",{},void 0,!0),t.actions?(a(),u("div",hn,[(a(!0),u(w,null,H(t.actions,n=>(a(),u("div",{key:n.link,class:"action"},[_(an,{tag:"a",size:"medium",theme:n.theme,text:n.text,href:n.link,target:n.target,rel:n.rel},null,8,["theme","text","href","target","rel"])]))),128))])):h("",!0),c(t.$slots,"home-hero-actions-after",{},void 0,!0)]),t.image||r(e)?(a(),u("div",mn,[d("div",_n,[o[0]||(o[0]=d("div",{class:"image-bg"},null,-1)),c(t.$slots,"home-hero-image",{},()=>[t.image?(a(),k(X,{key:0,class:"image-src",image:t.image},null,8,["image"])):h("",!0)],!0)])])):h("",!0)])],2))}}),bn=g(kn,[["__scopeId","data-v-4f9c455b"]]),gn=m({__name:"VPHomeHero",setup(s){const{frontmatter:e}=L();return(t,o)=>r(e).hero?(a(),k(bn,{key:0,class:"VPHomeHero",name:r(e).hero.name,text:r(e).hero.text,tagline:r(e).hero.tagline,image:r(e).hero.image,actions:r(e).hero.actions},{"home-hero-info-before":f(()=>[c(t.$slots,"home-hero-info-before")]),"home-hero-info":f(()=>[c(t.$slots,"home-hero-info")]),"home-hero-info-after":f(()=>[c(t.$slots,"home-hero-info-after")]),"home-hero-actions-after":f(()=>[c(t.$slots,"home-hero-actions-after")]),"home-hero-image":f(()=>[c(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):h("",!0)}}),$n={class:"box"},yn={key:0,class:"icon"},Pn=["innerHTML"],Ln=["innerHTML"],Vn=["innerHTML"],Sn={key:4,class:"link-text"},Tn={class:"link-text-value"},Nn=m({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(s){return(e,t)=>(a(),k(F,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:f(()=>[d("article",$n,[typeof e.icon=="object"&&e.icon.wrap?(a(),u("div",yn,[_(X,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(a(),k(X,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(a(),u("div",{key:2,class:"icon",innerHTML:e.icon},null,8,Pn)):h("",!0),d("h2",{class:"title",innerHTML:e.title},null,8,Ln),e.details?(a(),u("p",{key:3,class:"details",innerHTML:e.details},null,8,Vn)):h("",!0),e.linkText?(a(),u("div",Sn,[d("p",Tn,[z(I(e.linkText)+" ",1),t[0]||(t[0]=d("span",{class:"vpi-arrow-right link-text-icon"},null,-1))])])):h("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),Mn=g(Nn,[["__scopeId","data-v-a3976bdc"]]),In={key:0,class:"VPFeatures"},wn={class:"container"},An={class:"items"},Cn=m({__name:"VPFeatures",props:{features:{}},setup(s){const e=s,t=y(()=>{const o=e.features.length;if(o){if(o===2)return"grid-2";if(o===3)return"grid-3";if(o%3===0)return"grid-6";if(o>3)return"grid-4"}else return});return(o,n)=>o.features?(a(),u("div",In,[d("div",wn,[d("div",An,[(a(!0),u(w,null,H(o.features,i=>(a(),u("div",{key:i.title,class:M(["item",[t.value]])},[_(Mn,{icon:i.icon,title:i.title,details:i.details,link:i.link,"link-text":i.linkText,rel:i.rel,target:i.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):h("",!0)}}),Hn=g(Cn,[["__scopeId","data-v-a6181336"]]),Bn=m({__name:"VPHomeFeatures",setup(s){const{frontmatter:e}=L();return(t,o)=>r(e).features?(a(),k(Hn,{key:0,class:"VPHomeFeatures",features:r(e).features},null,8,["features"])):h("",!0)}}),En=m({__name:"VPHomeContent",setup(s){const{width:e}=Oe({initialWidth:0,includeScrollbar:!1});return(t,o)=>(a(),u("div",{class:"vp-doc container",style:Pe(r(e)?{"--vp-offset":`calc(50% - ${r(e)/2}px)`}:{})},[c(t.$slots,"default",{},void 0,!0)],4))}}),Fn=g(En,[["__scopeId","data-v-8e2d4988"]]),Dn=m({__name:"VPHome",setup(s){const{frontmatter:e,theme:t}=L();return(o,n)=>{const i=K("Content");return a(),u("div",{class:M(["VPHome",{"external-link-icon-enabled":r(t).externalLinkIcon}])},[c(o.$slots,"home-hero-before",{},void 0,!0),_(gn,null,{"home-hero-info-before":f(()=>[c(o.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(o.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(o.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(o.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(o.$slots,"home-hero-image",{},void 0,!0)]),_:3}),c(o.$slots,"home-hero-after",{},void 0,!0),c(o.$slots,"home-features-before",{},void 0,!0),_(Bn),c(o.$slots,"home-features-after",{},void 0,!0),r(e).markdownStyles!==!1?(a(),k(Fn,{key:0},{default:f(()=>[_(i)]),_:1})):(a(),k(i,{key:1}))],2)}}}),On=g(Dn,[["__scopeId","data-v-8b561e3d"]]),Gn={},Un={class:"VPPage"};function jn(s,e){const t=K("Content");return a(),u("div",Un,[c(s.$slots,"page-top"),_(t),c(s.$slots,"page-bottom")])}const zn=g(Gn,[["render",jn]]),Wn=m({__name:"VPContent",setup(s){const{page:e,frontmatter:t}=L(),{hasSidebar:o}=O();return(n,i)=>(a(),u("div",{class:M(["VPContent",{"has-sidebar":r(o),"is-home":r(t).layout==="home"}]),id:"VPContent"},[r(e).isNotFound?c(n.$slots,"not-found",{key:0},()=>[_(it)],!0):r(t).layout==="page"?(a(),k(zn,{key:1},{"page-top":f(()=>[c(n.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[c(n.$slots,"page-bottom",{},void 0,!0)]),_:3})):r(t).layout==="home"?(a(),k(On,{key:2},{"home-hero-before":f(()=>[c(n.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[c(n.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(n.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(n.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(n.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(n.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[c(n.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[c(n.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[c(n.$slots,"home-features-after",{},void 0,!0)]),_:3})):r(t).layout&&r(t).layout!=="doc"?(a(),k(E(r(t).layout),{key:3})):(a(),k(on,{key:4},{"doc-top":f(()=>[c(n.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[c(n.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":f(()=>[c(n.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[c(n.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[c(n.$slots,"doc-after",{},void 0,!0)]),"aside-top":f(()=>[c(n.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":f(()=>[c(n.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(n.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(n.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(n.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":f(()=>[c(n.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),Kn=g(Wn,[["__scopeId","data-v-1428d186"]]),qn={class:"container"},Rn=["innerHTML"],Jn=["innerHTML"],Xn=m({__name:"VPFooter",setup(s){const{theme:e,frontmatter:t}=L(),{hasSidebar:o}=O();return(n,i)=>r(e).footer&&r(t).footer!==!1?(a(),u("footer",{key:0,class:M(["VPFooter",{"has-sidebar":r(o)}])},[d("div",qn,[r(e).footer.message?(a(),u("p",{key:0,class:"message",innerHTML:r(e).footer.message},null,8,Rn)):h("",!0),r(e).footer.copyright?(a(),u("p",{key:1,class:"copyright",innerHTML:r(e).footer.copyright},null,8,Jn)):h("",!0)])],2)):h("",!0)}}),Yn=g(Xn,[["__scopeId","data-v-e315a0ad"]]);function Qn(){const{theme:s,frontmatter:e}=L(),t=$e([]),o=y(()=>t.value.length>0);return Q(()=>{t.value=he(e.value.outline??s.value.outline)}),{headers:t,hasLocalNav:o}}const Zn={class:"menu-text"},xn={class:"header"},eo={class:"outline"},to=m({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(s){const e=s,{theme:t}=L(),o=T(!1),n=T(0),i=T(),l=T();function v(b){var P;(P=i.value)!=null&&P.contains(b.target)||(o.value=!1)}D(o,b=>{if(b){document.addEventListener("click",v);return}document.removeEventListener("click",v)}),re("Escape",()=>{o.value=!1}),Q(()=>{o.value=!1});function p(){o.value=!o.value,n.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function $(b){b.target.classList.contains("outline-link")&&(l.value&&(l.value.style.transition="none"),Le(()=>{o.value=!1}))}function V(){o.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(b,P)=>(a(),u("div",{class:"VPLocalNavOutlineDropdown",style:Pe({"--vp-vh":n.value+"px"}),ref_key:"main",ref:i},[b.headers.length>0?(a(),u("button",{key:0,onClick:p,class:M({open:o.value})},[d("span",Zn,I(r(Me)(r(t))),1),P[0]||(P[0]=d("span",{class:"vpi-chevron-right icon"},null,-1))],2)):(a(),u("button",{key:1,onClick:V},I(r(t).returnToTopLabel||"Return to top"),1)),_(ue,{name:"flyout"},{default:f(()=>[o.value?(a(),u("div",{key:0,ref_key:"items",ref:l,class:"items",onClick:$},[d("div",xn,[d("a",{class:"top-link",href:"#",onClick:V},I(r(t).returnToTopLabel||"Return to top"),1)]),d("div",eo,[_(Ie,{headers:b.headers},null,8,["headers"])])],512)):h("",!0)]),_:1})],4))}}),no=g(to,[["__scopeId","data-v-8a42e2b4"]]),oo={class:"container"},so=["aria-expanded"],ao={class:"menu-text"},ro=m({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(s){const{theme:e,frontmatter:t}=L(),{hasSidebar:o}=O(),{headers:n}=Qn(),{y:i}=Ve(),l=T(0);j(()=>{l.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),Q(()=>{n.value=he(t.value.outline??e.value.outline)});const v=y(()=>n.value.length===0),p=y(()=>v.value&&!o.value),$=y(()=>({VPLocalNav:!0,"has-sidebar":o.value,empty:v.value,fixed:p.value}));return(V,b)=>r(t).layout!=="home"&&(!p.value||r(i)>=l.value)?(a(),u("div",{key:0,class:M($.value)},[d("div",oo,[r(o)?(a(),u("button",{key:0,class:"menu","aria-expanded":V.open,"aria-controls":"VPSidebarNav",onClick:b[0]||(b[0]=P=>V.$emit("open-menu"))},[b[1]||(b[1]=d("span",{class:"vpi-align-left menu-icon"},null,-1)),d("span",ao,I(r(e).sidebarMenuLabel||"Menu"),1)],8,so)):h("",!0),_(no,{headers:r(n),navHeight:l.value},null,8,["headers","navHeight"])])],2)):h("",!0)}}),io=g(ro,[["__scopeId","data-v-a6f0e41e"]]);function lo(){const s=T(!1);function e(){s.value=!0,window.addEventListener("resize",n)}function t(){s.value=!1,window.removeEventListener("resize",n)}function o(){s.value?t():e()}function n(){window.outerWidth>=768&&t()}const i=Z();return D(()=>i.path,t),{isScreenOpen:s,openScreen:e,closeScreen:t,toggleScreen:o}}const co={},uo={class:"VPSwitch",type:"button",role:"switch"},vo={class:"check"},po={key:0,class:"icon"};function fo(s,e){return a(),u("button",uo,[d("span",vo,[s.$slots.default?(a(),u("span",po,[c(s.$slots,"default",{},void 0,!0)])):h("",!0)])])}const ho=g(co,[["render",fo],["__scopeId","data-v-1d5665e3"]]),mo=m({__name:"VPSwitchAppearance",setup(s){const{isDark:e,theme:t}=L(),o=x("toggle-appearance",()=>{e.value=!e.value}),n=T("");return pe(()=>{n.value=e.value?t.value.lightModeSwitchTitle||"Switch to light theme":t.value.darkModeSwitchTitle||"Switch to dark theme"}),(i,l)=>(a(),k(ho,{title:n.value,class:"VPSwitchAppearance","aria-checked":r(e),onClick:r(o)},{default:f(()=>l[0]||(l[0]=[d("span",{class:"vpi-sun sun"},null,-1),d("span",{class:"vpi-moon moon"},null,-1)])),_:1},8,["title","aria-checked","onClick"]))}}),me=g(mo,[["__scopeId","data-v-5337faa4"]]),_o={key:0,class:"VPNavBarAppearance"},ko=m({__name:"VPNavBarAppearance",setup(s){const{site:e}=L();return(t,o)=>r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),u("div",_o,[_(me)])):h("",!0)}}),bo=g(ko,[["__scopeId","data-v-6c893767"]]),_e=T();let we=!1,se=0;function go(s){const e=T(!1);if(ee){!we&&$o(),se++;const t=D(_e,o=>{var n,i,l;o===s.el.value||(n=s.el.value)!=null&&n.contains(o)?(e.value=!0,(i=s.onFocus)==null||i.call(s)):(e.value=!1,(l=s.onBlur)==null||l.call(s))});ve(()=>{t(),se--,se||yo()})}return Ge(e)}function $o(){document.addEventListener("focusin",Ae),we=!0,_e.value=document.activeElement}function yo(){document.removeEventListener("focusin",Ae)}function Ae(){_e.value=document.activeElement}const Po={class:"VPMenuLink"},Lo=["innerHTML"],Vo=m({__name:"VPMenuLink",props:{item:{}},setup(s){const{page:e}=L();return(t,o)=>(a(),u("div",Po,[_(F,{class:M({active:r(W)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon},{default:f(()=>[d("span",{innerHTML:t.item.text},null,8,Lo)]),_:1},8,["class","href","target","rel","no-icon"])]))}}),te=g(Vo,[["__scopeId","data-v-35975db6"]]),So={class:"VPMenuGroup"},To={key:0,class:"title"},No=m({__name:"VPMenuGroup",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),u("div",So,[e.text?(a(),u("p",To,I(e.text),1)):h("",!0),(a(!0),u(w,null,H(e.items,o=>(a(),u(w,null,["link"in o?(a(),k(te,{key:0,item:o},null,8,["item"])):h("",!0)],64))),256))]))}}),Mo=g(No,[["__scopeId","data-v-69e747b5"]]),Io={class:"VPMenu"},wo={key:0,class:"items"},Ao=m({__name:"VPMenu",props:{items:{}},setup(s){return(e,t)=>(a(),u("div",Io,[e.items?(a(),u("div",wo,[(a(!0),u(w,null,H(e.items,o=>(a(),u(w,{key:JSON.stringify(o)},["link"in o?(a(),k(te,{key:0,item:o},null,8,["item"])):"component"in o?(a(),k(E(o.component),U({key:1,ref_for:!0},o.props),null,16)):(a(),k(Mo,{key:2,text:o.text,items:o.items},null,8,["text","items"]))],64))),128))])):h("",!0),c(e.$slots,"default",{},void 0,!0)]))}}),Co=g(Ao,[["__scopeId","data-v-b98bc113"]]),Ho=["aria-expanded","aria-label"],Bo={key:0,class:"text"},Eo=["innerHTML"],Fo={key:1,class:"vpi-more-horizontal icon"},Do={class:"menu"},Oo=m({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(s){const e=T(!1),t=T();go({el:t,onBlur:o});function o(){e.value=!1}return(n,i)=>(a(),u("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:i[1]||(i[1]=l=>e.value=!0),onMouseleave:i[2]||(i[2]=l=>e.value=!1)},[d("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":n.label,onClick:i[0]||(i[0]=l=>e.value=!e.value)},[n.button||n.icon?(a(),u("span",Bo,[n.icon?(a(),u("span",{key:0,class:M([n.icon,"option-icon"])},null,2)):h("",!0),n.button?(a(),u("span",{key:1,innerHTML:n.button},null,8,Eo)):h("",!0),i[3]||(i[3]=d("span",{class:"vpi-chevron-down text-icon"},null,-1))])):(a(),u("span",Fo))],8,Ho),d("div",Do,[_(Co,{items:n.items},{default:f(()=>[c(n.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),ke=g(Oo,[["__scopeId","data-v-cf11d7a2"]]),Go=["href","aria-label","innerHTML"],Uo=m({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(s){const e=s,t=T();j(async()=>{var i;await Le();const n=(i=t.value)==null?void 0:i.children[0];n instanceof HTMLElement&&n.className.startsWith("vpi-social-")&&(getComputedStyle(n).maskImage||getComputedStyle(n).webkitMaskImage)==="none"&&n.style.setProperty("--icon",`url('https://api.iconify.design/simple-icons/${e.icon}.svg')`)});const o=y(()=>typeof e.icon=="object"?e.icon.svg:``);return(n,i)=>(a(),u("a",{ref_key:"el",ref:t,class:"VPSocialLink no-icon",href:n.link,"aria-label":n.ariaLabel??(typeof n.icon=="string"?n.icon:""),target:"_blank",rel:"noopener",innerHTML:o.value},null,8,Go))}}),jo=g(Uo,[["__scopeId","data-v-bd121fe5"]]),zo={class:"VPSocialLinks"},Wo=m({__name:"VPSocialLinks",props:{links:{}},setup(s){return(e,t)=>(a(),u("div",zo,[(a(!0),u(w,null,H(e.links,({link:o,icon:n,ariaLabel:i})=>(a(),k(jo,{key:o,icon:n,link:o,ariaLabel:i},null,8,["icon","link","ariaLabel"]))),128))]))}}),be=g(Wo,[["__scopeId","data-v-7bc22406"]]),Ko={key:0,class:"group translations"},qo={class:"trans-title"},Ro={key:1,class:"group"},Jo={class:"item appearance"},Xo={class:"label"},Yo={class:"appearance-action"},Qo={key:2,class:"group"},Zo={class:"item social-links"},xo=m({__name:"VPNavBarExtra",setup(s){const{site:e,theme:t}=L(),{localeLinks:o,currentLang:n}=R({correspondingLink:!0}),i=y(()=>o.value.length&&n.value.label||e.value.appearance||t.value.socialLinks);return(l,v)=>i.value?(a(),k(ke,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:f(()=>[r(o).length&&r(n).label?(a(),u("div",Ko,[d("p",qo,I(r(n).label),1),(a(!0),u(w,null,H(r(o),p=>(a(),k(te,{key:p.link,item:p},null,8,["item"]))),128))])):h("",!0),r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),u("div",Ro,[d("div",Jo,[d("p",Xo,I(r(t).darkModeSwitchLabel||"Appearance"),1),d("div",Yo,[_(me)])])])):h("",!0),r(t).socialLinks?(a(),u("div",Qo,[d("div",Zo,[_(be,{class:"social-links-list",links:r(t).socialLinks},null,8,["links"])])])):h("",!0)]),_:1})):h("",!0)}}),es=g(xo,[["__scopeId","data-v-bb2aa2f0"]]),ts=["aria-expanded"],ns=m({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(s){return(e,t)=>(a(),u("button",{type:"button",class:M(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=o=>e.$emit("click"))},t[1]||(t[1]=[d("span",{class:"container"},[d("span",{class:"top"}),d("span",{class:"middle"}),d("span",{class:"bottom"})],-1)]),10,ts))}}),os=g(ns,[["__scopeId","data-v-e5dd9c1c"]]),ss=["innerHTML"],as=m({__name:"VPNavBarMenuLink",props:{item:{}},setup(s){const{page:e}=L();return(t,o)=>(a(),k(F,{class:M({VPNavBarMenuLink:!0,active:r(W)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,tabindex:"0"},{default:f(()=>[d("span",{innerHTML:t.item.text},null,8,ss)]),_:1},8,["class","href","target","rel","no-icon"]))}}),rs=g(as,[["__scopeId","data-v-e56f3d57"]]),is=m({__name:"VPNavBarMenuGroup",props:{item:{}},setup(s){const e=s,{page:t}=L(),o=i=>"component"in i?!1:"link"in i?W(t.value.relativePath,i.link,!!e.item.activeMatch):i.items.some(o),n=y(()=>o(e.item));return(i,l)=>(a(),k(ke,{class:M({VPNavBarMenuGroup:!0,active:r(W)(r(t).relativePath,i.item.activeMatch,!!i.item.activeMatch)||n.value}),button:i.item.text,items:i.item.items},null,8,["class","button","items"]))}}),ls={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},cs=m({__name:"VPNavBarMenu",setup(s){const{theme:e}=L();return(t,o)=>r(e).nav?(a(),u("nav",ls,[o[0]||(o[0]=d("span",{id:"main-nav-aria-label",class:"visually-hidden"}," Main Navigation ",-1)),(a(!0),u(w,null,H(r(e).nav,n=>(a(),u(w,{key:JSON.stringify(n)},["link"in n?(a(),k(rs,{key:0,item:n},null,8,["item"])):"component"in n?(a(),k(E(n.component),U({key:1,ref_for:!0},n.props),null,16)):(a(),k(is,{key:2,item:n},null,8,["item"]))],64))),128))])):h("",!0)}}),us=g(cs,[["__scopeId","data-v-dc692963"]]);function ds(s){const{localeIndex:e,theme:t}=L();function o(n){var A,C,S;const i=n.split("."),l=(A=t.value.search)==null?void 0:A.options,v=l&&typeof l=="object",p=v&&((S=(C=l.locales)==null?void 0:C[e.value])==null?void 0:S.translations)||null,$=v&&l.translations||null;let V=p,b=$,P=s;const N=i.pop();for(const B of i){let G=null;const q=P==null?void 0:P[B];q&&(G=P=q);const ne=b==null?void 0:b[B];ne&&(G=b=ne);const oe=V==null?void 0:V[B];oe&&(G=V=oe),q||(P=G),ne||(b=G),oe||(V=G)}return(V==null?void 0:V[N])??(b==null?void 0:b[N])??(P==null?void 0:P[N])??""}return o}const vs=["aria-label"],ps={class:"DocSearch-Button-Container"},fs={class:"DocSearch-Button-Placeholder"},ge=m({__name:"VPNavBarSearchButton",setup(s){const t=ds({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(o,n)=>(a(),u("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":r(t)("button.buttonAriaLabel")},[d("span",ps,[n[0]||(n[0]=d("span",{class:"vp-icon DocSearch-Search-Icon"},null,-1)),d("span",fs,I(r(t)("button.buttonText")),1)]),n[1]||(n[1]=d("span",{class:"DocSearch-Button-Keys"},[d("kbd",{class:"DocSearch-Button-Key"}),d("kbd",{class:"DocSearch-Button-Key"},"K")],-1))],8,vs))}}),hs={class:"VPNavBarSearch"},ms={id:"local-search"},_s={key:1,id:"docsearch"},ks=m({__name:"VPNavBarSearch",setup(s){const e=Ue(()=>je(()=>import("./VPLocalSearchBox.Crn_Pjbv.js"),__vite__mapDeps([0,1]))),t=()=>null,{theme:o}=L(),n=T(!1),i=T(!1);j(()=>{});function l(){n.value||(n.value=!0,setTimeout(v,16))}function v(){const b=new Event("keydown");b.key="k",b.metaKey=!0,window.dispatchEvent(b),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||v()},16)}function p(b){const P=b.target,N=P.tagName;return P.isContentEditable||N==="INPUT"||N==="SELECT"||N==="TEXTAREA"}const $=T(!1);re("k",b=>{(b.ctrlKey||b.metaKey)&&(b.preventDefault(),$.value=!0)}),re("/",b=>{p(b)||(b.preventDefault(),$.value=!0)});const V="local";return(b,P)=>{var N;return a(),u("div",hs,[r(V)==="local"?(a(),u(w,{key:0},[$.value?(a(),k(r(e),{key:0,onClose:P[0]||(P[0]=A=>$.value=!1)})):h("",!0),d("div",ms,[_(ge,{onClick:P[1]||(P[1]=A=>$.value=!0)})])],64)):r(V)==="algolia"?(a(),u(w,{key:1},[n.value?(a(),k(r(t),{key:0,algolia:((N=r(o).search)==null?void 0:N.options)??r(o).algolia,onVnodeBeforeMount:P[2]||(P[2]=A=>i.value=!0)},null,8,["algolia"])):h("",!0),i.value?h("",!0):(a(),u("div",_s,[_(ge,{onClick:l})]))],64)):h("",!0)])}}}),bs=m({__name:"VPNavBarSocialLinks",setup(s){const{theme:e}=L();return(t,o)=>r(e).socialLinks?(a(),k(be,{key:0,class:"VPNavBarSocialLinks",links:r(e).socialLinks},null,8,["links"])):h("",!0)}}),gs=g(bs,[["__scopeId","data-v-0394ad82"]]),$s=["href","rel","target"],ys=["innerHTML"],Ps={key:2},Ls=m({__name:"VPNavBarTitle",setup(s){const{site:e,theme:t}=L(),{hasSidebar:o}=O(),{currentLang:n}=R(),i=y(()=>{var p;return typeof t.value.logoLink=="string"?t.value.logoLink:(p=t.value.logoLink)==null?void 0:p.link}),l=y(()=>{var p;return typeof t.value.logoLink=="string"||(p=t.value.logoLink)==null?void 0:p.rel}),v=y(()=>{var p;return typeof t.value.logoLink=="string"||(p=t.value.logoLink)==null?void 0:p.target});return(p,$)=>(a(),u("div",{class:M(["VPNavBarTitle",{"has-sidebar":r(o)}])},[d("a",{class:"title",href:i.value??r(fe)(r(n).link),rel:l.value,target:v.value},[c(p.$slots,"nav-bar-title-before",{},void 0,!0),r(t).logo?(a(),k(X,{key:0,class:"logo",image:r(t).logo},null,8,["image"])):h("",!0),r(t).siteTitle?(a(),u("span",{key:1,innerHTML:r(t).siteTitle},null,8,ys)):r(t).siteTitle===void 0?(a(),u("span",Ps,I(r(e).title),1)):h("",!0),c(p.$slots,"nav-bar-title-after",{},void 0,!0)],8,$s)],2))}}),Vs=g(Ls,[["__scopeId","data-v-1168a8e4"]]),Ss={class:"items"},Ts={class:"title"},Ns=m({__name:"VPNavBarTranslations",setup(s){const{theme:e}=L(),{localeLinks:t,currentLang:o}=R({correspondingLink:!0});return(n,i)=>r(t).length&&r(o).label?(a(),k(ke,{key:0,class:"VPNavBarTranslations",icon:"vpi-languages",label:r(e).langMenuLabel||"Change language"},{default:f(()=>[d("div",Ss,[d("p",Ts,I(r(o).label),1),(a(!0),u(w,null,H(r(t),l=>(a(),k(te,{key:l.link,item:l},null,8,["item"]))),128))])]),_:1},8,["label"])):h("",!0)}}),Ms=g(Ns,[["__scopeId","data-v-88af2de4"]]),Is={class:"wrapper"},ws={class:"container"},As={class:"title"},Cs={class:"content"},Hs={class:"content-body"},Bs=m({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(s){const e=s,{y:t}=Ve(),{hasSidebar:o}=O(),{frontmatter:n}=L(),i=T({});return pe(()=>{i.value={"has-sidebar":o.value,home:n.value.layout==="home",top:t.value===0,"screen-open":e.isScreenOpen}}),(l,v)=>(a(),u("div",{class:M(["VPNavBar",i.value])},[d("div",Is,[d("div",ws,[d("div",As,[_(Vs,null,{"nav-bar-title-before":f(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),d("div",Cs,[d("div",Hs,[c(l.$slots,"nav-bar-content-before",{},void 0,!0),_(ks,{class:"search"}),_(us,{class:"menu"}),_(Ms,{class:"translations"}),_(bo,{class:"appearance"}),_(gs,{class:"social-links"}),_(es,{class:"extra"}),c(l.$slots,"nav-bar-content-after",{},void 0,!0),_(os,{class:"hamburger",active:l.isScreenOpen,onClick:v[0]||(v[0]=p=>l.$emit("toggle-screen"))},null,8,["active"])])])])]),v[1]||(v[1]=d("div",{class:"divider"},[d("div",{class:"divider-line"})],-1))],2))}}),Es=g(Bs,[["__scopeId","data-v-6aa21345"]]),Fs={key:0,class:"VPNavScreenAppearance"},Ds={class:"text"},Os=m({__name:"VPNavScreenAppearance",setup(s){const{site:e,theme:t}=L();return(o,n)=>r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),u("div",Fs,[d("p",Ds,I(r(t).darkModeSwitchLabel||"Appearance"),1),_(me)])):h("",!0)}}),Gs=g(Os,[["__scopeId","data-v-b44890b2"]]),Us=["innerHTML"],js=m({__name:"VPNavScreenMenuLink",props:{item:{}},setup(s){const e=x("close-screen");return(t,o)=>(a(),k(F,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:r(e)},{default:f(()=>[d("span",{innerHTML:t.item.text},null,8,Us)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),zs=g(js,[["__scopeId","data-v-df37e6dd"]]),Ws=["innerHTML"],Ks=m({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(s){const e=x("close-screen");return(t,o)=>(a(),k(F,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:r(e)},{default:f(()=>[d("span",{innerHTML:t.item.text},null,8,Ws)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),Ce=g(Ks,[["__scopeId","data-v-3e9c20e4"]]),qs={class:"VPNavScreenMenuGroupSection"},Rs={key:0,class:"title"},Js=m({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),u("div",qs,[e.text?(a(),u("p",Rs,I(e.text),1)):h("",!0),(a(!0),u(w,null,H(e.items,o=>(a(),k(Ce,{key:o.text,item:o},null,8,["item"]))),128))]))}}),Xs=g(Js,[["__scopeId","data-v-8133b170"]]),Ys=["aria-controls","aria-expanded"],Qs=["innerHTML"],Zs=["id"],xs={key:0,class:"item"},ea={key:1,class:"item"},ta={key:2,class:"group"},na=m({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(s){const e=s,t=T(!1),o=y(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function n(){t.value=!t.value}return(i,l)=>(a(),u("div",{class:M(["VPNavScreenMenuGroup",{open:t.value}])},[d("button",{class:"button","aria-controls":o.value,"aria-expanded":t.value,onClick:n},[d("span",{class:"button-text",innerHTML:i.text},null,8,Qs),l[0]||(l[0]=d("span",{class:"vpi-plus button-icon"},null,-1))],8,Ys),d("div",{id:o.value,class:"items"},[(a(!0),u(w,null,H(i.items,v=>(a(),u(w,{key:JSON.stringify(v)},["link"in v?(a(),u("div",xs,[_(Ce,{item:v},null,8,["item"])])):"component"in v?(a(),u("div",ea,[(a(),k(E(v.component),U({ref_for:!0},v.props,{"screen-menu":""}),null,16))])):(a(),u("div",ta,[_(Xs,{text:v.text,items:v.items},null,8,["text","items"])]))],64))),128))],8,Zs)],2))}}),oa=g(na,[["__scopeId","data-v-b9ab8c58"]]),sa={key:0,class:"VPNavScreenMenu"},aa=m({__name:"VPNavScreenMenu",setup(s){const{theme:e}=L();return(t,o)=>r(e).nav?(a(),u("nav",sa,[(a(!0),u(w,null,H(r(e).nav,n=>(a(),u(w,{key:JSON.stringify(n)},["link"in n?(a(),k(zs,{key:0,item:n},null,8,["item"])):"component"in n?(a(),k(E(n.component),U({key:1,ref_for:!0},n.props,{"screen-menu":""}),null,16)):(a(),k(oa,{key:2,text:n.text||"",items:n.items},null,8,["text","items"]))],64))),128))])):h("",!0)}}),ra=m({__name:"VPNavScreenSocialLinks",setup(s){const{theme:e}=L();return(t,o)=>r(e).socialLinks?(a(),k(be,{key:0,class:"VPNavScreenSocialLinks",links:r(e).socialLinks},null,8,["links"])):h("",!0)}}),ia={class:"list"},la=m({__name:"VPNavScreenTranslations",setup(s){const{localeLinks:e,currentLang:t}=R({correspondingLink:!0}),o=T(!1);function n(){o.value=!o.value}return(i,l)=>r(e).length&&r(t).label?(a(),u("div",{key:0,class:M(["VPNavScreenTranslations",{open:o.value}])},[d("button",{class:"title",onClick:n},[l[0]||(l[0]=d("span",{class:"vpi-languages icon lang"},null,-1)),z(" "+I(r(t).label)+" ",1),l[1]||(l[1]=d("span",{class:"vpi-chevron-down icon chevron"},null,-1))]),d("ul",ia,[(a(!0),u(w,null,H(r(e),v=>(a(),u("li",{key:v.link,class:"item"},[_(F,{class:"link",href:v.link},{default:f(()=>[z(I(v.text),1)]),_:2},1032,["href"])]))),128))])],2)):h("",!0)}}),ca=g(la,[["__scopeId","data-v-858fe1a4"]]),ua={class:"container"},da=m({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(s){const e=T(null),t=Se(ee?document.body:null);return(o,n)=>(a(),k(ue,{name:"fade",onEnter:n[0]||(n[0]=i=>t.value=!0),onAfterLeave:n[1]||(n[1]=i=>t.value=!1)},{default:f(()=>[o.open?(a(),u("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[d("div",ua,[c(o.$slots,"nav-screen-content-before",{},void 0,!0),_(aa,{class:"menu"}),_(ca,{class:"translations"}),_(Gs,{class:"appearance"}),_(ra,{class:"social-links"}),c(o.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):h("",!0)]),_:3}))}}),va=g(da,[["__scopeId","data-v-f2779853"]]),pa={key:0,class:"VPNav"},fa=m({__name:"VPNav",setup(s){const{isScreenOpen:e,closeScreen:t,toggleScreen:o}=lo(),{frontmatter:n}=L(),i=y(()=>n.value.navbar!==!1);return Te("close-screen",t),Y(()=>{ee&&document.documentElement.classList.toggle("hide-nav",!i.value)}),(l,v)=>i.value?(a(),u("header",pa,[_(Es,{"is-screen-open":r(e),onToggleScreen:r(o)},{"nav-bar-title-before":f(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[c(l.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[c(l.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),_(va,{open:r(e)},{"nav-screen-content-before":f(()=>[c(l.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[c(l.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):h("",!0)}}),ha=g(fa,[["__scopeId","data-v-ae24b3ad"]]),ma=["role","tabindex"],_a={key:1,class:"items"},ka=m({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(s){const e=s,{collapsed:t,collapsible:o,isLink:n,isActiveLink:i,hasActiveLink:l,hasChildren:v,toggle:p}=dt(y(()=>e.item)),$=y(()=>v.value?"section":"div"),V=y(()=>n.value?"a":"div"),b=y(()=>v.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),P=y(()=>n.value?void 0:"button"),N=y(()=>[[`level-${e.depth}`],{collapsible:o.value},{collapsed:t.value},{"is-link":n.value},{"is-active":i.value},{"has-active":l.value}]);function A(S){"key"in S&&S.key!=="Enter"||!e.item.link&&p()}function C(){e.item.link&&p()}return(S,B)=>{const G=K("VPSidebarItem",!0);return a(),k(E($.value),{class:M(["VPSidebarItem",N.value])},{default:f(()=>[S.item.text?(a(),u("div",U({key:0,class:"item",role:P.value},ze(S.item.items?{click:A,keydown:A}:{},!0),{tabindex:S.item.items&&0}),[B[1]||(B[1]=d("div",{class:"indicator"},null,-1)),S.item.link?(a(),k(F,{key:0,tag:V.value,class:"link",href:S.item.link,rel:S.item.rel,target:S.item.target},{default:f(()=>[(a(),k(E(b.value),{class:"text",innerHTML:S.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(a(),k(E(b.value),{key:1,class:"text",innerHTML:S.item.text},null,8,["innerHTML"])),S.item.collapsed!=null&&S.item.items&&S.item.items.length?(a(),u("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:C,onKeydown:We(C,["enter"]),tabindex:"0"},B[0]||(B[0]=[d("span",{class:"vpi-chevron-right caret-icon"},null,-1)]),32)):h("",!0)],16,ma)):h("",!0),S.item.items&&S.item.items.length?(a(),u("div",_a,[S.depth<5?(a(!0),u(w,{key:0},H(S.item.items,q=>(a(),k(G,{key:q.text,item:q,depth:S.depth+1},null,8,["item","depth"]))),128)):h("",!0)])):h("",!0)]),_:1},8,["class"])}}}),ba=g(ka,[["__scopeId","data-v-b3fd67f8"]]),ga=m({__name:"VPSidebarGroup",props:{items:{}},setup(s){const e=T(!0);let t=null;return j(()=>{t=setTimeout(()=>{t=null,e.value=!1},300)}),Ke(()=>{t!=null&&(clearTimeout(t),t=null)}),(o,n)=>(a(!0),u(w,null,H(o.items,i=>(a(),u("div",{key:i.text,class:M(["group",{"no-transition":e.value}])},[_(ba,{item:i,depth:0},null,8,["item"])],2))),128))}}),$a=g(ga,[["__scopeId","data-v-c40bc020"]]),ya={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Pa=m({__name:"VPSidebar",props:{open:{type:Boolean}},setup(s){const{sidebarGroups:e,hasSidebar:t}=O(),o=s,n=T(null),i=Se(ee?document.body:null);D([o,n],()=>{var v;o.open?(i.value=!0,(v=n.value)==null||v.focus()):i.value=!1},{immediate:!0,flush:"post"});const l=T(0);return D(e,()=>{l.value+=1},{deep:!0}),(v,p)=>r(t)?(a(),u("aside",{key:0,class:M(["VPSidebar",{open:v.open}]),ref_key:"navEl",ref:n,onClick:p[0]||(p[0]=qe(()=>{},["stop"]))},[p[2]||(p[2]=d("div",{class:"curtain"},null,-1)),d("nav",ya,[p[1]||(p[1]=d("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),c(v.$slots,"sidebar-nav-before",{},void 0,!0),(a(),k($a,{items:r(e),key:l.value},null,8,["items"])),c(v.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):h("",!0)}}),La=g(Pa,[["__scopeId","data-v-319d5ca6"]]),Va=m({__name:"VPSkipLink",setup(s){const{theme:e}=L(),t=Z(),o=T();D(()=>t.path,()=>o.value.focus());function n({target:i}){const l=document.getElementById(decodeURIComponent(i.hash).slice(1));if(l){const v=()=>{l.removeAttribute("tabindex"),l.removeEventListener("blur",v)};l.setAttribute("tabindex","-1"),l.addEventListener("blur",v),l.focus(),window.scrollTo(0,0)}}return(i,l)=>(a(),u(w,null,[d("span",{ref_key:"backToTop",ref:o,tabindex:"-1"},null,512),d("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:n},I(r(e).skipToContentLabel||"Skip to content"),1)],64))}}),Sa=g(Va,[["__scopeId","data-v-0b0ada53"]]),Ta=m({__name:"Layout",setup(s){const{isOpen:e,open:t,close:o}=O(),n=Z();D(()=>n.path,o),ut(e,o);const{frontmatter:i}=L(),l=Re(),v=y(()=>!!l["home-hero-image"]);return Te("hero-image-slot-exists",v),(p,$)=>{const V=K("Content");return r(i).layout!==!1?(a(),u("div",{key:0,class:M(["Layout",r(i).pageClass])},[c(p.$slots,"layout-top",{},void 0,!0),_(Sa),_(Qe,{class:"backdrop",show:r(e),onClick:r(o)},null,8,["show","onClick"]),_(ha,null,{"nav-bar-title-before":f(()=>[c(p.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(p.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[c(p.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[c(p.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":f(()=>[c(p.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[c(p.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),_(io,{open:r(e),onOpenMenu:r(t)},null,8,["open","onOpenMenu"]),_(La,{open:r(e)},{"sidebar-nav-before":f(()=>[c(p.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":f(()=>[c(p.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),_(Kn,null,{"page-top":f(()=>[c(p.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[c(p.$slots,"page-bottom",{},void 0,!0)]),"not-found":f(()=>[c(p.$slots,"not-found",{},void 0,!0)]),"home-hero-before":f(()=>[c(p.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[c(p.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(p.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(p.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(p.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(p.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[c(p.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[c(p.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[c(p.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":f(()=>[c(p.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[c(p.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[c(p.$slots,"doc-after",{},void 0,!0)]),"doc-top":f(()=>[c(p.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[c(p.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":f(()=>[c(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[c(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[c(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),_(Yn),c(p.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),k(V,{key:1}))}}}),Na=g(Ta,[["__scopeId","data-v-5d98c3a5"]]),Ia={Layout:Na,enhanceApp:({app:s})=>{s.component("Badge",Je)}};export{ds as c,Ia as t,L as u}; diff --git a/docs/.vitepress/dist/assets/chunks/theme.aZowWMZT.js b/docs/.vitepress/dist/assets/chunks/theme.aZowWMZT.js deleted file mode 100644 index 7c8454dcf..000000000 --- a/docs/.vitepress/dist/assets/chunks/theme.aZowWMZT.js +++ /dev/null @@ -1 +0,0 @@ -import{d as g,o as a,c as l,r as c,n as I,a as H,t as V,b as $,w as h,T as de,e as _,_ as m,u as De,i as Oe,f as Ue,g as ve,h as L,j as R,k,l as x,m as d,p as r,q as E,s as F,v as G,x as ie,y as j,z as X,A as he,B as ye,C as Ge,D as xe,E as q,F as T,G as A,H as we,I as ee,J as f,K as U,L as Pe,M as te,N as J,O as se,P as je,Q as qe,R as Ke,S as Re,U as Ve,V as We,W as Ye,X as Le,Y as Se,Z as Je,$ as Ze,a0 as Qe,a1 as Xe}from"./framework.MXVb71fM.js";const et=g({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(s){return(e,t)=>(a(),l("span",{class:I(["VPBadge",e.type])},[c(e.$slots,"default",{},()=>[H(V(e.text),1)])],2))}}),tt={key:0,class:"VPBackdrop"},st=g({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(s){return(e,t)=>(a(),$(de,{name:"fade"},{default:h(()=>[e.show?(a(),l("div",tt)):_("",!0)]),_:1}))}}),ot=m(st,[["__scopeId","data-v-c79a1216"]]),P=De;function nt(s,e){let t,n=!1;return()=>{t&&clearTimeout(t),n?t=setTimeout(s,e):(s(),(n=!0)&&setTimeout(()=>n=!1,e))}}function le(s){return/^\//.test(s)?s:`/${s}`}function pe(s){const{pathname:e,search:t,hash:n,protocol:o}=new URL(s,"http://a.com");if(Oe(s)||s.startsWith("#")||!o.startsWith("http")||!Ue(e))return s;const{site:i}=P(),u=e.endsWith("/")||e.endsWith(".html")?s:s.replace(/(?:(^\.+)\/)?.*$/,`$1${e.replace(/(\.md)?$/,i.value.cleanUrls?"":".html")}${t}${n}`);return ve(u)}const fe=L(R?location.hash:"");R&&window.addEventListener("hashchange",()=>{fe.value=location.hash});function W({removeCurrent:s=!0,correspondingLink:e=!1}={}){const{site:t,localeIndex:n,page:o,theme:i}=P(),u=k(()=>{var v,b;return{label:(v=t.value.locales[n.value])==null?void 0:v.label,link:((b=t.value.locales[n.value])==null?void 0:b.link)||(n.value==="root"?"/":`/${n.value}/`)}});return{localeLinks:k(()=>Object.entries(t.value.locales).flatMap(([v,b])=>s&&u.value.label===b.label?[]:{text:b.label,link:at(b.link||(v==="root"?"/":`/${v}/`),i.value.i18nRouting!==!1&&e,o.value.relativePath.slice(u.value.link.length-1),!t.value.cleanUrls)+fe.value})),currentLang:u}}function at(s,e,t,n){return e?s.replace(/\/$/,"")+le(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,n?".html":"")):s}const rt=s=>(E("data-v-f87ff6e4"),s=s(),F(),s),it={class:"NotFound"},lt={class:"code"},ct={class:"title"},ut=rt(()=>d("div",{class:"divider"},null,-1)),dt={class:"quote"},vt={class:"action"},ht=["href","aria-label"],pt=g({__name:"NotFound",setup(s){const{site:e,theme:t}=P(),{localeLinks:n}=W({removeCurrent:!1}),o=L("/");return x(()=>{var u;const i=window.location.pathname.replace(e.value.base,"").replace(/(^.*?\/).*$/,"/$1");n.value.length&&(o.value=((u=n.value.find(({link:p})=>p.startsWith(i)))==null?void 0:u.link)||n.value[0].link)}),(i,u)=>{var p,v,b,y,w;return a(),l("div",it,[d("p",lt,V(((p=r(t).notFound)==null?void 0:p.code)??"404"),1),d("h1",ct,V(((v=r(t).notFound)==null?void 0:v.title)??"PAGE NOT FOUND"),1),ut,d("blockquote",dt,V(((b=r(t).notFound)==null?void 0:b.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),d("div",vt,[d("a",{class:"link",href:r(ve)(o.value),"aria-label":((y=r(t).notFound)==null?void 0:y.linkLabel)??"go to home"},V(((w=r(t).notFound)==null?void 0:w.linkText)??"Take me home"),9,ht)])])}}}),ft=m(pt,[["__scopeId","data-v-f87ff6e4"]]);function Me(s,e){if(Array.isArray(s))return Z(s);if(s==null)return[];e=le(e);const t=Object.keys(s).sort((o,i)=>i.split("/").length-o.split("/").length).find(o=>e.startsWith(le(o))),n=t?s[t]:[];return Array.isArray(n)?Z(n):Z(n.items,n.base)}function _t(s){const e=[];let t=0;for(const n in s){const o=s[n];if(o.items){t=e.push(o);continue}e[t]||e.push({items:[]}),e[t].items.push(o)}return e}function mt(s){const e=[];function t(n){for(const o of n)o.text&&o.link&&e.push({text:o.text,link:o.link,docFooterText:o.docFooterText}),o.items&&t(o.items)}return t(s),e}function ce(s,e){return Array.isArray(e)?e.some(t=>ce(s,t)):G(s,e.link)?!0:e.items?ce(s,e.items):!1}function Z(s,e){return[...s].map(t=>{const n={...t},o=n.base||e;return o&&n.link&&(n.link=o+n.link),n.items&&(n.items=Z(n.items,o)),n})}function D(){const{frontmatter:s,page:e,theme:t}=P(),n=ie("(min-width: 960px)"),o=L(!1),i=k(()=>{const B=t.value.sidebar,S=e.value.relativePath;return B?Me(B,S):[]}),u=L(i.value);j(i,(B,S)=>{JSON.stringify(B)!==JSON.stringify(S)&&(u.value=i.value)});const p=k(()=>s.value.sidebar!==!1&&u.value.length>0&&s.value.layout!=="home"),v=k(()=>b?s.value.aside==null?t.value.aside==="left":s.value.aside==="left":!1),b=k(()=>s.value.layout==="home"?!1:s.value.aside!=null?!!s.value.aside:t.value.aside!==!1),y=k(()=>p.value&&n.value),w=k(()=>p.value?_t(u.value):[]);function M(){o.value=!0}function C(){o.value=!1}function N(){o.value?C():M()}return{isOpen:o,sidebar:u,sidebarGroups:w,hasSidebar:p,hasAside:b,leftAside:v,isSidebarEnabled:y,open:M,close:C,toggle:N}}function gt(s,e){let t;X(()=>{t=s.value?document.activeElement:void 0}),x(()=>{window.addEventListener("keyup",n)}),he(()=>{window.removeEventListener("keyup",n)});function n(o){o.key==="Escape"&&s.value&&(e(),t==null||t.focus())}}function $t(s){const{page:e}=P(),t=L(!1),n=k(()=>s.value.collapsed!=null),o=k(()=>!!s.value.link),i=L(!1),u=()=>{i.value=G(e.value.relativePath,s.value.link)};j([e,s,fe],u),x(u);const p=k(()=>i.value?!0:s.value.items?ce(e.value.relativePath,s.value.items):!1),v=k(()=>!!(s.value.items&&s.value.items.length));X(()=>{t.value=!!(n.value&&s.value.collapsed)}),ye(()=>{(i.value||p.value)&&(t.value=!1)});function b(){n.value&&(t.value=!t.value)}return{collapsed:t,collapsible:n,isLink:o,isActiveLink:i,hasActiveLink:p,hasChildren:v,toggle:b}}function kt(){const{hasSidebar:s}=D(),e=ie("(min-width: 960px)"),t=ie("(min-width: 1280px)");return{isAsideEnabled:k(()=>!t.value&&!e.value?!1:s.value?t.value:e.value)}}const ue=[];function Ce(s){return typeof s.outline=="object"&&!Array.isArray(s.outline)&&s.outline.label||s.outlineTitle||"On this page"}function _e(s){const e=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const n=Number(t.tagName[1]);return{element:t,title:bt(t),link:"#"+t.id,level:n}});return yt(e,s)}function bt(s){let e="";for(const t of s.childNodes)if(t.nodeType===1){if(t.classList.contains("VPBadge")||t.classList.contains("header-anchor")||t.classList.contains("ignore-header"))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function yt(s,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[n,o]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;s=s.filter(u=>u.level>=n&&u.level<=o),ue.length=0;for(const{element:u,link:p}of s)ue.push({element:u,link:p});const i=[];e:for(let u=0;u=0;v--){const b=s[v];if(b.level{requestAnimationFrame(i),window.addEventListener("scroll",n)}),Ge(()=>{u(location.hash)}),he(()=>{window.removeEventListener("scroll",n)});function i(){if(!t.value)return;const p=window.scrollY,v=window.innerHeight,b=document.body.offsetHeight,y=Math.abs(p+v-b)<1,w=ue.map(({element:C,link:N})=>({link:N,top:Pt(C)})).filter(({top:C})=>!Number.isNaN(C)).sort((C,N)=>C.top-N.top);if(!w.length){u(null);return}if(p<1){u(null);return}if(y){u(w[w.length-1].link);return}let M=null;for(const{link:C,top:N}of w){if(N>p+xe()+4)break;M=C}u(M)}function u(p){o&&o.classList.remove("active"),p==null?o=null:o=s.value.querySelector(`a[href="${decodeURIComponent(p)}"]`);const v=o;v?(v.classList.add("active"),e.value.style.top=v.offsetTop+39+"px",e.value.style.opacity="1"):(e.value.style.top="33px",e.value.style.opacity="0")}}function Pt(s){let e=0;for(;s!==document.body;){if(s===null)return NaN;e+=s.offsetTop,s=s.offsetParent}return e}const Vt=["href","title"],Lt=g({__name:"VPDocOutlineItem",props:{headers:{},root:{type:Boolean}},setup(s){function e({target:t}){const n=t.href.split("#")[1],o=document.getElementById(decodeURIComponent(n));o==null||o.focus({preventScroll:!0})}return(t,n)=>{const o=q("VPDocOutlineItem",!0);return a(),l("ul",{class:I(["VPDocOutlineItem",t.root?"root":"nested"])},[(a(!0),l(T,null,A(t.headers,({children:i,link:u,title:p})=>(a(),l("li",null,[d("a",{class:"outline-link",href:u,onClick:e,title:p},V(p),9,Vt),i!=null&&i.length?(a(),$(o,{key:0,headers:i},null,8,["headers"])):_("",!0)]))),256))],2)}}}),Ie=m(Lt,[["__scopeId","data-v-b933a997"]]),St=s=>(E("data-v-935f8a84"),s=s(),F(),s),Mt={class:"content"},Ct={class:"outline-title",role:"heading","aria-level":"2"},It={"aria-labelledby":"doc-outline-aria-label"},Tt=St(()=>d("span",{class:"visually-hidden",id:"doc-outline-aria-label"}," Table of Contents for current page ",-1)),Nt=g({__name:"VPDocAsideOutline",setup(s){const{frontmatter:e,theme:t}=P(),n=we([]);ee(()=>{n.value=_e(e.value.outline??t.value.outline)});const o=L(),i=L();return wt(o,i),(u,p)=>(a(),l("div",{class:I(["VPDocAsideOutline",{"has-outline":n.value.length>0}]),ref_key:"container",ref:o,role:"navigation"},[d("div",Mt,[d("div",{class:"outline-marker",ref_key:"marker",ref:i},null,512),d("div",Ct,V(r(Ce)(r(t))),1),d("nav",It,[Tt,f(Ie,{headers:n.value,root:!0},null,8,["headers"])])])],2))}}),Bt=m(Nt,[["__scopeId","data-v-935f8a84"]]),At={class:"VPDocAsideCarbonAds"},Ht=g({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(s){const e=()=>null;return(t,n)=>(a(),l("div",At,[f(r(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),zt=s=>(E("data-v-3f215769"),s=s(),F(),s),Et={class:"VPDocAside"},Ft=zt(()=>d("div",{class:"spacer"},null,-1)),Dt=g({__name:"VPDocAside",setup(s){const{theme:e}=P();return(t,n)=>(a(),l("div",Et,[c(t.$slots,"aside-top",{},void 0,!0),c(t.$slots,"aside-outline-before",{},void 0,!0),f(Bt),c(t.$slots,"aside-outline-after",{},void 0,!0),Ft,c(t.$slots,"aside-ads-before",{},void 0,!0),r(e).carbonAds?(a(),$(Ht,{key:0,"carbon-ads":r(e).carbonAds},null,8,["carbon-ads"])):_("",!0),c(t.$slots,"aside-ads-after",{},void 0,!0),c(t.$slots,"aside-bottom",{},void 0,!0)]))}}),Ot=m(Dt,[["__scopeId","data-v-3f215769"]]);function Ut(){const{theme:s,page:e}=P();return k(()=>{const{text:t="Edit this page",pattern:n=""}=s.value.editLink||{};let o;return typeof n=="function"?o=n(e.value):o=n.replace(/:path/g,e.value.filePath),{url:o,text:t}})}function Gt(){const{page:s,theme:e,frontmatter:t}=P();return k(()=>{var v,b,y,w,M,C,N,B;const n=Me(e.value.sidebar,s.value.relativePath),o=mt(n),i=o.findIndex(S=>G(s.value.relativePath,S.link)),u=((v=e.value.docFooter)==null?void 0:v.prev)===!1&&!t.value.prev||t.value.prev===!1,p=((b=e.value.docFooter)==null?void 0:b.next)===!1&&!t.value.next||t.value.next===!1;return{prev:u?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((y=o[i-1])==null?void 0:y.docFooterText)??((w=o[i-1])==null?void 0:w.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((M=o[i-1])==null?void 0:M.link)},next:p?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((C=o[i+1])==null?void 0:C.docFooterText)??((N=o[i+1])==null?void 0:N.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((B=o[i+1])==null?void 0:B.link)}}})}const xt={},jt={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},qt=d("path",{d:"M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z"},null,-1),Kt=d("path",{d:"M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z"},null,-1),Rt=[qt,Kt];function Wt(s,e){return a(),l("svg",jt,Rt)}const Yt=m(xt,[["render",Wt]]),z=g({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(s){const e=s,t=k(()=>e.tag??(e.href?"a":"span")),n=k(()=>e.href&&Pe.test(e.href));return(o,i)=>(a(),$(U(t.value),{class:I(["VPLink",{link:o.href,"vp-external-link-icon":n.value,"no-icon":o.noIcon}]),href:o.href?r(pe)(o.href):void 0,target:o.target??(n.value?"_blank":void 0),rel:o.rel??(n.value?"noreferrer":void 0)},{default:h(()=>[c(o.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),Jt={class:"VPLastUpdated"},Zt=["datetime"],Qt=g({__name:"VPDocFooterLastUpdated",setup(s){const{theme:e,page:t,frontmatter:n,lang:o}=P(),i=k(()=>new Date(n.value.lastUpdated??t.value.lastUpdated)),u=k(()=>i.value.toISOString()),p=L("");return x(()=>{X(()=>{var v,b,y;p.value=new Intl.DateTimeFormat((b=(v=e.value.lastUpdated)==null?void 0:v.formatOptions)!=null&&b.forceLocale?o.value:void 0,((y=e.value.lastUpdated)==null?void 0:y.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(i.value)})}),(v,b)=>{var y;return a(),l("p",Jt,[H(V(((y=r(e).lastUpdated)==null?void 0:y.text)||r(e).lastUpdatedText||"Last updated")+": ",1),d("time",{datetime:u.value},V(p.value),9,Zt)])}}}),Xt=m(Qt,[["__scopeId","data-v-7e05ebdb"]]),es={key:0,class:"VPDocFooter"},ts={key:0,class:"edit-info"},ss={key:0,class:"edit-link"},os={key:1,class:"last-updated"},ns={key:1,class:"prev-next"},as={class:"pager"},rs=["innerHTML"],is=["innerHTML"],ls={class:"pager"},cs=["innerHTML"],us=["innerHTML"],ds=g({__name:"VPDocFooter",setup(s){const{theme:e,page:t,frontmatter:n}=P(),o=Ut(),i=Gt(),u=k(()=>e.value.editLink&&n.value.editLink!==!1),p=k(()=>t.value.lastUpdated&&n.value.lastUpdated!==!1),v=k(()=>u.value||p.value||i.value.prev||i.value.next);return(b,y)=>{var w,M,C,N;return v.value?(a(),l("footer",es,[c(b.$slots,"doc-footer-before",{},void 0,!0),u.value||p.value?(a(),l("div",ts,[u.value?(a(),l("div",ss,[f(z,{class:"edit-link-button",href:r(o).url,"no-icon":!0},{default:h(()=>[f(Yt,{class:"edit-link-icon","aria-label":"edit icon"}),H(" "+V(r(o).text),1)]),_:1},8,["href"])])):_("",!0),p.value?(a(),l("div",os,[f(Xt)])):_("",!0)])):_("",!0),(w=r(i).prev)!=null&&w.link||(M=r(i).next)!=null&&M.link?(a(),l("nav",ns,[d("div",as,[(C=r(i).prev)!=null&&C.link?(a(),$(z,{key:0,class:"pager-link prev",href:r(i).prev.link},{default:h(()=>{var B;return[d("span",{class:"desc",innerHTML:((B=r(e).docFooter)==null?void 0:B.prev)||"Previous page"},null,8,rs),d("span",{class:"title",innerHTML:r(i).prev.text},null,8,is)]}),_:1},8,["href"])):_("",!0)]),d("div",ls,[(N=r(i).next)!=null&&N.link?(a(),$(z,{key:0,class:"pager-link next",href:r(i).next.link},{default:h(()=>{var B;return[d("span",{class:"desc",innerHTML:((B=r(e).docFooter)==null?void 0:B.next)||"Next page"},null,8,cs),d("span",{class:"title",innerHTML:r(i).next.text},null,8,us)]}),_:1},8,["href"])):_("",!0)])])):_("",!0)])):_("",!0)}}}),vs=m(ds,[["__scopeId","data-v-48f9bb55"]]),hs=s=>(E("data-v-39a288b8"),s=s(),F(),s),ps={class:"container"},fs=hs(()=>d("div",{class:"aside-curtain"},null,-1)),_s={class:"aside-container"},ms={class:"aside-content"},gs={class:"content"},$s={class:"content-container"},ks={class:"main"},bs=g({__name:"VPDoc",setup(s){const{theme:e}=P(),t=te(),{hasSidebar:n,hasAside:o,leftAside:i}=D(),u=k(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(p,v)=>{const b=q("Content");return a(),l("div",{class:I(["VPDoc",{"has-sidebar":r(n),"has-aside":r(o)}])},[c(p.$slots,"doc-top",{},void 0,!0),d("div",ps,[r(o)?(a(),l("div",{key:0,class:I(["aside",{"left-aside":r(i)}])},[fs,d("div",_s,[d("div",ms,[f(Ot,null,{"aside-top":h(()=>[c(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":h(()=>[c(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":h(()=>[c(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):_("",!0),d("div",gs,[d("div",$s,[c(p.$slots,"doc-before",{},void 0,!0),d("main",ks,[f(b,{class:I(["vp-doc",[u.value,r(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),f(vs,null,{"doc-footer-before":h(()=>[c(p.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),c(p.$slots,"doc-after",{},void 0,!0)])])]),c(p.$slots,"doc-bottom",{},void 0,!0)],2)}}}),ys=m(bs,[["__scopeId","data-v-39a288b8"]]),ws=g({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(s){const e=s,t=k(()=>e.href&&Pe.test(e.href)),n=k(()=>e.tag||e.href?"a":"button");return(o,i)=>(a(),$(U(n.value),{class:I(["VPButton",[o.size,o.theme]]),href:o.href?r(pe)(o.href):void 0,target:e.target??(t.value?"_blank":void 0),rel:e.rel??(t.value?"noreferrer":void 0)},{default:h(()=>[H(V(o.text),1)]),_:1},8,["class","href","target","rel"]))}}),Ps=m(ws,[["__scopeId","data-v-cad61b99"]]),Vs=["src","alt"],Ls=g({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(s){return(e,t)=>{const n=q("VPImage",!0);return e.image?(a(),l(T,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),l("img",J({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:r(ve)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,Vs)):(a(),l(T,{key:1},[f(n,J({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),f(n,J({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):_("",!0)}}}),Q=m(Ls,[["__scopeId","data-v-8426fc1a"]]),Ss=s=>(E("data-v-303bb580"),s=s(),F(),s),Ms={class:"container"},Cs={class:"main"},Is={key:0,class:"name"},Ts=["innerHTML"],Ns=["innerHTML"],Bs=["innerHTML"],As={key:0,class:"actions"},Hs={key:0,class:"image"},zs={class:"image-container"},Es=Ss(()=>d("div",{class:"image-bg"},null,-1)),Fs=g({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(s){const e=se("hero-image-slot-exists");return(t,n)=>(a(),l("div",{class:I(["VPHero",{"has-image":t.image||r(e)}])},[d("div",Ms,[d("div",Cs,[c(t.$slots,"home-hero-info-before",{},void 0,!0),c(t.$slots,"home-hero-info",{},()=>[t.name?(a(),l("h1",Is,[d("span",{innerHTML:t.name,class:"clip"},null,8,Ts)])):_("",!0),t.text?(a(),l("p",{key:1,innerHTML:t.text,class:"text"},null,8,Ns)):_("",!0),t.tagline?(a(),l("p",{key:2,innerHTML:t.tagline,class:"tagline"},null,8,Bs)):_("",!0)],!0),c(t.$slots,"home-hero-info-after",{},void 0,!0),t.actions?(a(),l("div",As,[(a(!0),l(T,null,A(t.actions,o=>(a(),l("div",{key:o.link,class:"action"},[f(Ps,{tag:"a",size:"medium",theme:o.theme,text:o.text,href:o.link,target:o.target,rel:o.rel},null,8,["theme","text","href","target","rel"])]))),128))])):_("",!0),c(t.$slots,"home-hero-actions-after",{},void 0,!0)]),t.image||r(e)?(a(),l("div",Hs,[d("div",zs,[Es,c(t.$slots,"home-hero-image",{},()=>[t.image?(a(),$(Q,{key:0,class:"image-src",image:t.image},null,8,["image"])):_("",!0)],!0)])])):_("",!0)])],2))}}),Ds=m(Fs,[["__scopeId","data-v-303bb580"]]),Os=g({__name:"VPHomeHero",setup(s){const{frontmatter:e}=P();return(t,n)=>r(e).hero?(a(),$(Ds,{key:0,class:"VPHomeHero",name:r(e).hero.name,text:r(e).hero.text,tagline:r(e).hero.tagline,image:r(e).hero.image,actions:r(e).hero.actions},{"home-hero-info-before":h(()=>[c(t.$slots,"home-hero-info-before")]),"home-hero-info":h(()=>[c(t.$slots,"home-hero-info")]),"home-hero-info-after":h(()=>[c(t.$slots,"home-hero-info-after")]),"home-hero-actions-after":h(()=>[c(t.$slots,"home-hero-actions-after")]),"home-hero-image":h(()=>[c(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):_("",!0)}}),Us={},Gs={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},xs=d("path",{d:"M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"},null,-1),js=[xs];function qs(s,e){return a(),l("svg",Gs,js)}const Ks=m(Us,[["render",qs]]),Rs={class:"box"},Ws={key:0,class:"icon"},Ys=["innerHTML"],Js=["innerHTML"],Zs=["innerHTML"],Qs={key:4,class:"link-text"},Xs={class:"link-text-value"},eo=g({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(s){return(e,t)=>(a(),$(z,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:h(()=>[d("article",Rs,[typeof e.icon=="object"&&e.icon.wrap?(a(),l("div",Ws,[f(Q,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(a(),$(Q,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(a(),l("div",{key:2,class:"icon",innerHTML:e.icon},null,8,Ys)):_("",!0),d("h2",{class:"title",innerHTML:e.title},null,8,Js),e.details?(a(),l("p",{key:3,class:"details",innerHTML:e.details},null,8,Zs)):_("",!0),e.linkText?(a(),l("div",Qs,[d("p",Xs,[H(V(e.linkText)+" ",1),f(Ks,{class:"link-text-icon"})])])):_("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),to=m(eo,[["__scopeId","data-v-33204567"]]),so={key:0,class:"VPFeatures"},oo={class:"container"},no={class:"items"},ao=g({__name:"VPFeatures",props:{features:{}},setup(s){const e=s,t=k(()=>{const n=e.features.length;if(n){if(n===2)return"grid-2";if(n===3)return"grid-3";if(n%3===0)return"grid-6";if(n>3)return"grid-4"}else return});return(n,o)=>n.features?(a(),l("div",so,[d("div",oo,[d("div",no,[(a(!0),l(T,null,A(n.features,i=>(a(),l("div",{key:i.title,class:I(["item",[t.value]])},[f(to,{icon:i.icon,title:i.title,details:i.details,link:i.link,"link-text":i.linkText,rel:i.rel,target:i.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):_("",!0)}}),ro=m(ao,[["__scopeId","data-v-a6181336"]]),io=g({__name:"VPHomeFeatures",setup(s){const{frontmatter:e}=P();return(t,n)=>r(e).features?(a(),$(ro,{key:0,class:"VPHomeFeatures",features:r(e).features},null,8,["features"])):_("",!0)}}),lo={class:"VPHome"},co=g({__name:"VPHome",setup(s){return(e,t)=>{const n=q("Content");return a(),l("div",lo,[c(e.$slots,"home-hero-before",{},void 0,!0),f(Os,null,{"home-hero-info-before":h(()=>[c(e.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(e.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(e.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(e.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(e.$slots,"home-hero-image",{},void 0,!0)]),_:3}),c(e.$slots,"home-hero-after",{},void 0,!0),c(e.$slots,"home-features-before",{},void 0,!0),f(io),c(e.$slots,"home-features-after",{},void 0,!0),f(n)])}}}),uo=m(co,[["__scopeId","data-v-c71b6826"]]),vo={},ho={class:"VPPage"};function po(s,e){const t=q("Content");return a(),l("div",ho,[c(s.$slots,"page-top"),f(t),c(s.$slots,"page-bottom")])}const fo=m(vo,[["render",po]]),_o=g({__name:"VPContent",setup(s){const{page:e,frontmatter:t}=P(),{hasSidebar:n}=D();return(o,i)=>(a(),l("div",{class:I(["VPContent",{"has-sidebar":r(n),"is-home":r(t).layout==="home"}]),id:"VPContent"},[r(e).isNotFound?c(o.$slots,"not-found",{key:0},()=>[f(ft)],!0):r(t).layout==="page"?(a(),$(fo,{key:1},{"page-top":h(()=>[c(o.$slots,"page-top",{},void 0,!0)]),"page-bottom":h(()=>[c(o.$slots,"page-bottom",{},void 0,!0)]),_:3})):r(t).layout==="home"?(a(),$(uo,{key:2},{"home-hero-before":h(()=>[c(o.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":h(()=>[c(o.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(o.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(o.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(o.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(o.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":h(()=>[c(o.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":h(()=>[c(o.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":h(()=>[c(o.$slots,"home-features-after",{},void 0,!0)]),_:3})):r(t).layout&&r(t).layout!=="doc"?(a(),$(U(r(t).layout),{key:3})):(a(),$(ys,{key:4},{"doc-top":h(()=>[c(o.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":h(()=>[c(o.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":h(()=>[c(o.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":h(()=>[c(o.$slots,"doc-before",{},void 0,!0)]),"doc-after":h(()=>[c(o.$slots,"doc-after",{},void 0,!0)]),"aside-top":h(()=>[c(o.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":h(()=>[c(o.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(o.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(o.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(o.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":h(()=>[c(o.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),mo=m(_o,[["__scopeId","data-v-1428d186"]]),go={class:"container"},$o=["innerHTML"],ko=["innerHTML"],bo=g({__name:"VPFooter",setup(s){const{theme:e,frontmatter:t}=P(),{hasSidebar:n}=D();return(o,i)=>r(e).footer&&r(t).footer!==!1?(a(),l("footer",{key:0,class:I(["VPFooter",{"has-sidebar":r(n)}])},[d("div",go,[r(e).footer.message?(a(),l("p",{key:0,class:"message",innerHTML:r(e).footer.message},null,8,$o)):_("",!0),r(e).footer.copyright?(a(),l("p",{key:1,class:"copyright",innerHTML:r(e).footer.copyright},null,8,ko)):_("",!0)])],2)):_("",!0)}}),yo=m(bo,[["__scopeId","data-v-e315a0ad"]]);function Te(){const{theme:s,frontmatter:e}=P(),t=we([]),n=k(()=>t.value.length>0);return ee(()=>{t.value=_e(e.value.outline??s.value.outline)}),{headers:t,hasLocalNav:n}}const wo={},Po={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Vo=d("path",{d:"M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"},null,-1),Lo=[Vo];function So(s,e){return a(),l("svg",Po,Lo)}const Ne=m(wo,[["render",So]]),Mo={class:"header"},Co={class:"outline"},Io=g({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(s){const e=s,{theme:t}=P(),n=L(!1),o=L(0),i=L(),u=L();je(i,()=>{n.value=!1}),qe("Escape",()=>{n.value=!1}),ee(()=>{n.value=!1});function p(){n.value=!n.value,o.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function v(y){y.target.classList.contains("outline-link")&&(u.value&&(u.value.style.transition="none"),Re(()=>{n.value=!1}))}function b(){n.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(y,w)=>(a(),l("div",{class:"VPLocalNavOutlineDropdown",style:Ke({"--vp-vh":o.value+"px"}),ref_key:"main",ref:i},[y.headers.length>0?(a(),l("button",{key:0,onClick:p,class:I({open:n.value})},[H(V(r(Ce)(r(t)))+" ",1),f(Ne,{class:"icon"})],2)):(a(),l("button",{key:1,onClick:b},V(r(t).returnToTopLabel||"Return to top"),1)),f(de,{name:"flyout"},{default:h(()=>[n.value?(a(),l("div",{key:0,ref_key:"items",ref:u,class:"items",onClick:v},[d("div",Mo,[d("a",{class:"top-link",href:"#",onClick:b},V(r(t).returnToTopLabel||"Return to top"),1)]),d("div",Co,[f(Ie,{headers:y.headers},null,8,["headers"])])],512)):_("",!0)]),_:1})],4))}}),To=m(Io,[["__scopeId","data-v-af18c0d5"]]),No={},Bo={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Ao=d("path",{d:"M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"},null,-1),Ho=d("path",{d:"M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"},null,-1),zo=d("path",{d:"M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"},null,-1),Eo=d("path",{d:"M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"},null,-1),Fo=[Ao,Ho,zo,Eo];function Do(s,e){return a(),l("svg",Bo,Fo)}const Oo=m(No,[["render",Do]]),Uo={class:"container"},Go=["aria-expanded"],xo={class:"menu-text"},jo=g({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(s){const{theme:e,frontmatter:t}=P(),{hasSidebar:n}=D(),{headers:o}=Te(),{y:i}=Ve(),u=L(0);x(()=>{u.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),ee(()=>{o.value=_e(t.value.outline??e.value.outline)});const p=k(()=>o.value.length===0),v=k(()=>p.value&&!n.value),b=k(()=>({VPLocalNav:!0,"has-sidebar":n.value,empty:p.value,fixed:v.value}));return(y,w)=>r(t).layout!=="home"&&(!v.value||r(i)>=u.value)?(a(),l("div",{key:0,class:I(b.value)},[d("div",Uo,[r(n)?(a(),l("button",{key:0,class:"menu","aria-expanded":y.open,"aria-controls":"VPSidebarNav",onClick:w[0]||(w[0]=M=>y.$emit("open-menu"))},[f(Oo,{class:"menu-icon"}),d("span",xo,V(r(e).sidebarMenuLabel||"Menu"),1)],8,Go)):_("",!0),f(To,{headers:r(o),navHeight:u.value},null,8,["headers","navHeight"])])],2)):_("",!0)}}),qo=m(jo,[["__scopeId","data-v-0282ae07"]]);function Ko(){const s=L(!1);function e(){s.value=!0,window.addEventListener("resize",o)}function t(){s.value=!1,window.removeEventListener("resize",o)}function n(){s.value?t():e()}function o(){window.outerWidth>=768&&t()}const i=te();return j(()=>i.path,t),{isScreenOpen:s,openScreen:e,closeScreen:t,toggleScreen:n}}const Ro={},Wo={class:"VPSwitch",type:"button",role:"switch"},Yo={class:"check"},Jo={key:0,class:"icon"};function Zo(s,e){return a(),l("button",Wo,[d("span",Yo,[s.$slots.default?(a(),l("span",Jo,[c(s.$slots,"default",{},void 0,!0)])):_("",!0)])])}const Qo=m(Ro,[["render",Zo],["__scopeId","data-v-b1685198"]]),Xo={},en={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},tn=d("path",{d:"M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"},null,-1),sn=[tn];function on(s,e){return a(),l("svg",en,sn)}const nn=m(Xo,[["render",on]]),an={},rn={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},ln=We('',9),cn=[ln];function un(s,e){return a(),l("svg",rn,cn)}const dn=m(an,[["render",un]]),vn=g({__name:"VPSwitchAppearance",setup(s){const{isDark:e,theme:t}=P(),n=se("toggle-appearance",()=>{e.value=!e.value}),o=k(()=>e.value?t.value.lightModeSwitchTitle||"Switch to light theme":t.value.darkModeSwitchTitle||"Switch to dark theme");return(i,u)=>(a(),$(Qo,{title:o.value,class:"VPSwitchAppearance","aria-checked":r(e),onClick:r(n)},{default:h(()=>[f(dn,{class:"sun"}),f(nn,{class:"moon"})]),_:1},8,["title","aria-checked","onClick"]))}}),me=m(vn,[["__scopeId","data-v-1736f215"]]),hn={key:0,class:"VPNavBarAppearance"},pn=g({__name:"VPNavBarAppearance",setup(s){const{site:e}=P();return(t,n)=>r(e).appearance&&r(e).appearance!=="force-dark"?(a(),l("div",hn,[f(me)])):_("",!0)}}),fn=m(pn,[["__scopeId","data-v-e6aabb21"]]),ge=L();let Be=!1,re=0;function _n(s){const e=L(!1);if(R){!Be&&mn(),re++;const t=j(ge,n=>{var o,i,u;n===s.el.value||(o=s.el.value)!=null&&o.contains(n)?(e.value=!0,(i=s.onFocus)==null||i.call(s)):(e.value=!1,(u=s.onBlur)==null||u.call(s))});he(()=>{t(),re--,re||gn()})}return Ye(e)}function mn(){document.addEventListener("focusin",Ae),Be=!0,ge.value=document.activeElement}function gn(){document.removeEventListener("focusin",Ae)}function Ae(){ge.value=document.activeElement}const $n={},kn={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},bn=d("path",{d:"M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"},null,-1),yn=[bn];function wn(s,e){return a(),l("svg",kn,yn)}const He=m($n,[["render",wn]]),Pn={},Vn={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Ln=d("circle",{cx:"12",cy:"12",r:"2"},null,-1),Sn=d("circle",{cx:"19",cy:"12",r:"2"},null,-1),Mn=d("circle",{cx:"5",cy:"12",r:"2"},null,-1),Cn=[Ln,Sn,Mn];function In(s,e){return a(),l("svg",Vn,Cn)}const Tn=m(Pn,[["render",In]]),Nn={class:"VPMenuLink"},Bn=g({__name:"VPMenuLink",props:{item:{}},setup(s){const{page:e}=P();return(t,n)=>(a(),l("div",Nn,[f(z,{class:I({active:r(G)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel},{default:h(()=>[H(V(t.item.text),1)]),_:1},8,["class","href","target","rel"])]))}}),oe=m(Bn,[["__scopeId","data-v-43f1e123"]]),An={class:"VPMenuGroup"},Hn={key:0,class:"title"},zn=g({__name:"VPMenuGroup",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),l("div",An,[e.text?(a(),l("p",Hn,V(e.text),1)):_("",!0),(a(!0),l(T,null,A(e.items,n=>(a(),l(T,null,["link"in n?(a(),$(oe,{key:0,item:n},null,8,["item"])):_("",!0)],64))),256))]))}}),En=m(zn,[["__scopeId","data-v-69e747b5"]]),Fn={class:"VPMenu"},Dn={key:0,class:"items"},On=g({__name:"VPMenu",props:{items:{}},setup(s){return(e,t)=>(a(),l("div",Fn,[e.items?(a(),l("div",Dn,[(a(!0),l(T,null,A(e.items,n=>(a(),l(T,{key:n.text},["link"in n?(a(),$(oe,{key:0,item:n},null,8,["item"])):(a(),$(En,{key:1,text:n.text,items:n.items},null,8,["text","items"]))],64))),128))])):_("",!0),c(e.$slots,"default",{},void 0,!0)]))}}),Un=m(On,[["__scopeId","data-v-e7ea1737"]]),Gn=["aria-expanded","aria-label"],xn={key:0,class:"text"},jn=["innerHTML"],qn={class:"menu"},Kn=g({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(s){const e=L(!1),t=L();_n({el:t,onBlur:n});function n(){e.value=!1}return(o,i)=>(a(),l("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:i[1]||(i[1]=u=>e.value=!0),onMouseleave:i[2]||(i[2]=u=>e.value=!1)},[d("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":o.label,onClick:i[0]||(i[0]=u=>e.value=!e.value)},[o.button||o.icon?(a(),l("span",xn,[o.icon?(a(),$(U(o.icon),{key:0,class:"option-icon"})):_("",!0),o.button?(a(),l("span",{key:1,innerHTML:o.button},null,8,jn)):_("",!0),f(He,{class:"text-icon"})])):(a(),$(Tn,{key:1,class:"icon"}))],8,Gn),d("div",qn,[f(Un,{items:o.items},{default:h(()=>[c(o.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),$e=m(Kn,[["__scopeId","data-v-9c007e85"]]),Rn={discord:'Discord',facebook:'Facebook',github:'GitHub',instagram:'Instagram',linkedin:'LinkedIn',mastodon:'Mastodon',npm:'npm',slack:'Slack',twitter:'Twitter',x:'X',youtube:'YouTube'},Wn=["href","aria-label","innerHTML"],Yn=g({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(s){const e=s,t=k(()=>typeof e.icon=="object"?e.icon.svg:Rn[e.icon]);return(n,o)=>(a(),l("a",{class:"VPSocialLink no-icon",href:n.link,"aria-label":n.ariaLabel??(typeof n.icon=="string"?n.icon:""),target:"_blank",rel:"noopener",innerHTML:t.value},null,8,Wn))}}),Jn=m(Yn,[["__scopeId","data-v-f80f8133"]]),Zn={class:"VPSocialLinks"},Qn=g({__name:"VPSocialLinks",props:{links:{}},setup(s){return(e,t)=>(a(),l("div",Zn,[(a(!0),l(T,null,A(e.links,({link:n,icon:o,ariaLabel:i})=>(a(),$(Jn,{key:n,icon:o,link:n,ariaLabel:i},null,8,["icon","link","ariaLabel"]))),128))]))}}),ke=m(Qn,[["__scopeId","data-v-7bc22406"]]),Xn={key:0,class:"group translations"},ea={class:"trans-title"},ta={key:1,class:"group"},sa={class:"item appearance"},oa={class:"label"},na={class:"appearance-action"},aa={key:2,class:"group"},ra={class:"item social-links"},ia=g({__name:"VPNavBarExtra",setup(s){const{site:e,theme:t}=P(),{localeLinks:n,currentLang:o}=W({correspondingLink:!0}),i=k(()=>n.value.length&&o.value.label||e.value.appearance||t.value.socialLinks);return(u,p)=>i.value?(a(),$($e,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:h(()=>[r(n).length&&r(o).label?(a(),l("div",Xn,[d("p",ea,V(r(o).label),1),(a(!0),l(T,null,A(r(n),v=>(a(),$(oe,{key:v.link,item:v},null,8,["item"]))),128))])):_("",!0),r(e).appearance&&r(e).appearance!=="force-dark"?(a(),l("div",ta,[d("div",sa,[d("p",oa,V(r(t).darkModeSwitchLabel||"Appearance"),1),d("div",na,[f(me)])])])):_("",!0),r(t).socialLinks?(a(),l("div",aa,[d("div",ra,[f(ke,{class:"social-links-list",links:r(t).socialLinks},null,8,["links"])])])):_("",!0)]),_:1})):_("",!0)}}),la=m(ia,[["__scopeId","data-v-d0bd9dde"]]),ca=s=>(E("data-v-e5dd9c1c"),s=s(),F(),s),ua=["aria-expanded"],da=ca(()=>d("span",{class:"container"},[d("span",{class:"top"}),d("span",{class:"middle"}),d("span",{class:"bottom"})],-1)),va=[da],ha=g({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(s){return(e,t)=>(a(),l("button",{type:"button",class:I(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=n=>e.$emit("click"))},va,10,ua))}}),pa=m(ha,[["__scopeId","data-v-e5dd9c1c"]]),fa=["innerHTML"],_a=g({__name:"VPNavBarMenuLink",props:{item:{}},setup(s){const{page:e}=P();return(t,n)=>(a(),$(z,{class:I({VPNavBarMenuLink:!0,active:r(G)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,tabindex:"0"},{default:h(()=>[d("span",{innerHTML:t.item.text},null,8,fa)]),_:1},8,["class","href","target","rel"]))}}),ma=m(_a,[["__scopeId","data-v-42ef59de"]]),ga=g({__name:"VPNavBarMenuGroup",props:{item:{}},setup(s){const e=s,{page:t}=P(),n=i=>"link"in i?G(t.value.relativePath,i.link,!!e.item.activeMatch):i.items.some(n),o=k(()=>n(e.item));return(i,u)=>(a(),$($e,{class:I({VPNavBarMenuGroup:!0,active:r(G)(r(t).relativePath,i.item.activeMatch,!!i.item.activeMatch)||o.value}),button:i.item.text,items:i.item.items},null,8,["class","button","items"]))}}),$a=s=>(E("data-v-7f418b0f"),s=s(),F(),s),ka={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},ba=$a(()=>d("span",{id:"main-nav-aria-label",class:"visually-hidden"},"Main Navigation",-1)),ya=g({__name:"VPNavBarMenu",setup(s){const{theme:e}=P();return(t,n)=>r(e).nav?(a(),l("nav",ka,[ba,(a(!0),l(T,null,A(r(e).nav,o=>(a(),l(T,{key:o.text},["link"in o?(a(),$(ma,{key:0,item:o},null,8,["item"])):(a(),$(ga,{key:1,item:o},null,8,["item"]))],64))),128))])):_("",!0)}}),wa=m(ya,[["__scopeId","data-v-7f418b0f"]]);function Pa(s){const{localeIndex:e,theme:t}=P();function n(o){var N,B,S;const i=o.split("."),u=(N=t.value.search)==null?void 0:N.options,p=u&&typeof u=="object",v=p&&((S=(B=u.locales)==null?void 0:B[e.value])==null?void 0:S.translations)||null,b=p&&u.translations||null;let y=v,w=b,M=s;const C=i.pop();for(const Y of i){let O=null;const K=M==null?void 0:M[Y];K&&(O=M=K);const ne=w==null?void 0:w[Y];ne&&(O=w=ne);const ae=y==null?void 0:y[Y];ae&&(O=y=ae),K||(M=O),ne||(w=O),ae||(y=O)}return(y==null?void 0:y[C])??(w==null?void 0:w[C])??(M==null?void 0:M[C])??""}return n}const Va=["aria-label"],La={class:"DocSearch-Button-Container"},Sa=d("svg",{class:"DocSearch-Search-Icon",width:"20",height:"20",viewBox:"0 0 20 20","aria-label":"search icon"},[d("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none","fill-rule":"evenodd","stroke-linecap":"round","stroke-linejoin":"round"})],-1),Ma={class:"DocSearch-Button-Placeholder"},Ca=d("span",{class:"DocSearch-Button-Keys"},[d("kbd",{class:"DocSearch-Button-Key"}),d("kbd",{class:"DocSearch-Button-Key"},"K")],-1),be=g({__name:"VPNavBarSearchButton",setup(s){const t=Pa({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(n,o)=>(a(),l("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":r(t)("button.buttonAriaLabel")},[d("span",La,[Sa,d("span",Ma,V(r(t)("button.buttonText")),1)]),Ca],8,Va))}}),Ia={class:"VPNavBarSearch"},Ta={id:"local-search"},Na={key:1,id:"docsearch"},Ba=g({__name:"VPNavBarSearch",setup(s){const e=()=>null,t=()=>null,{theme:n}=P(),o=L(!1),i=L(!1);x(()=>{});function u(){o.value||(o.value=!0,setTimeout(p,16))}function p(){const y=new Event("keydown");y.key="k",y.metaKey=!0,window.dispatchEvent(y),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||p()},16)}const v=L(!1),b="";return(y,w)=>{var M;return a(),l("div",Ia,[r(b)==="local"?(a(),l(T,{key:0},[v.value?(a(),$(r(e),{key:0,onClose:w[0]||(w[0]=C=>v.value=!1)})):_("",!0),d("div",Ta,[f(be,{onClick:w[1]||(w[1]=C=>v.value=!0)})])],64)):r(b)==="algolia"?(a(),l(T,{key:1},[o.value?(a(),$(r(t),{key:0,algolia:((M=r(n).search)==null?void 0:M.options)??r(n).algolia,onVnodeBeforeMount:w[2]||(w[2]=C=>i.value=!0)},null,8,["algolia"])):_("",!0),i.value?_("",!0):(a(),l("div",Na,[f(be,{onClick:u})]))],64)):_("",!0)])}}}),Aa=g({__name:"VPNavBarSocialLinks",setup(s){const{theme:e}=P();return(t,n)=>r(e).socialLinks?(a(),$(ke,{key:0,class:"VPNavBarSocialLinks",links:r(e).socialLinks},null,8,["links"])):_("",!0)}}),Ha=m(Aa,[["__scopeId","data-v-0394ad82"]]),za=["href","rel","target"],Ea={key:1},Fa={key:2},Da=g({__name:"VPNavBarTitle",setup(s){const{site:e,theme:t}=P(),{hasSidebar:n}=D(),{currentLang:o}=W(),i=k(()=>{var v;return typeof t.value.logoLink=="string"?t.value.logoLink:(v=t.value.logoLink)==null?void 0:v.link}),u=k(()=>{var v;return typeof t.value.logoLink=="string"||(v=t.value.logoLink)==null?void 0:v.rel}),p=k(()=>{var v;return typeof t.value.logoLink=="string"||(v=t.value.logoLink)==null?void 0:v.target});return(v,b)=>(a(),l("div",{class:I(["VPNavBarTitle",{"has-sidebar":r(n)}])},[d("a",{class:"title",href:i.value??r(pe)(r(o).link),rel:u.value,target:p.value},[c(v.$slots,"nav-bar-title-before",{},void 0,!0),r(t).logo?(a(),$(Q,{key:0,class:"logo",image:r(t).logo},null,8,["image"])):_("",!0),r(t).siteTitle?(a(),l("span",Ea,V(r(t).siteTitle),1)):r(t).siteTitle===void 0?(a(),l("span",Fa,V(r(e).title),1)):_("",!0),c(v.$slots,"nav-bar-title-after",{},void 0,!0)],8,za)],2))}}),Oa=m(Da,[["__scopeId","data-v-ab179fa1"]]),Ua={},Ga={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},xa=d("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),ja=d("path",{d:" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z ",class:"css-c4d79v"},null,-1),qa=[xa,ja];function Ka(s,e){return a(),l("svg",Ga,qa)}const ze=m(Ua,[["render",Ka]]),Ra={class:"items"},Wa={class:"title"},Ya=g({__name:"VPNavBarTranslations",setup(s){const{theme:e}=P(),{localeLinks:t,currentLang:n}=W({correspondingLink:!0});return(o,i)=>r(t).length&&r(n).label?(a(),$($e,{key:0,class:"VPNavBarTranslations",icon:ze,label:r(e).langMenuLabel||"Change language"},{default:h(()=>[d("div",Ra,[d("p",Wa,V(r(n).label),1),(a(!0),l(T,null,A(r(t),u=>(a(),$(oe,{key:u.link,item:u},null,8,["item"]))),128))])]),_:1},8,["label"])):_("",!0)}}),Ja=m(Ya,[["__scopeId","data-v-74abcbb9"]]),Za=s=>(E("data-v-19c990f1"),s=s(),F(),s),Qa={class:"wrapper"},Xa={class:"container"},er={class:"title"},tr={class:"content"},sr={class:"content-body"},or=Za(()=>d("div",{class:"divider"},[d("div",{class:"divider-line"})],-1)),nr=g({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(s){const{y:e}=Ve(),{hasSidebar:t}=D(),{hasLocalNav:n}=Te(),{frontmatter:o}=P(),i=L({});return ye(()=>{i.value={"has-sidebar":t.value,"has-local-nav":n.value,top:o.value.layout==="home"&&e.value===0}}),(u,p)=>(a(),l("div",{class:I(["VPNavBar",i.value])},[d("div",Qa,[d("div",Xa,[d("div",er,[f(Oa,null,{"nav-bar-title-before":h(()=>[c(u.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(u.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),d("div",tr,[d("div",sr,[c(u.$slots,"nav-bar-content-before",{},void 0,!0),f(Ba,{class:"search"}),f(wa,{class:"menu"}),f(Ja,{class:"translations"}),f(fn,{class:"appearance"}),f(Ha,{class:"social-links"}),f(la,{class:"extra"}),c(u.$slots,"nav-bar-content-after",{},void 0,!0),f(pa,{class:"hamburger",active:u.isScreenOpen,onClick:p[0]||(p[0]=v=>u.$emit("toggle-screen"))},null,8,["active"])])])])]),or],2))}}),ar=m(nr,[["__scopeId","data-v-19c990f1"]]),rr={key:0,class:"VPNavScreenAppearance"},ir={class:"text"},lr=g({__name:"VPNavScreenAppearance",setup(s){const{site:e,theme:t}=P();return(n,o)=>r(e).appearance&&r(e).appearance!=="force-dark"?(a(),l("div",rr,[d("p",ir,V(r(t).darkModeSwitchLabel||"Appearance"),1),f(me)])):_("",!0)}}),cr=m(lr,[["__scopeId","data-v-2d7af913"]]),ur=g({__name:"VPNavScreenMenuLink",props:{item:{}},setup(s){const e=se("close-screen");return(t,n)=>(a(),$(z,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:r(e)},{default:h(()=>[H(V(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}}),dr=m(ur,[["__scopeId","data-v-05f27b2a"]]),vr={},hr={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},pr=d("path",{d:"M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z"},null,-1),fr=[pr];function _r(s,e){return a(),l("svg",hr,fr)}const mr=m(vr,[["render",_r]]),gr=g({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(s){const e=se("close-screen");return(t,n)=>(a(),$(z,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,onClick:r(e)},{default:h(()=>[H(V(t.item.text),1)]),_:1},8,["href","target","rel","onClick"]))}}),Ee=m(gr,[["__scopeId","data-v-19976ae1"]]),$r={class:"VPNavScreenMenuGroupSection"},kr={key:0,class:"title"},br=g({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(s){return(e,t)=>(a(),l("div",$r,[e.text?(a(),l("p",kr,V(e.text),1)):_("",!0),(a(!0),l(T,null,A(e.items,n=>(a(),$(Ee,{key:n.text,item:n},null,8,["item"]))),128))]))}}),yr=m(br,[["__scopeId","data-v-8133b170"]]),wr=["aria-controls","aria-expanded"],Pr=["innerHTML"],Vr=["id"],Lr={key:1,class:"group"},Sr=g({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(s){const e=s,t=L(!1),n=k(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function o(){t.value=!t.value}return(i,u)=>(a(),l("div",{class:I(["VPNavScreenMenuGroup",{open:t.value}])},[d("button",{class:"button","aria-controls":n.value,"aria-expanded":t.value,onClick:o},[d("span",{class:"button-text",innerHTML:i.text},null,8,Pr),f(mr,{class:"button-icon"})],8,wr),d("div",{id:n.value,class:"items"},[(a(!0),l(T,null,A(i.items,p=>(a(),l(T,{key:p.text},["link"in p?(a(),l("div",{key:p.text,class:"item"},[f(Ee,{item:p},null,8,["item"])])):(a(),l("div",Lr,[f(yr,{text:p.text,items:p.items},null,8,["text","items"])]))],64))),128))],8,Vr)],2))}}),Mr=m(Sr,[["__scopeId","data-v-65ef89ca"]]),Cr={key:0,class:"VPNavScreenMenu"},Ir=g({__name:"VPNavScreenMenu",setup(s){const{theme:e}=P();return(t,n)=>r(e).nav?(a(),l("nav",Cr,[(a(!0),l(T,null,A(r(e).nav,o=>(a(),l(T,{key:o.text},["link"in o?(a(),$(dr,{key:0,item:o},null,8,["item"])):(a(),$(Mr,{key:1,text:o.text||"",items:o.items},null,8,["text","items"]))],64))),128))])):_("",!0)}}),Tr=g({__name:"VPNavScreenSocialLinks",setup(s){const{theme:e}=P();return(t,n)=>r(e).socialLinks?(a(),$(ke,{key:0,class:"VPNavScreenSocialLinks",links:r(e).socialLinks},null,8,["links"])):_("",!0)}}),Nr={class:"list"},Br=g({__name:"VPNavScreenTranslations",setup(s){const{localeLinks:e,currentLang:t}=W({correspondingLink:!0}),n=L(!1);function o(){n.value=!n.value}return(i,u)=>r(e).length&&r(t).label?(a(),l("div",{key:0,class:I(["VPNavScreenTranslations",{open:n.value}])},[d("button",{class:"title",onClick:o},[f(ze,{class:"icon lang"}),H(" "+V(r(t).label)+" ",1),f(He,{class:"icon chevron"})]),d("ul",Nr,[(a(!0),l(T,null,A(r(e),p=>(a(),l("li",{key:p.link,class:"item"},[f(z,{class:"link",href:p.link},{default:h(()=>[H(V(p.text),1)]),_:2},1032,["href"])]))),128))])],2)):_("",!0)}}),Ar=m(Br,[["__scopeId","data-v-d72aa483"]]),Hr={class:"container"},zr=g({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(s){const e=L(null),t=Le(R?document.body:null);return(n,o)=>(a(),$(de,{name:"fade",onEnter:o[0]||(o[0]=i=>t.value=!0),onAfterLeave:o[1]||(o[1]=i=>t.value=!1)},{default:h(()=>[n.open?(a(),l("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[d("div",Hr,[c(n.$slots,"nav-screen-content-before",{},void 0,!0),f(Ir,{class:"menu"}),f(Ar,{class:"translations"}),f(cr,{class:"appearance"}),f(Tr,{class:"social-links"}),c(n.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):_("",!0)]),_:3}))}}),Er=m(zr,[["__scopeId","data-v-cc5739dd"]]),Fr={key:0,class:"VPNav"},Dr=g({__name:"VPNav",setup(s){const{isScreenOpen:e,closeScreen:t,toggleScreen:n}=Ko(),{frontmatter:o}=P(),i=k(()=>o.value.navbar!==!1);return Se("close-screen",t),X(()=>{R&&document.documentElement.classList.toggle("hide-nav",!i.value)}),(u,p)=>i.value?(a(),l("header",Fr,[f(ar,{"is-screen-open":r(e),onToggleScreen:r(n)},{"nav-bar-title-before":h(()=>[c(u.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(u.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":h(()=>[c(u.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":h(()=>[c(u.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),f(Er,{open:r(e)},{"nav-screen-content-before":h(()=>[c(u.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":h(()=>[c(u.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):_("",!0)}}),Or=m(Dr,[["__scopeId","data-v-ae24b3ad"]]),Ur=s=>(E("data-v-e31bd47b"),s=s(),F(),s),Gr=["role","tabindex"],xr=Ur(()=>d("div",{class:"indicator"},null,-1)),jr={key:1,class:"items"},qr=g({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(s){const e=s,{collapsed:t,collapsible:n,isLink:o,isActiveLink:i,hasActiveLink:u,hasChildren:p,toggle:v}=$t(k(()=>e.item)),b=k(()=>p.value?"section":"div"),y=k(()=>o.value?"a":"div"),w=k(()=>p.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),M=k(()=>o.value?void 0:"button"),C=k(()=>[[`level-${e.depth}`],{collapsible:n.value},{collapsed:t.value},{"is-link":o.value},{"is-active":i.value},{"has-active":u.value}]);function N(S){"key"in S&&S.key!=="Enter"||!e.item.link&&v()}function B(){e.item.link&&v()}return(S,Y)=>{const O=q("VPSidebarItem",!0);return a(),$(U(b.value),{class:I(["VPSidebarItem",C.value])},{default:h(()=>[S.item.text?(a(),l("div",J({key:0,class:"item",role:M.value},Ze(S.item.items?{click:N,keydown:N}:{},!0),{tabindex:S.item.items&&0}),[xr,S.item.link?(a(),$(z,{key:0,tag:y.value,class:"link",href:S.item.link,rel:S.item.rel,target:S.item.target},{default:h(()=>[(a(),$(U(w.value),{class:"text",innerHTML:S.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(a(),$(U(w.value),{key:1,class:"text",innerHTML:S.item.text},null,8,["innerHTML"])),S.item.collapsed!=null?(a(),l("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:B,onKeydown:Je(B,["enter"]),tabindex:"0"},[f(Ne,{class:"caret-icon"})],32)):_("",!0)],16,Gr)):_("",!0),S.item.items&&S.item.items.length?(a(),l("div",jr,[S.depth<5?(a(!0),l(T,{key:0},A(S.item.items,K=>(a(),$(O,{key:K.text,item:K,depth:S.depth+1},null,8,["item","depth"]))),128)):_("",!0)])):_("",!0)]),_:1},8,["class"])}}}),Kr=m(qr,[["__scopeId","data-v-e31bd47b"]]),Fe=s=>(E("data-v-575e6a36"),s=s(),F(),s),Rr=Fe(()=>d("div",{class:"curtain"},null,-1)),Wr={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Yr=Fe(()=>d("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),Jr=g({__name:"VPSidebar",props:{open:{type:Boolean}},setup(s){const{sidebarGroups:e,hasSidebar:t}=D(),n=s,o=L(null),i=Le(R?document.body:null);return j([n,o],()=>{var u;n.open?(i.value=!0,(u=o.value)==null||u.focus()):i.value=!1},{immediate:!0,flush:"post"}),(u,p)=>r(t)?(a(),l("aside",{key:0,class:I(["VPSidebar",{open:u.open}]),ref_key:"navEl",ref:o,onClick:p[0]||(p[0]=Qe(()=>{},["stop"]))},[Rr,d("nav",Wr,[Yr,c(u.$slots,"sidebar-nav-before",{},void 0,!0),(a(!0),l(T,null,A(r(e),v=>(a(),l("div",{key:v.text,class:"group"},[f(Kr,{item:v,depth:0},null,8,["item"])]))),128)),c(u.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):_("",!0)}}),Zr=m(Jr,[["__scopeId","data-v-575e6a36"]]),Qr=g({__name:"VPSkipLink",setup(s){const e=te(),t=L();j(()=>e.path,()=>t.value.focus());function n({target:o}){const i=document.getElementById(decodeURIComponent(o.hash).slice(1));if(i){const u=()=>{i.removeAttribute("tabindex"),i.removeEventListener("blur",u)};i.setAttribute("tabindex","-1"),i.addEventListener("blur",u),i.focus(),window.scrollTo(0,0)}}return(o,i)=>(a(),l(T,null,[d("span",{ref_key:"backToTop",ref:t,tabindex:"-1"},null,512),d("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:n}," Skip to content ")],64))}}),Xr=m(Qr,[["__scopeId","data-v-0f60ec36"]]),ei=g({__name:"Layout",setup(s){const{isOpen:e,open:t,close:n}=D(),o=te();j(()=>o.path,n),gt(e,n);const{frontmatter:i}=P(),u=Xe(),p=k(()=>!!u["home-hero-image"]);return Se("hero-image-slot-exists",p),(v,b)=>{const y=q("Content");return r(i).layout!==!1?(a(),l("div",{key:0,class:I(["Layout",r(i).pageClass])},[c(v.$slots,"layout-top",{},void 0,!0),f(Xr),f(ot,{class:"backdrop",show:r(e),onClick:r(n)},null,8,["show","onClick"]),f(Or,null,{"nav-bar-title-before":h(()=>[c(v.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":h(()=>[c(v.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":h(()=>[c(v.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":h(()=>[c(v.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":h(()=>[c(v.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":h(()=>[c(v.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),f(qo,{open:r(e),onOpenMenu:r(t)},null,8,["open","onOpenMenu"]),f(Zr,{open:r(e)},{"sidebar-nav-before":h(()=>[c(v.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":h(()=>[c(v.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),f(mo,null,{"page-top":h(()=>[c(v.$slots,"page-top",{},void 0,!0)]),"page-bottom":h(()=>[c(v.$slots,"page-bottom",{},void 0,!0)]),"not-found":h(()=>[c(v.$slots,"not-found",{},void 0,!0)]),"home-hero-before":h(()=>[c(v.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":h(()=>[c(v.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":h(()=>[c(v.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":h(()=>[c(v.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":h(()=>[c(v.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":h(()=>[c(v.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":h(()=>[c(v.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":h(()=>[c(v.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":h(()=>[c(v.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":h(()=>[c(v.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":h(()=>[c(v.$slots,"doc-before",{},void 0,!0)]),"doc-after":h(()=>[c(v.$slots,"doc-after",{},void 0,!0)]),"doc-top":h(()=>[c(v.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":h(()=>[c(v.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":h(()=>[c(v.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":h(()=>[c(v.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":h(()=>[c(v.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":h(()=>[c(v.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":h(()=>[c(v.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":h(()=>[c(v.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),f(yo),c(v.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),$(y,{key:1}))}}}),ti=m(ei,[["__scopeId","data-v-5d98c3a5"]]),oi={Layout:ti,enhanceApp:({app:s})=>{s.component("Badge",et)}};export{oi as t}; diff --git a/docs/.vitepress/dist/assets/index.md.DsfENmNR.js b/docs/.vitepress/dist/assets/index.md.DsfENmNR.js new file mode 100644 index 000000000..5b0559d47 --- /dev/null +++ b/docs/.vitepress/dist/assets/index.md.DsfENmNR.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Bioloop","text":"Data Management and Archival","tagline":"Simplify the process of data archival.","image":{"dark":"/dark-logo.svg","light":"/logo.svg","alt":"Bioloop"},"actions":[{"theme":"alt","text":"Github","link":"https://github.com/IUSCA/bioloop"}]},"features":[{"title":"Installation","details":"Get Started with Bioloop","link":"/installation/install-docker"},{"title":"UI","details":"Explore the features of our UI.","link":"/ui/overview"},{"title":"API","details":"The backend.","link":"/api/introduction"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),o={name:"index.md"};function i(l,n,r,s,d,c){return a(),e("div")}const m=t(o,[["render",i]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/index.md.DsfENmNR.lean.js b/docs/.vitepress/dist/assets/index.md.DsfENmNR.lean.js new file mode 100644 index 000000000..5b0559d47 --- /dev/null +++ b/docs/.vitepress/dist/assets/index.md.DsfENmNR.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Bioloop","text":"Data Management and Archival","tagline":"Simplify the process of data archival.","image":{"dark":"/dark-logo.svg","light":"/logo.svg","alt":"Bioloop"},"actions":[{"theme":"alt","text":"Github","link":"https://github.com/IUSCA/bioloop"}]},"features":[{"title":"Installation","details":"Get Started with Bioloop","link":"/installation/install-docker"},{"title":"UI","details":"Explore the features of our UI.","link":"/ui/overview"},{"title":"API","details":"The backend.","link":"/api/introduction"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),o={name:"index.md"};function i(l,n,r,s,d,c){return a(),e("div")}const m=t(o,[["render",i]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/index.md.RtVxx9Um.js b/docs/.vitepress/dist/assets/index.md.RtVxx9Um.js deleted file mode 100644 index 0c14f0dab..000000000 --- a/docs/.vitepress/dist/assets/index.md.RtVxx9Um.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as t,c as e,o as a}from"./chunks/framework.MXVb71fM.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Bioloop","text":"Data Management and Archival","tagline":"Simplify the process of data archival.","image":{"dark":"/dark-logo.svg","light":"/logo.svg","alt":"Bioloop"},"actions":[{"theme":"alt","text":"Github","link":"https://github.com/IUSCA/bioloop"}]},"features":[{"title":"Installation","details":"Get Started with Bioloop","link":"/install-docker"},{"title":"UI","details":"Explore the features of our UI.","link":"/ui/"},{"title":"API","details":"The backend.","link":"/api/"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),o={name:"index.md"};function i(l,n,r,s,d,c){return a(),e("div")}const m=t(o,[["render",i]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/index.md.RtVxx9Um.lean.js b/docs/.vitepress/dist/assets/index.md.RtVxx9Um.lean.js deleted file mode 100644 index 0c14f0dab..000000000 --- a/docs/.vitepress/dist/assets/index.md.RtVxx9Um.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as t,c as e,o as a}from"./chunks/framework.MXVb71fM.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Bioloop","text":"Data Management and Archival","tagline":"Simplify the process of data archival.","image":{"dark":"/dark-logo.svg","light":"/logo.svg","alt":"Bioloop"},"actions":[{"theme":"alt","text":"Github","link":"https://github.com/IUSCA/bioloop"}]},"features":[{"title":"Installation","details":"Get Started with Bioloop","link":"/install-docker"},{"title":"UI","details":"Explore the features of our UI.","link":"/ui/"},{"title":"API","details":"The backend.","link":"/api/"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}'),o={name:"index.md"};function i(l,n,r,s,d,c){return a(),e("div")}const m=t(o,[["render",i]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.js b/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.js deleted file mode 100644 index 1163e2de1..000000000 --- a/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.js +++ /dev/null @@ -1,13 +0,0 @@ -import{_ as e,c as a,o as s,V as t}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"Run via docker","description":"","frontmatter":{},"headers":[],"relativePath":"install-docker.md","filePath":"install-docker.md"}'),o={name:"install-docker.md"},n=t(`

Run via docker

Docker standardizes the server environment and makes it easier to get a local development environment set up and running.

Setup

Requires docker. Docker desktop should work too.

For development purposes, shared volumes are used in docker-compose.yml to ensure container node_modules are not confused with host level node_modules. This approach also keeps node_modules folders out of the local directory to make it easier to find and grep.

To make adjustments to the way the application runs, edit and review docker-compose.yml.

The UI and API containers have been set to run on start up to install / update dependencies.

Set up the front-end ui client or back-end api server as needed.

.env files

ui/, api/ and workers/ all contain .env.example files. Copy these to a corresponding .env file and update values accordingly.

cp ui/.env.example ui/.env
-cp api/.env.example api/.env
-cp workers/.env.example workers/.env

OpenSSL

cd ui/
-mkdir .cert
-openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
cd api/keys
-./genkeys.sh

Note: when running under Windows, it may be necessary to run the openssl commands via Cygwin

Setup Migration and Seed Database

Run the initial migration:

docker compose exec api bash
-npx prisma migrate dev

Add any usernames you need to work with in api/prisma/data.js then seed the db:

npx prisma db seed

Starting / Stopping

Use Docker Compose

Bring up the containers:

docker compose up -d

Make sure everything is running (no Exit 1 States):

docker compose ps

To shut down the containers:

docker compose down -v

To see what is going on in a specific container:

docker compose logs -f api

Linting

To use linting with the docker setup you must have the dev dependencies of the api and ui installed locally as well as the VSCode extention ESLint (dbaeumer.vscode-eslint). You will need to run the install command for both api and ui:

npm install --save-dev

You can also install Dev Containers (ms-vscode-remote.remote-containers) to remote into both the api and ui containers separately. You'd have to have two instances of vscode running, but if you don't want to install dependencies locally this is the best way to run with automatic linting.

Testing

Try it out how it is. Open a browser and go to:

https://localhost

You may need to specify the https:// prefix manually. You may also have to accept a warning about an insecure connection since it's a self-signed certificate. The default configuration should be enough to get up and running.

Test the API with:

http://127.0.0.1:3030/health

To make more complex requests, use an API development tool like Hoppscotch or Insomnia:

https://hoppscotch.io/

To POST a request, choose POST, specify the URL, and in Body choose application/x-www-form-urlencoded for the Content Type

Queue

This application makes use of the Rhythm API for managing worker queues.

Queue folders need to belong to docker group

db/queue/
-chown -R \${USER}:docker db/queue/

Quick start

Getting the user permissions set correctly is an important step in making the application run.

bash
bin/dev.sh

Run this script from the project root.

  • It creates a .env file in the project root which has the user id (uid) and group id (gid) of the project root directory's owner. The processes inside the api and worker_api docker containers are run as a user with this UID and GID.
  • It builds both the api and worker_api images
  • It runs all the containers (ui, api, worker_api, queue, postgres, mongo_db)

Troubleshooting

Most containers have curl available. Connect to one and then try making requests to the service you're having issue with.

docker compose exec web bash
-curl -X GET http://api:3030/

(in this case, we don't need the /api suffix since we're behind the nginx proxy that normally adds /api for us)

Also, you can always insert console.log() statements in the code to see what values are at any given point.

You can check which ports are available locally and find something unique.

netstat -panl | grep " LISTEN "

Docker-compose

-f

If you have a compose file named something other than docker-compose.yml, you can specify the name with a -f flag:

docker compose -f docker-compose-prod.yml up -d

TIP: Shortcuts

Create bash aliases

The above commands can get tiring to type every time you want to take action with a compose environment. These shortcuts help.

Add the following to your .bashrc file (or equivalent)

alias dcu='docker compose up -d'
-alias dcd='docker compose down --remove-orphans'
-alias dcp='docker compose ps'
-alias dce='docker compose exec'
-alias dcl='docker compose logs'

via https://charlesbrandt.com/system/virtualization/docker-compose.html#shell-shortcuts

`,69),i=[n];function p(c,l,d,r,h,u){return s(),a("div",null,i)}const k=e(o,[["render",p]]);export{m as __pageData,k as default}; diff --git a/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.lean.js b/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.lean.js deleted file mode 100644 index 2c30ed231..000000000 --- a/docs/.vitepress/dist/assets/install-docker.md.-cK3qjUI.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as a,o as s,V as t}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"Run via docker","description":"","frontmatter":{},"headers":[],"relativePath":"install-docker.md","filePath":"install-docker.md"}'),o={name:"install-docker.md"},n=t("",69),i=[n];function p(c,l,d,r,h,u){return s(),a("div",null,i)}const k=e(o,[["render",p]]);export{m as __pageData,k as default}; diff --git a/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.js b/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.js new file mode 100644 index 000000000..d7f249a92 --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Installation","description":"","frontmatter":{"title":"Installation","order":1},"headers":[],"relativePath":"installation/index.md","filePath":"installation/index.md","lastUpdated":null}'),n={name:"installation/index.md"};function i(o,s,l,r,d,c){return a(),e("div")}const m=t(n,[["render",i]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.lean.js b/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.lean.js new file mode 100644 index 000000000..d7f249a92 --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_index.md.DgJxg3Lo.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Installation","description":"","frontmatter":{"title":"Installation","order":1},"headers":[],"relativePath":"installation/index.md","filePath":"installation/index.md","lastUpdated":null}'),n={name:"installation/index.md"};function i(o,s,l,r,d,c){return a(),e("div")}const m=t(n,[["render",i]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.js b/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.js new file mode 100644 index 000000000..2ceb48d1a --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.js @@ -0,0 +1,37 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Docker","description":"","frontmatter":{"title":"Docker","order":1},"headers":[],"relativePath":"installation/install-docker.md","filePath":"installation/install-docker.md","lastUpdated":null}'),n={name:"installation/install-docker.md"};function l(h,s,p,k,o,r){return e(),a("div",null,s[0]||(s[0]=[t(`

Installation Guide

This guide will help you set up Bioloop using Docker for local development or production deployment.

Prerequisites

  • Docker Engine or Docker Desktop
  • Node.js 16+ (for local development without Docker)
  • OpenSSL (for generating certificates)
  • Git (for cloning the repository)

Quick Start

  1. Clone the repository and navigate to the project directory
bash
git clone https://github.com/IUSCA/bioloop.git
+cd bioloop
  1. Set up environment files
bash
cp ui/.env.example ui/.env
+cp api/.env.example api/.env
+cp workers/.env.example workers/.env
  1. Generate required certificates
bash
# For UI HTTPS
+cd ui/
+mkdir .cert
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem 
+cd ..
+
+# For API JWT
+cd api/keys
+./genkeys.sh
+cd ../..
  1. Start the services
bash
docker compose up -d

Development Setup Details

The project uses Docker Compose for development with some key features:

  • Shared volumes for node_modules to avoid conflicts with host
  • Hot-reload enabled for UI and API
  • Automatic dependency installation on container startup
  • PostgreSQL database with automatic migrations

Configuration

Environment Variables

Each component requires specific environment variables to be set:

  • UI: Authentication endpoints, API URL
  • API: Database connection, JWT secrets
  • Workers: Queue settings, processing parameters

See the .env.example files in each directory for required variables.

Docker Configuration

The application behavior can be customized by editing:

  • docker-compose.yml - Development setup
  • docker-compose-prod.yml - Production configuration

Database Setup

  1. Run initial migrations:
bash
docker compose exec api bash
+npx prisma migrate dev
  1. Seed the database:
bash
# Edit api/prisma/data.js to add required users first
+npx prisma db seed

Common Operations

Starting Services

bash
docker compose up -d      # Start all services
+docker compose up ui api  # Start specific services

Checking Status

bash
docker compose ps         # List container status
+docker compose logs -f    # Follow all logs
+docker compose logs api   # View API logs

Stopping Services

bash
docker compose down       # Stop and remove containers
+docker compose down -v    # Also remove volumes

Development Tools

Code Linting

Two options for ESLint integration:

  1. Local Installation:
bash
# Install dev dependencies locally
+cd api && npm install --save-dev
+cd ../ui && npm install --save-dev
+
+# Install VSCode ESLint extension
+code --install-extension dbaeumer.vscode-eslint
  1. Using Dev Containers:
  • Install VSCode Dev Containers extension
  • Open API and UI folders in separate VSCode windows
  • Reopen in container when prompted

Testing

  1. UI Testing:
bash
# Access the UI
+open https://localhost

Note: Accept the self-signed certificate warning

  1. API Testing:
bash
# Check API health
+curl http://127.0.0.1:3030/health
+
+# For complex API testing, use:
+- Hoppscotch (https://hoppscotch.io)
+- Insomnia
+- Postman

Queue System

The application uses Rhythm API for task queues.

Setup queue permissions:

bash
sudo chown -R \${USER}:docker db/queue/

Troubleshooting Guide

Common Issues

  1. Container Access:
bash
docker compose exec web bash
+curl -X GET http://api:3030/
  1. Port Conflicts:
bash
netstat -panl | grep " LISTEN "
  1. Logs:
bash
docker compose logs -f service_name

Development Tips

  1. Use the quick start script:
bash
bin/dev.sh

This handles:

  • User permissions setup
  • Image building
  • Container orchestration
  1. Useful Docker Compose Aliases:
bash
# Add to ~/.bashrc
+alias dcu='docker compose up -d'
+alias dcd='docker compose down --remove-orphans'
+alias dcp='docker compose ps'
+alias dce='docker compose exec'
+alias dcl='docker compose logs'
`,68)]))}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.lean.js b/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.lean.js new file mode 100644 index 000000000..5fd34cfe3 --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_install-docker.md.C33pKkyI.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Docker","description":"","frontmatter":{"title":"Docker","order":1},"headers":[],"relativePath":"installation/install-docker.md","filePath":"installation/install-docker.md","lastUpdated":null}'),n={name:"installation/install-docker.md"};function l(h,s,p,k,o,r){return e(),a("div",null,s[0]||(s[0]=[t("",68)]))}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.js b/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.js new file mode 100644 index 000000000..646414f53 --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.js @@ -0,0 +1,38 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Local","description":"","frontmatter":{"title":"Local","order":2},"headers":[],"relativePath":"installation/install-local.md","filePath":"installation/install-local.md","lastUpdated":null}'),n={name:"installation/install-local.md"};function l(p,s,h,k,o,r){return e(),a("div",null,s[0]||(s[0]=[t(`

Local Installation Guide

This guide provides step-by-step instructions for setting up and running the Bioloop application on your local machine.

Overview

The Bioloop application consists of several components:

  • API server
  • Web UI
  • Worker processes
  • PostgreSQL database

This guide covers the installation and configuration of all these components in a local development environment.


Steps to setup API and run natively on development machine (not using docker)

  • Create .env file
  • Generate token signing key pair
  • Install dependencies
  • Generate API Doc
bash
cd api/
+cp .env.example .env
+
+cd keys
+./genkeys.sh
+cd ..
+
+npm install && npm install --save-dev
+npm run swagger

This step is required only if you are working with workflows, otherwise you can set WORKFLOW_AUTH_TOKEN to any value and API calls to Rhythm API will fail but the App will still run.

Generate an access token to connect to the Rhythm API.

  • Go to Rhythm API instance (local or deployed)- cd <rhythm_api>
  • If rhythm api is running locally: python -m rhythm_api.scripts.issue_token --sub <app-id>
  • If rhythm api is running in docker: sudo docker compose -f "docker-compose-prod.yml" exec api python -m rhythm_api.scripts.issue_token --sub <app-id>

Make these changes to the api/.env file:

bash
NODE_ENV=default
+WORKFLOW_AUTH_TOKEN=<token>
+DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"
  • Initialize database
  • Set up schema
  • Populate with dummy data
bash
docker-compose up -d postgres
+cd api/
+npx prisma db push
+npx prisma db seed

Start the server: npm run start

Steps to setup UI and run natively on development machine (not using docker)

  • Create .env file
  • Create self-signed certificate for https://localhost
  • install dependencies
bash
cd ui/
+cp .env.example .env
+
+mkdir .cert
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
+
+npm install && npm install --save-dev

Make these changes to ui/.env file:

  • change the hostname in VITE_API_REDIRECT_URL to localhost
bash
VITE_API_REDIRECT_URL=http://localhost:3030

Start the vite server: npm run dev

Set Up Workers locally

Running workers on your dev machine has limitations:

  • Cannot work with SDA - cannot install hsi on dev machine.
  • Difficult to test with large files (~100GB)
  • Workers run external commands - tar, fastqc, multiqc whose interface and behavior may change between OS platforms.
  • Working with mounted file systems (Slate Scratch, and others) has its own quirks which you will not encounter on your dev machine.

Steps:

  • Install miniconda

  • Create a virtual environment: conda create -n colo python=3.9

    • The production servers colo23, colo25 have python version 3.9.8 installed (as of June 2023). If the default python version in the production servers change, update it in your development machine too.
  • Activate it: conda activate colo

  • Install poetry - pip install -U poetry

  • Install dependencies - poetry install

    • Poetry will detect it is running in a virtual environment and won't create another/
  • Create .env

bash
cd workers
+cp .env.example .env
  • Generate an auth token to access the app api and add it to .env against AUTH_TOKEN.
bash
cd api/
+node src/scripts/issue_token.js svc_tasks
  • Workers connect to the mongodb and rabbitmq of a Rhythm API instance. You can either setup a Rhythm API instance locally or connect to core-dev1.sca.iu.edu using Group VPN (This option is not recommended as it is used for production now. We plan to use core.sca.iu.edu for production in the future.)

  • Update paths in config for local development: TODO

  • Stat celery:

bash
cd workers
+workers/scripts/start_celery.sh

Setup a Test Instance of Workers in colo nodes

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

image

  • start postgres locally using docker
bash
cd <app_name>
+docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
+docker-compose up queue mongo -d
+poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
+npm run start
bash
cd <app_name>/ui
+npm run dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \\
+  -A \\
+  -R 3130:localhost:3030 \\
+  -R 28017:localhost:27017 \\
+  -R 5772:localhost:5672 \\
+  bioloopuser@colo23.carbonate.uits.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
+colo23> git checkout dev
+colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
+colo23> poetry install
+colo23> poetry shell
+colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1
`,52)]))}const F=i(n,[["render",l]]);export{c as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.lean.js b/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.lean.js new file mode 100644 index 000000000..bd7d68760 --- /dev/null +++ b/docs/.vitepress/dist/assets/installation_install-local.md.DZiWe0sy.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Local","description":"","frontmatter":{"title":"Local","order":2},"headers":[],"relativePath":"installation/install-local.md","filePath":"installation/install-local.md","lastUpdated":null}'),n={name:"installation/install-local.md"};function l(p,s,h,k,o,r){return e(),a("div",null,s[0]||(s[0]=[t("",52)]))}const F=i(n,[["render",l]]);export{c as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2 deleted file mode 100644 index 2a6872967..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 new file mode 100644 index 000000000..b6b603d59 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-cyrillic.-nLMcIwj.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic.-nLMcIwj.woff2 deleted file mode 100644 index f64035158..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-cyrillic.-nLMcIwj.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 b/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 new file mode 100644 index 000000000..def40a4f6 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 new file mode 100644 index 000000000..e070c3d30 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek-ext.hznxWNZO.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek-ext.hznxWNZO.woff2 deleted file mode 100644 index 002189603..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-greek-ext.hznxWNZO.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 new file mode 100644 index 000000000..a3c16ca40 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-greek.PSfer2Kc.woff2 b/docs/.vitepress/dist/assets/inter-italic-greek.PSfer2Kc.woff2 deleted file mode 100644 index 71c265f85..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-greek.PSfer2Kc.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 new file mode 100644 index 000000000..2210a899e Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin-ext.RnFly65-.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin-ext.RnFly65-.woff2 deleted file mode 100644 index 9c1b9440e..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-latin-ext.RnFly65-.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin.27E69YJn.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin.27E69YJn.woff2 deleted file mode 100644 index 01fcf2072..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-latin.27E69YJn.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 b/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 new file mode 100644 index 000000000..790d62dc7 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 b/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 new file mode 100644 index 000000000..1eec0775a Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-italic-vietnamese.xzQHe1q1.woff2 b/docs/.vitepress/dist/assets/inter-italic-vietnamese.xzQHe1q1.woff2 deleted file mode 100644 index e4f788ee0..000000000 Binary files a/docs/.vitepress/dist/assets/inter-italic-vietnamese.xzQHe1q1.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2 deleted file mode 100644 index 28593ccb8..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 new file mode 100644 index 000000000..2cfe61536 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 new file mode 100644 index 000000000..e3886dd14 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-cyrillic.jIZ9REo5.woff2 b/docs/.vitepress/dist/assets/inter-roman-cyrillic.jIZ9REo5.woff2 deleted file mode 100644 index a20adc161..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-cyrillic.jIZ9REo5.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek-ext.9JiNzaSO.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek-ext.9JiNzaSO.woff2 deleted file mode 100644 index e3b0be76d..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-greek-ext.9JiNzaSO.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 new file mode 100644 index 000000000..36d67487d Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 new file mode 100644 index 000000000..2bed1e85e Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-greek.Cb5wWeGA.woff2 b/docs/.vitepress/dist/assets/inter-roman-greek.Cb5wWeGA.woff2 deleted file mode 100644 index f790e047d..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-greek.Cb5wWeGA.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 new file mode 100644 index 000000000..9a8d1e2b5 Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin-ext.GZWE-KO4.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin-ext.GZWE-KO4.woff2 deleted file mode 100644 index 715bd903b..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-latin-ext.GZWE-KO4.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 new file mode 100644 index 000000000..07d3c53ae Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-latin.bvIUbFQP.woff2 b/docs/.vitepress/dist/assets/inter-roman-latin.bvIUbFQP.woff2 deleted file mode 100644 index a540b7afe..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-latin.bvIUbFQP.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 b/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 new file mode 100644 index 000000000..57bdc22ae Binary files /dev/null and b/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 differ diff --git a/docs/.vitepress/dist/assets/inter-roman-vietnamese.paY3CzEB.woff2 b/docs/.vitepress/dist/assets/inter-roman-vietnamese.paY3CzEB.woff2 deleted file mode 100644 index 5a9f9cb9c..000000000 Binary files a/docs/.vitepress/dist/assets/inter-roman-vietnamese.paY3CzEB.woff2 and /dev/null differ diff --git a/docs/.vitepress/dist/assets/metrics.md.mr45A369.js b/docs/.vitepress/dist/assets/metrics.md.mr45A369.js new file mode 100644 index 000000000..1893565f2 --- /dev/null +++ b/docs/.vitepress/dist/assets/metrics.md.mr45A369.js @@ -0,0 +1 @@ +import{_ as t,c as i,o as a,ag as s}from"./chunks/framework.C9SxlbOG.js";const p=JSON.parse('{"title":"Metrics and Monitoring","description":"","frontmatter":{},"headers":[],"relativePath":"metrics.md","filePath":"metrics.md","lastUpdated":null}'),o={name:"metrics.md"};function r(n,e,l,c,h,d){return a(),i("div",null,e[0]||(e[0]=[s('

Metrics and Monitoring

This project uses Prometheus and Grafana for monitoring and visualizing application performance metrics. These tools are essential for understanding system behavior, identifying bottlenecks, and ensuring the application runs smoothly in production.

Prometheus

Prometheus is a powerful monitoring system that collects and stores metrics from various sources. It is configured to scrape metrics from the following targets:

  • Database (Postgres): Metrics are exposed via postgres_exporter, which is defined in docker-compose.yml.
  • Node.js and Express.js app: Metrics are exposed via the prom-client library.

Configuration

  • Service Definition: The Prometheus service is defined in docker-compose.yml.
  • Configuration File: Located at metrics/prometheus/config/prometheus.yml.

Without Prometheus

Without Prometheus, there would be no centralized system to collect and store metrics, making it difficult to monitor application performance or detect issues in real-time.

Integration

Prometheus runs in the same Docker network as the Node.js app and Postgres database, allowing it to scrape metrics directly from their endpoints.

Grafana

Grafana is used to visualize the metrics collected by Prometheus. It provides dashboards that make it easy to analyze system performance and identify trends.

Features

  • Pre-configured Dashboards:
    • Node.js app metrics
    • Postgres metrics
  • Datasource: Automatically configured to use Prometheus as the datasource.
  • Access: Accessible at https://localhost/grafana/. Only users with the admin role can access it.

Configuration

  • Service Definition: The Grafana service is defined in docker-compose.yml.
  • Configuration Files:
    • metrics/grafana/config/grafana.ini: Contains Grafana server and authentication settings.
    • metrics/grafana/provisioning/datasources: Configures Prometheus as the datasource.
    • metrics/grafana/provisioning/dashboards: Defines the dashboards to be imported.

Authentication and Authorization

  • JWT Authentication: Grafana uses JWT tokens for authentication.
    • Admin users receive a secure, HTTPS-only cookie containing the JWT token.
    • The reverse proxy forwards this token to Grafana as a header (X-JWT-Assertion).
  • Reverse Proxy:
    • In development: Vite is used as the reverse proxy (ui/vite.config.js).
    • In production: Nginx is used as the reverse proxy (nginx/conf/app.conf).

Integration

Grafana is integrated into the same Docker network as Prometheus, ensuring seamless access to metrics.

How This Setup Helps

  • Centralized Monitoring: Prometheus collects metrics from multiple sources, while Grafana visualizes them in a single interface.
  • Maintainability: The configuration files are modular and well-organized, making it easy to update or extend the setup.
  • Real-time Insights: Developers can monitor application performance in real-time, enabling faster debugging and optimization.

Usage Instructions

  1. Start the Services: Ensure Docker is running and start the services using:

    bash
    docker-compose up -d
  2. View Dashboards:

    • Log into bioloop with the admin credentials.
    • In the sidebar, click on Metrics to access the Grafana dashboards screen.
    • Navigate to the pre-configured dashboards for Node.js and Postgres metrics.
  3. Add New Metrics:

    • For Node.js: See Instrumentation to add custom metrics.
    • For Postgres: Update the queries.yml file in metrics/postgres_exporter/.
  4. Restart Services: After making changes to configurations, restart the affected services:

    bash
    docker-compose restart <service_name>

This setup ensures a robust monitoring system that is easy to maintain and extend as the application grows.

',26)]))}const g=t(o,[["render",r]]);export{p as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/metrics.md.mr45A369.lean.js b/docs/.vitepress/dist/assets/metrics.md.mr45A369.lean.js new file mode 100644 index 000000000..e51298bcf --- /dev/null +++ b/docs/.vitepress/dist/assets/metrics.md.mr45A369.lean.js @@ -0,0 +1 @@ +import{_ as t,c as i,o as a,ag as s}from"./chunks/framework.C9SxlbOG.js";const p=JSON.parse('{"title":"Metrics and Monitoring","description":"","frontmatter":{},"headers":[],"relativePath":"metrics.md","filePath":"metrics.md","lastUpdated":null}'),o={name:"metrics.md"};function r(n,e,l,c,h,d){return a(),i("div",null,e[0]||(e[0]=[s("",26)]))}const g=t(o,[["render",r]]);export{p as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.js b/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.js new file mode 100644 index 000000000..c5d96cc92 --- /dev/null +++ b/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.js @@ -0,0 +1 @@ +import{_ as t,c as s,o as i,ag as a}from"./chunks/framework.C9SxlbOG.js";const h=JSON.parse('{"title":"Contributing","description":"","frontmatter":{"title":"Contributing"},"headers":[],"relativePath":"pull_request_template.md","filePath":"pull_request_template.md","lastUpdated":null}'),n={name:"pull_request_template.md"};function o(r,e,l,p,c,d){return i(),s("div",null,e[0]||(e[0]=[a("

Description

Please provide a brief description of the changes made in this PR.

Related Issue(s)

Closes #[Issue Number]

If applicable, please reference the issue(s) that this PR addresses. If the PR does not address any specific issue, you can remove this section.

Changes Made

List the main changes made in this PR. Be as specific as possible.

  • [ ] Feature added
  • [ ] Bug fixed
  • [ ] Code refactored
  • [ ] Tests changed
  • [ ] Documentation updated
  • [ ] Other changes: [describe]

Screenshots (if applicable)

Provide screenshots or GIFs that visually represent the changes. If not applicable, you can remove this section.

Checklist

Before submitting this PR, please make sure that:

  • [ ] Your code passes linting and coding style checks.
  • [ ] Documentation has been updated to reflect the changes.
  • [ ] You have reviewed your own code and resolved any merge conflicts.
  • [ ] You have requested a review from at least one team member.
  • [ ] Any relevant issue(s) have been linked to this PR.

Additional Information

Add any other information or context that may be relevant to this PR. This can include potential impacts, known issues, or future work related to this change.

",15)]))}const m=t(n,[["render",o]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.lean.js b/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.lean.js new file mode 100644 index 000000000..9a4414dda --- /dev/null +++ b/docs/.vitepress/dist/assets/pull_request_template.md.DoTLC-PI.lean.js @@ -0,0 +1 @@ +import{_ as t,c as s,o as i,ag as a}from"./chunks/framework.C9SxlbOG.js";const h=JSON.parse('{"title":"Contributing","description":"","frontmatter":{"title":"Contributing"},"headers":[],"relativePath":"pull_request_template.md","filePath":"pull_request_template.md","lastUpdated":null}'),n={name:"pull_request_template.md"};function o(r,e,l,p,c,d){return i(),s("div",null,e[0]||(e[0]=[a("",15)]))}const m=t(n,[["render",o]]);export{h as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.js b/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.js deleted file mode 100644 index 654558573..000000000 --- a/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as s,V as a}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"pull_request_template.md","filePath":"pull_request_template.md"}'),i={name:"pull_request_template.md"},o=a("

Description

Please provide a brief description of the changes made in this PR.

Related Issue(s)

Closes #[Issue Number]

If applicable, please reference the issue(s) that this PR addresses. If the PR does not address any specific issue, you can remove this section.

Changes Made

List the main changes made in this PR. Be as specific as possible.

  • [ ] Feature added
  • [ ] Bug fixed
  • [ ] Code refactored
  • [ ] Documentation updated
  • [ ] Other changes: [describe]

Screenshots (if applicable)

Provide screenshots or GIFs that visually represent the changes. If not applicable, you can remove this section.

Checklist

Before submitting this PR, please make sure that:

  • [ ] Your code passes linting and coding style checks.
  • [ ] Documentation has been updated to reflect the changes.
  • [ ] You have reviewed your own code and resolved any merge conflicts.
  • [ ] You have requested a review from at least one team member.
  • [ ] Any relevant issue(s) have been linked to this PR.

Additional Information

Add any other information or context that may be relevant to this PR. This can include potential impacts, known issues, or future work related to this change.

",15),n=[o];function r(l,p,c,d,h,u){return s(),t("div",null,n)}const f=e(i,[["render",r]]);export{m as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.lean.js b/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.lean.js deleted file mode 100644 index ae9894916..000000000 --- a/docs/.vitepress/dist/assets/pull_request_template.md.Du47Iz7_.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as s,V as a}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"pull_request_template.md","filePath":"pull_request_template.md"}'),i={name:"pull_request_template.md"},o=a("",15),n=[o];function r(l,p,c,d,h,u){return s(),t("div",null,n)}const f=e(i,[["render",r]]);export{m as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.js b/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.js deleted file mode 100644 index 0eaf13f53..000000000 --- a/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as a,V as o,a5 as s}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"Secure Download Documentation","description":"","frontmatter":{},"headers":[],"relativePath":"secure_download.md","filePath":"secure_download.md"}'),i={name:"secure_download.md"},n=o('

Secure Download Documentation

Table of Contents

  • Introduction
  • Requirements
  • Staging the Dataset
  • Access Control
  • Architecture Overview
  • Downloading a Dataset File

1. Introduction

This document provides an overview of the Secure Download functionality, detailing the requirements and architecture of the system. The primary goal is to allow authorized users to download dataset files both directly from the browser and from Slate Scratch while ensuring strict access control to prevent unauthorized access.

2. Requirements and Limitations

Users with access to the dataset should be able to download dataset files directly from their web browsers. The file download link should be accessible only to users with the necessary permissions.

Dataset files must be staged before attempting to download.

The staged files should be protected from unauthorizzed users who have access to slate scratch from navigating and/or downloading dataset files.

2.1 Limitations

The UI and API are running on a node where the Slate Scratch path is not mounted, preventing direct file access. An Nginx server is hosted on the colo node, which has access to the staged files. However, configuring Nginx as a simple file server would allow anyone to access any files. However, the download file server cannot determine users'access because the access control data, specifying which users have access to which datasets and files, is maintained by the API in a PostgreSQL database.

3. Staging the Dataset

The Staging Dataset functionality allows users to request the download of dataset files through the UI or API. Staging a dataset involves the transfer of the requested dataset file from the SDA (Source Data Archive) to a temporary location known as the Slate Scratch path. This intermediate step ensures efficient and secure access to the dataset while preventing unauthorized access through path enumeration.

Staging Process:

  • User Request: Users initiate a dataset file download request through either the user interface (UI) or the application programming interface (API).

  • Rhythm Workflow: The request triggers a rhythm workflow "Stage" on the colo node where the celery tasks are registered. "stage_dataset", "validate_dataset", "setup_dataset_download" are the celery tasks involved in this workflow.

  • Path Randomization: During dataset staging, a random string, in the form of a universally unique identifier (UUID) called stage_alias, is incorporated into the path. This UUID is generated to limit access to datasets through path enumeration. The staging path follows the pattern: <stage_directory>/<dataset_stage_alias>/<dataset_name>. "stage_dataset" celery task downloads the dataset tar from SDA and extarcts to this randomized path.

Example:

/stage_directory/6a5b1734-98e8-4e47-ae5f-1a9e5d8d9f7c/dataset_name
  • Symlink Creation: A symlink <download_path>/<dataset_stage_alias> is created to point to <stage_directory>/<dataset_stage_alias>/<dataset_name>. This will be path given to the users who want to download the data from the Slate Scratch directly. The file download nginx server is configured to server files from the <download_path> directory.

To enhance security, the access control list (ACL) of the <stage_directory>/<dataset_stage_alias> directory is carefully configured. It is limited to granting only the "execute" permission bit (--x) for the "others" group. This permission setup ensures that users with access to the Slate Scratch path cannot browse or navigate through all datasets present in the directory. Instead, only users who possess the complete path <stage_directory>/<dataset_stage_alias>/<dataset_name> are allowed to read the contents of the staged dataset.

UUID Generation

The UUID used in the staging path is generated deterministically. It is a function of the dataset type, dataset name, and a salt string. This deterministic approach ensures consistency and allows authorized users to access the staged dataset when needed, while making it computationally infeasible for users to guess the path of other datasets.

By implementing these measures, the Staging Dataset functionality maintains data security and privacy, preventing unauthorized access and ensuring the integrity of the staged datasets.

4. Access Control

Access control is managed through the project membership and user roles by the API. Users of "operator" and "admin" roles can access any dataset and its files. Users with "user" can only access the datasets that are associated with projects the user is a member of. This role-based access control ensures that sensitive dataset information is only accessible to those who have been explicitly granted access rights.

To further enhance security and prevent unauthorized access to dataset files, a secure ephemeral resource bound URL is constructed. This URL contains critical components to ensure its validity and security: Components of the Secure URL:

  • File Path: The URL contains the file path relative to the download directory of the dataset file that needs to be downloaded.
  • JWT (JSON Web Token): A very short-lived JWT is included as part of the URL. The JWT payload includes the file path as one of its components.

URL Validation:

For the URL to be considered valid, the following conditions must be met:

  • JWT Validity: The JWT included in the URL must be valid, meaning it must have a valid signature and must not be expired.
  • Path Match: The file path inside the JWT payload should match the file path specified in the path section of the URL.

By enforcing these conditions, the system ensures that even if an unauthorized user somehow obtains a link to a dataset file sometime later, they cannot access it. It also makes sure that a token cannot be used to download other files even if it is unexpired and has a valid signature.

5. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the file browser view to initiate file downloads.
  • API: This node serves the user interface and API endpoints with user and dataset metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Workers / Colo Node: Staging of dataset files occurs on this node via a Celery task. The node also hosts an Nginx server with access to the staged files.
  • Signet: An OAuth server supporting client credential flow with download file scope to create secure tokens.
  • File Download Server (Nginx): A file server on the colo adjacent to data which recieves requests from users to download large dataset files.
  • Secure Download API: A lightweight app with one endpoint that validates the incoming requests to the file download server

6. Downloading a Dataset File

To download a dataset file:

  • Step 1. An authorized user logs in through the UI and requests to download a dataset file.
  • Steps 2,3. The API checks if the user has access to the requested file by querying the database and constructs the download file path.
  • Step 4,5. Requests the Signet oauth server for a download token with the file path
  • Step 6. Responds to UI with the download file url and download token.
  • Step 7. UI Client constructs a URL by including the download token as a query parameter and requests the file download server.
  • Step 8. File server forwards the request to SecureDownloadAPI for validation.
  • Step 9.10. SecureDownloadAPI fetches the public JWT verfication keys from the Signet Oauth server in the form of JWKS and perform URL validation as described in section 4.
  • Step 11. If it is valid, it responds to file server (Nginx) with a special header x-accel-redirect with a value of the path of the requested file.
  • Step 12. Nginx performs internal redirect and sends the requested file to the UIclient with headers invoking a browser file download.
',37),r=[n];function l(d,h,c,u,p,g){return a(),t("div",null,r)}const w=e(i,[["render",l]]);export{m as __pageData,w as default}; diff --git a/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.lean.js b/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.lean.js deleted file mode 100644 index b22b0854e..000000000 --- a/docs/.vitepress/dist/assets/secure_download.md.3INgcYxh.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as a,V as o,a5 as s}from"./chunks/framework.MXVb71fM.js";const m=JSON.parse('{"title":"Secure Download Documentation","description":"","frontmatter":{},"headers":[],"relativePath":"secure_download.md","filePath":"secure_download.md"}'),i={name:"secure_download.md"},n=o("",37),r=[n];function l(d,h,c,u,p,g){return a(),t("div",null,r)}const w=e(i,[["render",l]]);export{m as __pageData,w as default}; diff --git a/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.js b/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.js new file mode 100644 index 000000000..982e4dba4 --- /dev/null +++ b/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.js @@ -0,0 +1,2 @@ +import{_ as t,c as a,o as s,ag as o}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/secure-download-arch-diagram.png",p=JSON.parse('{"title":"Secure Download","description":"","frontmatter":{},"headers":[],"relativePath":"secure_download.md","filePath":"secure_download.md","lastUpdated":null}'),n={name:"secure_download.md"};function r(l,e,d,h,c,u){return s(),a("div",null,e[0]||(e[0]=[o(`

Secure Download

Table of Contents

  • Introduction
  • Requirements
  • Staging the Dataset
  • Access Control
  • Architecture Overview
  • Downloading a Dataset File

1. Introduction

This document provides an overview of the Secure Download functionality, detailing the requirements and architecture of the system. The primary goal is to allow authorized users to download dataset files both directly from the browser and from Slate Scratch while ensuring strict access control to prevent unauthorized access.

2. Requirements and Limitations

Users with access to the dataset should be able to download dataset files directly from their web browsers. The file download link should be accessible only to users with the necessary permissions.

Dataset files must be staged before attempting to download.

The staged files should be protected from unauthorized users who have access to slate scratch from navigating and/or downloading dataset files.

2.1 Limitations

The UI and API are running on a node where the Slate Scratch path is not mounted, preventing direct file access. An Nginx server is hosted on the colo node, which has access to the staged files. However, configuring Nginx as a simple file server would allow anyone to access any files. However, the download file server cannot determine users'access because the access control data, specifying which users have access to which datasets and files, is maintained by the API in a PostgreSQL database.

3. Staging the Dataset

The Staging Dataset functionality allows users to request the download of individual dataset files (or the entire dataset, as a bundled file) through the UI or API. Staging a dataset involves the transfer of the corresponding archived bundle from the SDA (Source Data Archive) to a temporary location known as the Slate Scratch path. This intermediate step ensures efficient and secure access to the dataset while preventing unauthorized access through path enumeration.

Staging Process:

  • User Request: Users initiate a dataset file/bundle download request through either the user interface (UI) or the application programming interface (API).

  • Rhythm Workflow: The request triggers a rhythm workflow "Stage" on the colo node where the celery tasks are registered. "stage_dataset", "validate_dataset", "setup_dataset_download" are the celery tasks involved in this workflow.

  • Path Randomization: During dataset staging, the path of the staged dataset files/bundle are obfuscated through the means of a random universally unique identifier (UUID) called stage_alias. This UUID is generated to limit access to datasets through path enumeration.

    • The stage_dataset celery task downloads the dataset bundle from SDA, and extracts the dataset files and the bundle to their corresponding randomized paths.
      • The dataset's staging path follows the pattern: <stage_directory>/<dataset_stage_alias>/<dataset_name>.
      • The bundle's staging path follows the pattern: <bundle_stage_directory>/<dataset_bundle_name>.

Example:

/stage_directory/6a5b1734-98e8-4e47-ae5f-1a9e5d8d9f7c/dataset_name
+/bundle_stage_directory/bundle_name
  • Symlink Creation: Two symlinks are created to facilitate downloads.
    • <download_path>/<dataset_stage_alias>. This points to <stage_directory>/<dataset_stage_alias>/<dataset_name>. This will be path given to the users who want to download the dataset files from the Slate Scratch directly.
    • <download_path>/<dataset_bundle_name>. This points to <bundle_stage_directory>/<dataset_name>. This will be path given to the users who want to download the dataset as a bundle from the Slate Scratch directly.

The file download nginx server is configured to serve files from the <download_path> directory.

To enhance security, the access control list (ACL) of the <stage_directory>/<dataset_stage_alias> directory is carefully configured. It is limited to granting only the "execute" permission bit (--x) for the "others" group. This permission setup ensures that users with access to the Slate Scratch path cannot browse or navigate through all datasets present in the directory. Instead, only users who possess the complete path <stage_directory>/<dataset_stage_alias>/<dataset_name> are allowed to read the contents of the staged dataset.

UUID Generation

The UUIDs used in the staging paths of the dataset and the bundle are generated deterministically. They are a function of the dataset type, dataset (or bundle) name, and a salt string. This deterministic approach ensures consistency and allows authorized users to access the staged dataset/bundle when needed, while making it computationally infeasible for users to guess the path of other datasets.

By implementing these measures, the Staging Dataset functionality maintains data security and privacy, preventing unauthorized access and ensuring the integrity of the staged datasets.

4. Access Control

Access control is managed through the project membership and user roles by the API. Users of "operator" and "admin" roles can access any dataset and its files. Users with "user" can only access the datasets that are associated with projects the user is a member of. This role-based access control ensures that sensitive dataset information is only accessible to those who have been explicitly granted access rights.

To further enhance security and prevent unauthorized access to dataset files, a secure ephemeral resource bound URL is constructed. This URL contains critical components to ensure its validity and security: Components of the Secure URL:

  • File Path: The URL contains the file path relative to the download directory of the dataset file that needs to be downloaded.
  • JWT (JSON Web Token): A very short-lived JWT is included as part of the URL. The JWT payload includes the file path as one of its components.

URL Validation:

For the URL to be considered valid, the following conditions must be met:

  • JWT Validity: The JWT included in the URL must be valid, meaning it must have a valid signature and must not be expired.
  • Path Match: The file path inside the JWT payload should match the file path specified in the path section of the URL.

By enforcing these conditions, the system ensures that even if an unauthorized user somehow obtains a link to a dataset file sometime later, they cannot access it. It also makes sure that a token cannot be used to download other files even if it is unexpired and has a valid signature.

5. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the file browser view to initiate file downloads.
  • API: This node serves the user interface and API endpoints with user and dataset metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Workers / Colo Node: Staging of dataset files occurs on this node via a Celery task. The node also hosts an Nginx server with access to the staged files.
  • Signet: An OAuth server supporting client credential flow with download file scope to create secure tokens.
  • File Download Server (Nginx): A file server on the colo adjacent to data which receives requests from users to download large dataset files.
  • Secure Download API: A lightweight app with one endpoint that validates the incoming requests to the file download server

6. Downloading a Dataset File

To download a dataset file:

  • Step 1. An authorized user logs in through the UI and requests to download a dataset file.
  • Steps 2,3. The API checks if the user has access to the requested file by querying the database and constructs the download file path.
  • Step 4,5. Requests the Signet oauth server for a download token with the file path
  • Step 6. Responds to UI with the download file url and download token.
  • Step 7. UI Client constructs a URL by including the download token as a query parameter and requests the file download server.
  • Step 8. File server forwards the request to SecureDownloadAPI for validation.
  • Step 9.10. SecureDownloadAPI fetches the public JWT verification keys from the Signet Oauth server in the form of JWKS and perform URL validation as described in section 4.
  • Step 11. If it is valid, it responds to file server (Nginx) with a special header x-accel-redirect with a value of the path of the requested file.
  • Step 12. Nginx performs internal redirect and sends the requested file to the UIclient with headers invoking a browser file download.
',38)]))}const f=t(n,[["render",r]]);export{p as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.lean.js b/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.lean.js new file mode 100644 index 000000000..c22505573 --- /dev/null +++ b/docs/.vitepress/dist/assets/secure_download.md.CSwnByz7.lean.js @@ -0,0 +1 @@ +import{_ as t,c as a,o as s,ag as o}from"./chunks/framework.C9SxlbOG.js";const i="/bioloop/docs/secure-download-arch-diagram.png",p=JSON.parse('{"title":"Secure Download","description":"","frontmatter":{},"headers":[],"relativePath":"secure_download.md","filePath":"secure_download.md","lastUpdated":null}'),n={name:"secure_download.md"};function r(l,e,d,h,c,u){return s(),a("div",null,e[0]||(e[0]=[o("",38)]))}const f=t(n,[["render",r]]);export{p as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/style.CHrFrWDG.css b/docs/.vitepress/dist/assets/style.CHrFrWDG.css new file mode 100644 index 000000000..b3f189de0 --- /dev/null +++ b/docs/.vitepress/dist/assets/style.CHrFrWDG.css @@ -0,0 +1 @@ +@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-greek.BBVDIX6e.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-roman-latin.Di8DUHzh.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/bioloop/docs/assets/inter-italic-latin.C2AdPX0b.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Punctuation SC;font-weight:400;src:local("PingFang SC Regular"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:500;src:local("PingFang SC Medium"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:600;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:700;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: #3c3c43;--vp-c-text-2: #67676c;--vp-c-text-3: #929295}.dark{--vp-c-text-1: #dfdfd6;--vp-c-text-2: #98989f;--vp-c-text-3: #6a6a71}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;font-optical-sizing:auto}:root:where(:lang(zh)){--vp-font-family-base: "Punctuation SC", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:lang(es),:lang(pt){--vp-code-copy-copied-text-content: "Copiado"}:lang(fa){--vp-code-copy-copied-text-content: "کپی شد"}:lang(ko){--vp-code-copy-copied-text-content: "복사됨"}:lang(ru){--vp-code-copy-copied-text-content: "Скопировано"}:lang(zh){--vp-code-copy-copied-text-content: "已复制"}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{overflow-x:auto}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc h4{margin:24px 0 0;letter-spacing:-.01em;line-height:24px;font-size:18px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s;color:var(--vp-c-text-2)}.vp-doc blockquote>p{margin:0;font-size:16px;transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code,.vp-doc h4>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;-webkit-user-select:none;user-select:none;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(:is(.no-icon,svg a,:has(img,svg))):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-c79a1216]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-c79a1216],.VPBackdrop.fade-leave-to[data-v-c79a1216]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c79a1216]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c79a1216]{display:none}}.NotFound[data-v-d6be1790]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-d6be1790]{padding:96px 32px 168px}}.code[data-v-d6be1790]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-d6be1790]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-d6be1790]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-d6be1790]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-d6be1790]{padding-top:20px}.link[data-v-d6be1790]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-d6be1790]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-b933a997]{position:relative;z-index:1}.nested[data-v-b933a997]{padding-right:16px;padding-left:16px}.outline-link[data-v-b933a997]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-b933a997]:hover,.outline-link.active[data-v-b933a997]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-b933a997]{padding-left:13px}.VPDocAsideOutline[data-v-a5bbad30]{display:none}.VPDocAsideOutline.has-outline[data-v-a5bbad30]{display:block}.content[data-v-a5bbad30]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-a5bbad30]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-a5bbad30]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-3f215769]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3f215769]{flex-grow:1}.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3f215769] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-e98dd255]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-e98dd255]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-e257564d]{margin-top:64px}.edit-info[data-v-e257564d]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-e257564d]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-e257564d]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-e257564d]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-e257564d]{margin-right:8px}.prev-next[data-v-e257564d]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-e257564d]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-e257564d]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-e257564d]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-e257564d]{margin-left:auto;text-align:right}.desc[data-v-e257564d]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-e257564d]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-39a288b8]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-39a288b8]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-39a288b8]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-39a288b8]{display:flex;justify-content:center}.VPDoc .aside[data-v-39a288b8]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{max-width:1104px}}.container[data-v-39a288b8]{margin:0 auto;width:100%}.aside[data-v-39a288b8]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-39a288b8]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-39a288b8]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-39a288b8]::-webkit-scrollbar{display:none}.aside-curtain[data-v-39a288b8]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-39a288b8]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-39a288b8]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-39a288b8]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-39a288b8]{order:1;margin:0;min-width:640px}}.content-container[data-v-39a288b8]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-39a288b8]{max-width:688px}.VPButton[data-v-fa7799d5]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-fa7799d5]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-fa7799d5]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-fa7799d5]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-fa7799d5]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-fa7799d5]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-fa7799d5]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-fa7799d5]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-fa7799d5]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-fa7799d5]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-fa7799d5]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-fa7799d5]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-fa7799d5]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-8426fc1a]{display:none}.dark .VPImage.light[data-v-8426fc1a]{display:none}.VPHero[data-v-4f9c455b]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-4f9c455b]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-4f9c455b]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-4f9c455b]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-4f9c455b]{flex-direction:row}}.main[data-v-4f9c455b]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-4f9c455b]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-4f9c455b]{text-align:left}}@media (min-width: 960px){.main[data-v-4f9c455b]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-4f9c455b]{max-width:592px}}.heading[data-v-4f9c455b]{display:flex;flex-direction:column}.name[data-v-4f9c455b],.text[data-v-4f9c455b]{width:fit-content;max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-4f9c455b],.VPHero.has-image .text[data-v-4f9c455b]{margin:0 auto}.name[data-v-4f9c455b]{color:var(--vp-home-hero-name-color)}.clip[data-v-4f9c455b]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-4f9c455b],.text[data-v-4f9c455b]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-4f9c455b],.text[data-v-4f9c455b]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-4f9c455b],.VPHero.has-image .text[data-v-4f9c455b]{margin:0}}.tagline[data-v-4f9c455b]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-4f9c455b]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-4f9c455b]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-4f9c455b]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-4f9c455b]{margin:0}}.actions[data-v-4f9c455b]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-4f9c455b]{justify-content:center}@media (min-width: 640px){.actions[data-v-4f9c455b]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-4f9c455b]{justify-content:flex-start}}.action[data-v-4f9c455b]{flex-shrink:0;padding:6px}.image[data-v-4f9c455b]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-4f9c455b]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-4f9c455b]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-4f9c455b]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-4f9c455b]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-4f9c455b]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-4f9c455b]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-4f9c455b]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-4f9c455b]{width:320px;height:320px}}[data-v-4f9c455b] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-4f9c455b] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-4f9c455b] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-a3976bdc]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-a3976bdc]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-a3976bdc]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-a3976bdc]>.VPImage{margin-bottom:20px}.icon[data-v-a3976bdc]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-a3976bdc]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-a3976bdc]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-a3976bdc]{padding-top:8px}.link-text-value[data-v-a3976bdc]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-a3976bdc]{margin-left:6px}.VPFeatures[data-v-a6181336]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-a6181336]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-a6181336]{padding:0 64px}}.container[data-v-a6181336]{margin:0 auto;max-width:1152px}.items[data-v-a6181336]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-a6181336]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336]{width:50%}.item.grid-3[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-a6181336]{width:25%}}.container[data-v-8e2d4988]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-8e2d4988]{padding:0 48px}}@media (min-width: 960px){.container[data-v-8e2d4988]{width:100%;padding:0 64px}}.vp-doc[data-v-8e2d4988] .VPHomeSponsors,.vp-doc[data-v-8e2d4988] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-8e2d4988] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-8e2d4988] .VPHomeSponsors a,.vp-doc[data-v-8e2d4988] .VPTeamPage a{text-decoration:none}.VPHome[data-v-8b561e3d]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-8b561e3d]{margin-bottom:128px}}.VPContent[data-v-1428d186]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-1428d186]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-1428d186]{margin:0}@media (min-width: 960px){.VPContent[data-v-1428d186]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-1428d186]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-1428d186]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-e315a0ad]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-e315a0ad]{display:none}.VPFooter[data-v-e315a0ad] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-e315a0ad] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-e315a0ad]{padding:32px}}.container[data-v-e315a0ad]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-e315a0ad],.copyright[data-v-e315a0ad]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-8a42e2b4]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-8a42e2b4]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-8a42e2b4]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-8a42e2b4]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-8a42e2b4]{color:var(--vp-c-text-1)}.icon[data-v-8a42e2b4]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-8a42e2b4]{font-size:14px}.icon[data-v-8a42e2b4]{font-size:16px}}.open>.icon[data-v-8a42e2b4]{transform:rotate(90deg)}.items[data-v-8a42e2b4]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-8a42e2b4]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-8a42e2b4]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-8a42e2b4]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-8a42e2b4]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-8a42e2b4]{transition:all .2s ease-out}.flyout-leave-active[data-v-8a42e2b4]{transition:all .15s ease-in}.flyout-enter-from[data-v-8a42e2b4],.flyout-leave-to[data-v-8a42e2b4]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-a6f0e41e]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-a6f0e41e]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-a6f0e41e]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-a6f0e41e]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-a6f0e41e]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-a6f0e41e]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-a6f0e41e]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-a6f0e41e]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-a6f0e41e]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-a6f0e41e]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-a6f0e41e]{display:none}}.menu-icon[data-v-a6f0e41e]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-a6f0e41e]{padding:12px 32px 11px}}.VPSwitch[data-v-1d5665e3]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-1d5665e3]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-1d5665e3]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-1d5665e3]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-1d5665e3] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-1d5665e3] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-5337faa4]{opacity:1}.moon[data-v-5337faa4],.dark .sun[data-v-5337faa4]{opacity:0}.dark .moon[data-v-5337faa4]{opacity:1}.dark .VPSwitchAppearance[data-v-5337faa4] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-6c893767]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-6c893767]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-35975db6]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-35975db6]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-35975db6]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-35975db6]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-69e747b5]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-69e747b5]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-69e747b5]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-69e747b5]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-b98bc113]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-b98bc113] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-b98bc113] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-b98bc113] .group:last-child{padding-bottom:0}.VPMenu[data-v-b98bc113] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-b98bc113] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-b98bc113] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-b98bc113] .action{padding-left:24px}.VPFlyout[data-v-cf11d7a2]{position:relative}.VPFlyout[data-v-cf11d7a2]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-cf11d7a2]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-cf11d7a2]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-cf11d7a2]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-cf11d7a2]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-cf11d7a2]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-cf11d7a2],.button[aria-expanded=true]+.menu[data-v-cf11d7a2]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-cf11d7a2]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-cf11d7a2]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-cf11d7a2]{margin-right:0;font-size:16px}.text-icon[data-v-cf11d7a2]{margin-left:4px;font-size:14px}.icon[data-v-cf11d7a2]{font-size:20px;transition:fill .25s}.menu[data-v-cf11d7a2]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-bd121fe5]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-bd121fe5]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-bd121fe5]>svg,.VPSocialLink[data-v-bd121fe5]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-7bc22406]{display:flex;justify-content:center}.VPNavBarExtra[data-v-bb2aa2f0]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-bb2aa2f0]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-bb2aa2f0]{display:none}}.trans-title[data-v-bb2aa2f0]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-bb2aa2f0],.item.social-links[data-v-bb2aa2f0]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-bb2aa2f0]{min-width:176px}.appearance-action[data-v-bb2aa2f0]{margin-right:-2px}.social-links-list[data-v-bb2aa2f0]{margin:-4px -8px}.VPNavBarHamburger[data-v-e5dd9c1c]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-e5dd9c1c]{display:none}}.container[data-v-e5dd9c1c]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-e5dd9c1c]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-e5dd9c1c],.middle[data-v-e5dd9c1c],.bottom[data-v-e5dd9c1c]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(0)}.middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-e56f3d57]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-e56f3d57],.VPNavBarMenuLink[data-v-e56f3d57]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-dc692963]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-dc692963]{display:flex}}/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button-Key--pressed{transform:none;box-shadow:none}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-0394ad82]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-0394ad82]{display:flex;align-items:center}}.title[data-v-1168a8e4]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-1168a8e4]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-1168a8e4]{border-bottom-color:var(--vp-c-divider)}}[data-v-1168a8e4] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-88af2de4]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-88af2de4]{display:flex;align-items:center}}.title[data-v-88af2de4]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-6aa21345]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-6aa21345]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-6aa21345]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-6aa21345]:not(.home){background-color:transparent}.VPNavBar[data-v-6aa21345]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-6aa21345]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-6aa21345]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-6aa21345]{padding:0}}.container[data-v-6aa21345]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-6aa21345],.container>.content[data-v-6aa21345]{pointer-events:none}.container[data-v-6aa21345] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-6aa21345]{max-width:100%}}.title[data-v-6aa21345]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-6aa21345]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-6aa21345]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-6aa21345]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-6aa21345]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-6aa21345]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-6aa21345]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.home.top) .content-body[data-v-6aa21345]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-6aa21345]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-6aa21345]{column-gap:.5rem}}.menu+.translations[data-v-6aa21345]:before,.menu+.appearance[data-v-6aa21345]:before,.menu+.social-links[data-v-6aa21345]:before,.translations+.appearance[data-v-6aa21345]:before,.appearance+.social-links[data-v-6aa21345]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-6aa21345]:before,.translations+.appearance[data-v-6aa21345]:before{margin-right:16px}.appearance+.social-links[data-v-6aa21345]:before{margin-left:16px}.social-links[data-v-6aa21345]{margin-right:-8px}.divider[data-v-6aa21345]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-6aa21345]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-6aa21345]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-6aa21345]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-6aa21345]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-6aa21345]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-6aa21345]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-b44890b2]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-b44890b2]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-df37e6dd]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-df37e6dd]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-3e9c20e4]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-3e9c20e4]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-8133b170]{display:block}.title[data-v-8133b170]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-b9ab8c58]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-b9ab8c58]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-b9ab8c58]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-b9ab8c58]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-b9ab8c58]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-b9ab8c58]{transform:rotate(45deg)}.button[data-v-b9ab8c58]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-b9ab8c58]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-b9ab8c58]{transition:transform .25s}.group[data-v-b9ab8c58]:first-child{padding-top:0}.group+.group[data-v-b9ab8c58],.group+.item[data-v-b9ab8c58]{padding-top:4px}.VPNavScreenTranslations[data-v-858fe1a4]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-858fe1a4]{height:auto}.title[data-v-858fe1a4]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-858fe1a4]{font-size:16px}.icon.lang[data-v-858fe1a4]{margin-right:8px}.icon.chevron[data-v-858fe1a4]{margin-left:4px}.list[data-v-858fe1a4]{padding:4px 0 0 24px}.link[data-v-858fe1a4]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-f2779853]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .25s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-f2779853],.VPNavScreen.fade-leave-active[data-v-f2779853]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-f2779853],.VPNavScreen.fade-leave-active .container[data-v-f2779853]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-f2779853],.VPNavScreen.fade-leave-to[data-v-f2779853]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-f2779853],.VPNavScreen.fade-leave-to .container[data-v-f2779853]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-f2779853]{display:none}}.container[data-v-f2779853]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-f2779853],.menu+.appearance[data-v-f2779853],.translations+.appearance[data-v-f2779853]{margin-top:24px}.menu+.social-links[data-v-f2779853]{margin-top:16px}.appearance+.social-links[data-v-f2779853]{margin-top:16px}.VPNav[data-v-ae24b3ad]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-ae24b3ad]{position:fixed}}.VPSidebarItem.level-0[data-v-b3fd67f8]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-b3fd67f8]{padding-bottom:10px}.item[data-v-b3fd67f8]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-b3fd67f8]{cursor:pointer}.indicator[data-v-b3fd67f8]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-b3fd67f8],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-b3fd67f8],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-b3fd67f8],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-b3fd67f8]{background-color:var(--vp-c-brand-1)}.link[data-v-b3fd67f8]{display:flex;align-items:center;flex-grow:1}.text[data-v-b3fd67f8]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-b3fd67f8]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-b3fd67f8],.VPSidebarItem.level-2 .text[data-v-b3fd67f8],.VPSidebarItem.level-3 .text[data-v-b3fd67f8],.VPSidebarItem.level-4 .text[data-v-b3fd67f8],.VPSidebarItem.level-5 .text[data-v-b3fd67f8]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-b3fd67f8],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-b3fd67f8]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.has-active>.item>.text[data-v-b3fd67f8],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-b3fd67f8]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-b3fd67f8],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-b3fd67f8]{color:var(--vp-c-brand-1)}.caret[data-v-b3fd67f8]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-b3fd67f8]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-b3fd67f8]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-b3fd67f8]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-b3fd67f8]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-b3fd67f8],.VPSidebarItem.level-2 .items[data-v-b3fd67f8],.VPSidebarItem.level-3 .items[data-v-b3fd67f8],.VPSidebarItem.level-4 .items[data-v-b3fd67f8],.VPSidebarItem.level-5 .items[data-v-b3fd67f8]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-b3fd67f8]{display:none}.no-transition[data-v-c40bc020] .caret-icon{transition:none}.group+.group[data-v-c40bc020]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-c40bc020]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-319d5ca6]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-319d5ca6]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-319d5ca6]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-319d5ca6]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-319d5ca6]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-319d5ca6]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-319d5ca6]{outline:0}.VPSkipLink[data-v-0b0ada53]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-0b0ada53]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-0b0ada53]{top:14px;left:16px}}.Layout[data-v-5d98c3a5]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-3d121b4a]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-3d121b4a]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{margin:128px 0}}.VPHomeSponsors[data-v-3d121b4a]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-3d121b4a]{padding:0 64px}}.container[data-v-3d121b4a]{margin:0 auto;max-width:1152px}.love[data-v-3d121b4a]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-3d121b4a]{display:inline-block}.message[data-v-3d121b4a]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-3d121b4a]{padding-top:32px}.action[data-v-3d121b4a]{padding-top:40px;text-align:center}.VPTeamMembersItem[data-v-f3fa364a]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f3fa364a]{padding:32px}.VPTeamMembersItem.small .data[data-v-f3fa364a]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f3fa364a]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f3fa364a]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f3fa364a]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f3fa364a]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f3fa364a]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f3fa364a]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f3fa364a]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f3fa364a]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f3fa364a]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f3fa364a]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f3fa364a]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f3fa364a]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f3fa364a]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f3fa364a]{text-align:center}.avatar[data-v-f3fa364a]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f3fa364a]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f3fa364a]{margin:0;font-weight:600}.affiliation[data-v-f3fa364a]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f3fa364a]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f3fa364a]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f3fa364a]{margin:0 auto}.desc[data-v-f3fa364a] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f3fa364a]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f3fa364a]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-f3fa364a]:hover,.sp .sp-link.link[data-v-f3fa364a]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f3fa364a]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4]{max-width:876px}.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4]{max-width:760px}.container[data-v-6cb0dbc4]{display:grid;gap:24px;margin:0 auto;max-width:1152px}.VPTeamPage[data-v-7c57f839]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-7c57f839]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-7c57f839-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-7c57f839-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers+.VPTeamPageSection[data-v-7c57f839-s]{margin-top:96px}}.VPTeamMembers[data-v-7c57f839-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-7c57f839-s]{padding:0 64px}}.VPTeamPageSection[data-v-b1a88750]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1a88750]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1a88750]{padding:0 64px}}.title[data-v-b1a88750]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1a88750]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1a88750]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-b1a88750]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-b1a88750]{padding-top:40px}.VPTeamPageTitle[data-v-bf2cbdac]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:80px 64px 48px}}.title[data-v-bf2cbdac]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-bf2cbdac]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-bf2cbdac]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-bf2cbdac]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.center{display:block;margin-left:auto;margin-right:auto}.VPLocalSearchBox[data-v-ce626c7c]{position:fixed;z-index:100;top:0;right:0;bottom:0;left:0;display:flex}.backdrop[data-v-ce626c7c]{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--vp-backdrop-bg-color);transition:opacity .5s}.shell[data-v-ce626c7c]{position:relative;padding:12px;margin:64px auto;display:flex;flex-direction:column;gap:16px;background:var(--vp-local-search-bg);width:min(100vw - 60px,900px);height:min-content;max-height:min(100vh - 128px,900px);border-radius:6px}@media (max-width: 767px){.shell[data-v-ce626c7c]{margin:0;width:100vw;height:100vh;max-height:none;border-radius:0}}.search-bar[data-v-ce626c7c]{border:1px solid var(--vp-c-divider);border-radius:4px;display:flex;align-items:center;padding:0 12px;cursor:text}@media (max-width: 767px){.search-bar[data-v-ce626c7c]{padding:0 8px}}.search-bar[data-v-ce626c7c]:focus-within{border-color:var(--vp-c-brand-1)}.local-search-icon[data-v-ce626c7c]{display:block;font-size:18px}.navigate-icon[data-v-ce626c7c]{display:block;font-size:14px}.search-icon[data-v-ce626c7c]{margin:8px}@media (max-width: 767px){.search-icon[data-v-ce626c7c]{display:none}}.search-input[data-v-ce626c7c]{padding:6px 12px;font-size:inherit;width:100%}@media (max-width: 767px){.search-input[data-v-ce626c7c]{padding:6px 4px}}.search-actions[data-v-ce626c7c]{display:flex;gap:4px}@media (any-pointer: coarse){.search-actions[data-v-ce626c7c]{gap:8px}}@media (min-width: 769px){.search-actions.before[data-v-ce626c7c]{display:none}}.search-actions button[data-v-ce626c7c]{padding:8px}.search-actions button[data-v-ce626c7c]:not([disabled]):hover,.toggle-layout-button.detailed-list[data-v-ce626c7c]{color:var(--vp-c-brand-1)}.search-actions button.clear-button[data-v-ce626c7c]:disabled{opacity:.37}.search-keyboard-shortcuts[data-v-ce626c7c]{font-size:.8rem;opacity:75%;display:flex;flex-wrap:wrap;gap:16px;line-height:14px}.search-keyboard-shortcuts span[data-v-ce626c7c]{display:flex;align-items:center;gap:4px}@media (max-width: 767px){.search-keyboard-shortcuts[data-v-ce626c7c]{display:none}}.search-keyboard-shortcuts kbd[data-v-ce626c7c]{background:#8080801a;border-radius:4px;padding:3px 6px;min-width:24px;display:inline-block;text-align:center;vertical-align:middle;border:1px solid rgba(128,128,128,.15);box-shadow:0 2px 2px #0000001a}.results[data-v-ce626c7c]{display:flex;flex-direction:column;gap:6px;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}.result[data-v-ce626c7c]{display:flex;align-items:center;gap:8px;border-radius:4px;transition:none;line-height:1rem;border:solid 2px var(--vp-local-search-result-border);outline:none}.result>div[data-v-ce626c7c]{margin:12px;width:100%;overflow:hidden}@media (max-width: 767px){.result>div[data-v-ce626c7c]{margin:8px}}.titles[data-v-ce626c7c]{display:flex;flex-wrap:wrap;gap:4px;position:relative;z-index:1001;padding:2px 0}.title[data-v-ce626c7c]{display:flex;align-items:center;gap:4px}.title.main[data-v-ce626c7c]{font-weight:500}.title-icon[data-v-ce626c7c]{opacity:.5;font-weight:500;color:var(--vp-c-brand-1)}.title svg[data-v-ce626c7c]{opacity:.5}.result.selected[data-v-ce626c7c]{--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);border-color:var(--vp-local-search-result-selected-border)}.excerpt-wrapper[data-v-ce626c7c]{position:relative}.excerpt[data-v-ce626c7c]{opacity:50%;pointer-events:none;max-height:140px;overflow:hidden;position:relative;margin-top:4px}.result.selected .excerpt[data-v-ce626c7c]{opacity:1}.excerpt[data-v-ce626c7c] *{font-size:.8rem!important;line-height:130%!important}.titles[data-v-ce626c7c] mark,.excerpt[data-v-ce626c7c] mark{background-color:var(--vp-local-search-highlight-bg);color:var(--vp-local-search-highlight-text);border-radius:2px;padding:0 2px}.excerpt[data-v-ce626c7c] .vp-code-group .tabs{display:none}.excerpt[data-v-ce626c7c] .vp-code-group div[class*=language-]{border-radius:8px!important}.excerpt-gradient-bottom[data-v-ce626c7c]{position:absolute;bottom:-1px;left:0;width:100%;height:8px;background:linear-gradient(transparent,var(--vp-local-search-result-bg));z-index:1000}.excerpt-gradient-top[data-v-ce626c7c]{position:absolute;top:-1px;left:0;width:100%;height:8px;background:linear-gradient(var(--vp-local-search-result-bg),transparent);z-index:1000}.result.selected .titles[data-v-ce626c7c],.result.selected .title-icon[data-v-ce626c7c]{color:var(--vp-c-brand-1)!important}.no-results[data-v-ce626c7c]{font-size:.9rem;text-align:center;padding:12px}svg[data-v-ce626c7c]{flex:none} diff --git a/docs/.vitepress/dist/assets/style.bc2Cu_Y_.css b/docs/.vitepress/dist/assets/style.bc2Cu_Y_.css deleted file mode 100644 index a9ada6718..000000000 --- a/docs/.vitepress/dist/assets/style.bc2Cu_Y_.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-cyrillic.jIZ9REo5.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-cyrillic-ext.8T9wMG5w.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-greek.Cb5wWeGA.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-greek-ext.9JiNzaSO.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-latin.bvIUbFQP.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-latin-ext.GZWE-KO4.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:normal;font-named-instance:"Regular";src:url(/bioloop/docs/assets/inter-roman-vietnamese.paY3CzEB.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-cyrillic.-nLMcIwj.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-cyrillic-ext.OVycGSDq.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-greek.PSfer2Kc.woff2) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-greek-ext.hznxWNZO.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-latin.27E69YJn.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-latin-ext.RnFly65-.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:italic;font-named-instance:"Italic";src:url(/bioloop/docs/assets/inter-italic-vietnamese.xzQHe1q1.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Chinese Quotes;src:local("PingFang SC Regular"),local("PingFang SC"),local("SimHei"),local("Source Han Sans SC");unicode-range:U+2018,U+2019,U+201C,U+201D}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: rgba(60, 60, 67);--vp-c-text-2: rgba(60, 60, 67, .78);--vp-c-text-3: rgba(60, 60, 67, .56)}.dark{--vp-c-text-1: rgba(255, 255, 245, .86);--vp-c-text-2: rgba(235, 235, 245, .6);--vp-c-text-3: rgba(235, 235, 245, .38)}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E")}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{display:inline-block;margin:auto 2px -2px}mjx-container>svg{margin:auto}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s}.vp-doc blockquote>p{margin:0;font-size:16px;color:var(--vp-c-text-2);transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin-bottom:4px;text-align:center;letter-spacing:1px;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-c79a1216]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-c79a1216],.VPBackdrop.fade-leave-to[data-v-c79a1216]{opacity:0}.VPBackdrop.fade-leave-active[data-v-c79a1216]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-c79a1216]{display:none}}.NotFound[data-v-f87ff6e4]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-f87ff6e4]{padding:96px 32px 168px}}.code[data-v-f87ff6e4]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-f87ff6e4]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-f87ff6e4]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-f87ff6e4]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-f87ff6e4]{padding-top:20px}.link[data-v-f87ff6e4]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-f87ff6e4]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-b933a997]{position:relative;z-index:1}.nested[data-v-b933a997]{padding-right:16px;padding-left:16px}.outline-link[data-v-b933a997]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-b933a997]:hover,.outline-link.active[data-v-b933a997]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-b933a997]{padding-left:13px}.VPDocAsideOutline[data-v-935f8a84]{display:none}.VPDocAsideOutline.has-outline[data-v-935f8a84]{display:block}.content[data-v-935f8a84]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-935f8a84]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-935f8a84]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-3f215769]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3f215769]{flex-grow:1}.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3f215769] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3f215769] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-7e05ebdb]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-7e05ebdb]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-48f9bb55]{margin-top:64px}.edit-info[data-v-48f9bb55]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-48f9bb55]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-48f9bb55]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-48f9bb55]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-48f9bb55]{margin-right:8px;width:14px;height:14px;fill:currentColor}.prev-next[data-v-48f9bb55]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-48f9bb55]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-48f9bb55]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-48f9bb55]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-48f9bb55]{margin-left:auto;text-align:right}.desc[data-v-48f9bb55]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-48f9bb55]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-39a288b8]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-39a288b8]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-39a288b8]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-39a288b8]{display:flex;justify-content:center}.VPDoc .aside[data-v-39a288b8]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-39a288b8]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-39a288b8]{max-width:1104px}}.container[data-v-39a288b8]{margin:0 auto;width:100%}.aside[data-v-39a288b8]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-39a288b8]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-39a288b8]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-39a288b8]::-webkit-scrollbar{display:none}.aside-curtain[data-v-39a288b8]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-39a288b8]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-39a288b8]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-39a288b8]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-39a288b8]{order:1;margin:0;min-width:640px}}.content-container[data-v-39a288b8]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-39a288b8]{max-width:688px}.VPButton[data-v-cad61b99]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-cad61b99]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-cad61b99]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-cad61b99]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-cad61b99]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-cad61b99]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-cad61b99]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-cad61b99]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-cad61b99]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-cad61b99]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-cad61b99]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-cad61b99]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-cad61b99]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-8426fc1a]{display:none}.dark .VPImage.light[data-v-8426fc1a]{display:none}.VPHero[data-v-303bb580]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-303bb580]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-303bb580]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-303bb580]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-303bb580]{flex-direction:row}}.main[data-v-303bb580]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-303bb580]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-303bb580]{text-align:left}}@media (min-width: 960px){.main[data-v-303bb580]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-303bb580]{max-width:592px}}.name[data-v-303bb580],.text[data-v-303bb580]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-303bb580],.VPHero.has-image .text[data-v-303bb580]{margin:0 auto}.name[data-v-303bb580]{color:var(--vp-home-hero-name-color)}.clip[data-v-303bb580]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-303bb580],.text[data-v-303bb580]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-303bb580],.text[data-v-303bb580]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-303bb580],.VPHero.has-image .text[data-v-303bb580]{margin:0}}.tagline[data-v-303bb580]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-303bb580]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-303bb580]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-303bb580]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-303bb580]{margin:0}}.actions[data-v-303bb580]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-303bb580]{justify-content:center}@media (min-width: 640px){.actions[data-v-303bb580]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-303bb580]{justify-content:flex-start}}.action[data-v-303bb580]{flex-shrink:0;padding:6px}.image[data-v-303bb580]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-303bb580]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-303bb580]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-303bb580]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-303bb580]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-303bb580]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-303bb580]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-303bb580]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-303bb580]{width:320px;height:320px}}[data-v-303bb580] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-303bb580] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-303bb580] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-33204567]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-33204567]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-33204567]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-33204567]>.VPImage{margin-bottom:20px}.icon[data-v-33204567]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-33204567]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-33204567]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-33204567]{padding-top:8px}.link-text-value[data-v-33204567]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-33204567]{display:inline-block;margin-left:6px;width:14px;height:14px;fill:currentColor}.VPFeatures[data-v-a6181336]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-a6181336]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-a6181336]{padding:0 64px}}.container[data-v-a6181336]{margin:0 auto;max-width:1152px}.items[data-v-a6181336]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-a6181336]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-a6181336],.item.grid-4[data-v-a6181336]{width:50%}.item.grid-3[data-v-a6181336],.item.grid-6[data-v-a6181336]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-a6181336]{width:25%}}.VPHome[data-v-c71b6826]{padding-bottom:96px}.VPHome[data-v-c71b6826] .VPHomeSponsors{margin-top:112px;margin-bottom:-128px}@media (min-width: 768px){.VPHome[data-v-c71b6826]{padding-bottom:128px}}.VPContent[data-v-1428d186]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-1428d186]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-1428d186]{margin:0}@media (min-width: 960px){.VPContent[data-v-1428d186]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-1428d186]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-1428d186]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-e315a0ad]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-e315a0ad]{display:none}.VPFooter[data-v-e315a0ad] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-e315a0ad] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-e315a0ad]{padding:32px}}.container[data-v-e315a0ad]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-e315a0ad],.copyright[data-v-e315a0ad]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-af18c0d5]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-af18c0d5]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-af18c0d5]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-af18c0d5]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-af18c0d5]{color:var(--vp-c-text-1)}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-af18c0d5]{font-size:14px}}.icon[data-v-af18c0d5]{display:inline-block;vertical-align:middle;margin-left:2px;width:14px;height:14px;fill:currentColor}.open>.icon[data-v-af18c0d5]{transform:rotate(90deg)}.items[data-v-af18c0d5]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-af18c0d5]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-af18c0d5]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-af18c0d5]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-af18c0d5]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-af18c0d5]{transition:all .2s ease-out}.flyout-leave-active[data-v-af18c0d5]{transition:all .15s ease-in}.flyout-enter-from[data-v-af18c0d5],.flyout-leave-to[data-v-af18c0d5]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-0282ae07]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-0282ae07]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-0282ae07]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-0282ae07]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-0282ae07]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-0282ae07]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-0282ae07]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-0282ae07]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-0282ae07]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-0282ae07]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-0282ae07]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-0282ae07]{display:none}}.menu-icon[data-v-0282ae07]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPOutlineDropdown[data-v-0282ae07]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-0282ae07]{padding:12px 32px 11px}}.VPSwitch[data-v-b1685198]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-b1685198]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-b1685198]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-b1685198]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-b1685198] svg{position:absolute;top:3px;left:3px;width:12px;height:12px;fill:var(--vp-c-text-2)}.dark .icon[data-v-b1685198] svg{fill:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-1736f215]{opacity:1}.moon[data-v-1736f215],.dark .sun[data-v-1736f215]{opacity:0}.dark .moon[data-v-1736f215]{opacity:1}.dark .VPSwitchAppearance[data-v-1736f215] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-e6aabb21]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-e6aabb21]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-43f1e123]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-43f1e123]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-43f1e123]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-43f1e123]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-69e747b5]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-69e747b5]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-69e747b5]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-69e747b5]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-e7ea1737]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-e7ea1737] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-e7ea1737] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-e7ea1737] .group:last-child{padding-bottom:0}.VPMenu[data-v-e7ea1737] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-e7ea1737] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-e7ea1737] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-e7ea1737] .action{padding-left:24px}.VPFlyout[data-v-9c007e85]{position:relative}.VPFlyout[data-v-9c007e85]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-9c007e85]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-9c007e85]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-9c007e85]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-9c007e85]{color:var(--vp-c-brand-2)}.VPFlyout:hover .menu[data-v-9c007e85],.button[aria-expanded=true]+.menu[data-v-9c007e85]{opacity:1;visibility:visible;transform:translateY(0)}.button[aria-expanded=false]+.menu[data-v-9c007e85]{opacity:0;visibility:hidden;transform:translateY(0)}.button[data-v-9c007e85]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-9c007e85]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-9c007e85]{margin-right:0;width:16px;height:16px;fill:currentColor}.text-icon[data-v-9c007e85]{margin-left:4px;width:14px;height:14px;fill:currentColor}.icon[data-v-9c007e85]{width:20px;height:20px;fill:currentColor;transition:fill .25s}.menu[data-v-9c007e85]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-f80f8133]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-f80f8133]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-f80f8133]>svg{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-7bc22406]{display:flex;justify-content:center}.VPNavBarExtra[data-v-d0bd9dde]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-d0bd9dde]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-d0bd9dde]{display:none}}.trans-title[data-v-d0bd9dde]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-d0bd9dde],.item.social-links[data-v-d0bd9dde]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-d0bd9dde]{min-width:176px}.appearance-action[data-v-d0bd9dde]{margin-right:-2px}.social-links-list[data-v-d0bd9dde]{margin:-4px -8px}.VPNavBarHamburger[data-v-e5dd9c1c]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-e5dd9c1c]{display:none}}.container[data-v-e5dd9c1c]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-e5dd9c1c]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c],.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-e5dd9c1c],.middle[data-v-e5dd9c1c],.bottom[data-v-e5dd9c1c]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-e5dd9c1c]{top:0;left:0;transform:translate(0)}.middle[data-v-e5dd9c1c]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-e5dd9c1c]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-42ef59de]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-42ef59de],.VPNavBarMenuLink[data-v-42ef59de]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-7f418b0f]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-7f418b0f]{display:flex}}/*! @docsearch/css 3.5.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-0394ad82]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-0394ad82]{display:flex;align-items:center}}.title[data-v-ab179fa1]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-ab179fa1]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-ab179fa1]{border-bottom-color:var(--vp-c-divider)}}[data-v-ab179fa1] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-74abcbb9]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-74abcbb9]{display:flex;align-items:center}}.title[data-v-74abcbb9]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-19c990f1]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .5s}.VPNavBar.has-local-nav[data-v-19c990f1]{background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar.has-local-nav[data-v-19c990f1]{background-color:transparent}.VPNavBar[data-v-19c990f1]:not(.has-sidebar):not(.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-19c990f1]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-19c990f1]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-19c990f1]{padding:0}}.container[data-v-19c990f1]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-19c990f1],.container>.content[data-v-19c990f1]{pointer-events:none}.container[data-v-19c990f1] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-19c990f1]{max-width:100%}}.title[data-v-19c990f1]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-19c990f1]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-19c990f1]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-19c990f1]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-19c990f1]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-19c990f1]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-19c990f1]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.top) .content-body[data-v-19c990f1]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.top) .content-body[data-v-19c990f1]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-19c990f1]{column-gap:.5rem}}.menu+.translations[data-v-19c990f1]:before,.menu+.appearance[data-v-19c990f1]:before,.menu+.social-links[data-v-19c990f1]:before,.translations+.appearance[data-v-19c990f1]:before,.appearance+.social-links[data-v-19c990f1]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-19c990f1]:before,.translations+.appearance[data-v-19c990f1]:before{margin-right:16px}.appearance+.social-links[data-v-19c990f1]:before{margin-left:16px}.social-links[data-v-19c990f1]{margin-right:-8px}.divider[data-v-19c990f1]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-19c990f1]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-19c990f1]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-19c990f1]{width:100%;height:1px;transition:background-color .5s}.VPNavBar.has-local-nav .divider-line[data-v-19c990f1]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.top) .divider-line[data-v-19c990f1]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.top) .divider[data-v-19c990f1]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-2d7af913]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-2d7af913]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-05f27b2a]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-05f27b2a]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-19976ae1]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-19976ae1]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-8133b170]{display:block}.title[data-v-8133b170]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-65ef89ca]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-65ef89ca]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-65ef89ca]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-65ef89ca]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-65ef89ca]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-65ef89ca]{transform:rotate(45deg)}.button[data-v-65ef89ca]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-65ef89ca]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-65ef89ca]{width:14px;height:14px;fill:var(--vp-c-text-2);transition:fill .5s,transform .25s}.group[data-v-65ef89ca]:first-child{padding-top:0}.group+.group[data-v-65ef89ca],.group+.item[data-v-65ef89ca]{padding-top:4px}.VPNavScreenTranslations[data-v-d72aa483]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-d72aa483]{height:auto}.title[data-v-d72aa483]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-d72aa483]{width:16px;height:16px;fill:currentColor}.icon.lang[data-v-d72aa483]{margin-right:8px}.icon.chevron[data-v-d72aa483]{margin-left:4px}.list[data-v-d72aa483]{padding:4px 0 0 24px}.link[data-v-d72aa483]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-cc5739dd]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .5s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-cc5739dd],.VPNavScreen.fade-leave-active[data-v-cc5739dd]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-cc5739dd],.VPNavScreen.fade-leave-active .container[data-v-cc5739dd]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-cc5739dd],.VPNavScreen.fade-leave-to[data-v-cc5739dd]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-cc5739dd],.VPNavScreen.fade-leave-to .container[data-v-cc5739dd]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-cc5739dd]{display:none}}.container[data-v-cc5739dd]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-cc5739dd],.menu+.appearance[data-v-cc5739dd],.translations+.appearance[data-v-cc5739dd]{margin-top:24px}.menu+.social-links[data-v-cc5739dd]{margin-top:16px}.appearance+.social-links[data-v-cc5739dd]{margin-top:16px}.VPNav[data-v-ae24b3ad]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-ae24b3ad]{position:fixed}}.VPSidebarItem.level-0[data-v-e31bd47b]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-e31bd47b]{padding-bottom:10px}.item[data-v-e31bd47b]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-e31bd47b]{cursor:pointer}.indicator[data-v-e31bd47b]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-e31bd47b],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-e31bd47b]{background-color:var(--vp-c-brand-1)}.link[data-v-e31bd47b]{display:flex;align-items:center;flex-grow:1}.text[data-v-e31bd47b]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-e31bd47b]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-e31bd47b],.VPSidebarItem.level-2 .text[data-v-e31bd47b],.VPSidebarItem.level-3 .text[data-v-e31bd47b],.VPSidebarItem.level-4 .text[data-v-e31bd47b],.VPSidebarItem.level-5 .text[data-v-e31bd47b]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-e31bd47b],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-e31bd47b]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-1.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-2.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-3.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-4.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-5.has-active>.item>.text[data-v-e31bd47b],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-e31bd47b],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-e31bd47b]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-e31bd47b],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-e31bd47b]{color:var(--vp-c-brand-1)}.caret[data-v-e31bd47b]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-e31bd47b]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-e31bd47b]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-e31bd47b]{width:18px;height:18px;fill:currentColor;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-e31bd47b]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-e31bd47b],.VPSidebarItem.level-2 .items[data-v-e31bd47b],.VPSidebarItem.level-3 .items[data-v-e31bd47b],.VPSidebarItem.level-4 .items[data-v-e31bd47b],.VPSidebarItem.level-5 .items[data-v-e31bd47b]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-e31bd47b]{display:none}.VPSidebar[data-v-575e6a36]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-575e6a36]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-575e6a36]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-575e6a36]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-575e6a36]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-575e6a36]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-575e6a36]{outline:0}.group+.group[data-v-575e6a36]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-575e6a36]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSkipLink[data-v-0f60ec36]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-0f60ec36]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-0f60ec36]{top:14px;left:16px}}.Layout[data-v-5d98c3a5]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-96bd69d5]{border-top:1px solid var(--vp-c-gutter);padding:88px 24px 96px;background-color:var(--vp-c-bg)}.container[data-v-96bd69d5]{margin:0 auto;max-width:1152px}.love[data-v-96bd69d5]{margin:0 auto;width:28px;height:28px;color:var(--vp-c-text-3)}.icon[data-v-96bd69d5]{width:28px;height:28px;fill:currentColor}.message[data-v-96bd69d5]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-96bd69d5]{padding-top:32px}.action[data-v-96bd69d5]{padding-top:40px;text-align:center}.VPTeamPage[data-v-10b00018]{padding-bottom:96px}@media (min-width: 768px){.VPTeamPage[data-v-10b00018]{padding-bottom:128px}}.VPTeamPageSection+.VPTeamPageSection[data-v-10b00018-s],.VPTeamMembers+.VPTeamPageSection[data-v-10b00018-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-10b00018-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-10b00018-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-10b00018-s],.VPTeamMembers+.VPTeamPageSection[data-v-10b00018-s]{margin-top:96px}}.VPTeamMembers[data-v-10b00018-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-10b00018-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-10b00018-s]{padding:0 64px}}.VPTeamPageTitle[data-v-bf2cbdac]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-bf2cbdac]{padding:80px 64px 48px}}.title[data-v-bf2cbdac]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-bf2cbdac]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-bf2cbdac]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-bf2cbdac]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-b1a88750]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-b1a88750]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-b1a88750]{padding:0 64px}}.title[data-v-b1a88750]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-b1a88750]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-b1a88750]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-b1a88750]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-b1a88750]{padding-top:40px}.VPTeamMembersItem[data-v-0d3d0d4d]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-0d3d0d4d]{padding:32px}.VPTeamMembersItem.small .data[data-v-0d3d0d4d]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-0d3d0d4d]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-0d3d0d4d]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-0d3d0d4d]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-0d3d0d4d]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-0d3d0d4d]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-0d3d0d4d]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-0d3d0d4d]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-0d3d0d4d]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-0d3d0d4d]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-0d3d0d4d]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-0d3d0d4d]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-0d3d0d4d]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-0d3d0d4d]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-0d3d0d4d]{text-align:center}.avatar[data-v-0d3d0d4d]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-0d3d0d4d]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-0d3d0d4d]{margin:0;font-weight:600}.affiliation[data-v-0d3d0d4d]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-0d3d0d4d]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-0d3d0d4d]:hover{color:var(--vp-c-brand-1)}.desc[data-v-0d3d0d4d]{margin:0 auto}.desc[data-v-0d3d0d4d] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-0d3d0d4d]{display:flex;justify-content:center;height:56px}.sp-link[data-v-0d3d0d4d]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-0d3d0d4d]:hover,.sp .sp-link.link[data-v-0d3d0d4d]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-0d3d0d4d]{margin-right:8px;width:16px;height:16px;fill:currentColor}.VPTeamMembers.small .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4]{max-width:876px}.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-6cb0dbc4]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4]{max-width:760px}.container[data-v-6cb0dbc4]{display:grid;gap:24px;margin:0 auto;max-width:1152px}.center{display:block;margin-left:auto;margin-right:auto} diff --git a/docs/.vitepress/dist/assets/template.md.1ENxh8wO.js b/docs/.vitepress/dist/assets/template.md.1ENxh8wO.js deleted file mode 100644 index bc0db7e27..000000000 --- a/docs/.vitepress/dist/assets/template.md.1ENxh8wO.js +++ /dev/null @@ -1,43 +0,0 @@ -import{_ as s,c as i,o as a,V as e}from"./chunks/framework.MXVb71fM.js";const u=JSON.parse('{"title":"Create a repository","description":"","frontmatter":{},"headers":[],"relativePath":"template.md","filePath":"template.md"}'),t={name:"template.md"},n=e(`

Create a repository

Fork this repo IUSCA/<app_name> (only the org owners can do this, ask Charles.)

Turn on issues in the new repo (only repo owners can do this, ask Charles.)

Clone repository

bash
git clone <url>
-cd <project>

Add bioloop as remote

bash
git remote add bioloop git@github.com:IUSCA/bioloop.git
-
-# to merge updates from bioloop
-# git fetch bioloop
-# git merge bioloop/main

Replace the name "bioloop" with the new project name (<app_name>) in these files:

  • docker-compose.yml and docker-compose-prod.yml: Change "name"
  • ui/src/config.js - Change "appTitle"
  • api/config/default.json and api/config/production.json: Change "app_id", "auth.jwt.iss"
  • workers/workers/config/common.py: Change "app_id" and "service_user"
  • workers/workers/config/production.py and workers/workers/scripts/start_worker.sh: Change "app_id" and "base_url"
  • workers/ecosystem.config.js (line 7): change celery hostname and queues values
  • README.md and workers/README.md: replace the references to bioloop with <app_name>
  • Update content in ui/src/pages/about.vue
  • Create custom logo.svg

Steps to setup API and run natively on development machine (not using docker)

  • Create .env file
  • Generate token signing key pair
  • Install dependencies
  • Generate API Doc
bash
cd api/
-cp .env.example .env
-
-cd keys
-./genkeys.sh
-cd ..
-
-npm install && npm install --save-dev
-npm run swagger

This step is required only if you are working with workflows, otherwise you can set WORKFLOW_AUTH_TOKEN to any value and API calls to Rhythm API will fail but the App will still run.

Generate an access token to connect to the Rhythm API.

  • Go to Rhythm API instance (local or deployed)- cd <rhythm_api>
  • If rhythm api is running locally: python -m rhythm_api.scripts.issue_token --sub <app-id>
  • If rhythm api is running in docker: sudo docker compose -f "docker-compose-prod.yml" exec api python -m rhythm_api.scripts.issue_token --sub <app-id>

Make these changes to the api/.env file:

bash
NODE_ENV=default
-WORKFLOW_AUTH_TOKEN=<token>
-DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"
  • Initialize database
  • Set up schema
  • Populate with dummy data
bash
docker-compose up -d postgres
-cd api/
-npx prisma db push
-npx prisma db seed

Start the server: npm run start

Steps to setup UI and run natively on development machine (not using docker)

  • Create .env file
  • Create self-signed certificate for https://localhost
  • install dependencies
bash
cd ui/
-cp .env.example .env
-
-mkdir .cert
-openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
-
-npm install && npm install --save-dev

Make these changes to ui/.env file:

  • change the hostname in VITE_API_REDIRECT_URL to localhost
bash
VITE_API_REDIRECT_URL=http://localhost:3030

Start the vite server: npm run dev

Set Up Workers locally

Running workers on your dev machine has limitations:

  • Cannot work with SDA - cannot install hsi on dev machine.
  • Difficult to test with large files (~100GB)
  • Workers run external commands - tar, fastqc, multiqc whose interface and behavior may change between OS platforms.
  • Working with mounted file systems (Slate Scratch, and others) has its own quirks which you will not encounter on your dev machine.

Steps:

  • Install miniconda

  • Create a virtual environment: conda create -n colo python=3.9

    • The production servers colo23, colo25 have python version 3.9.8 installed (as of June 2023). If the default python version in the production servers change, update it in your development machine too.
  • Activate it: conda activate colo

  • Install poetry - pip install -U poetry

  • Install dependencies - poetry install

    • Poetry will detect it is running in a virtual environment and won't create another/
  • Create .env

bash
cd workers
-cp .env.example .env
  • Generate an auth token to access the app api and add it to .env against AUTH_TOKEN.
bash
cd api/
-node src/scripts/issue_token.js svc_tasks
  • Workers connect to the mongodb and rabbitmq of a Rhythm API instance. You can either setup a Rhythm API instance locally or connect to core-dev1.sca.iu.edu using Group VPN (This option is not recommended as it is used for production now. We plan to use core.sca.iu.edu for production in the future.)

  • Update paths in config for local development: TODO

  • Stat celery:

bash
cd workers
-workers/scripts/start_celery.sh

Setup a Test Instance of Workers in colo nodes

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

image

  • start postgres locally using docker
bash
cd <app_name>
-docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
-docker-compose up queue mongo -d
-poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
-npm run start
bash
cd <app_name>/ui
-npm run dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \\
-  -A \\
-  -R 3130:localhost:3030 \\
-  -R 28017:localhost:27017 \\
-  -R 5772:localhost:5672 \\
-  bioloopuser@colo23.carbonate.uits.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
-colo23> git checkout dev
-colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
-colo23> poetry install
-colo23> poetry shell
-colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1
`,54),l=[n];function p(h,o,k,r,d,c){return a(),i("div",null,l)}const F=s(t,[["render",p]]);export{u as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/template.md.1ENxh8wO.lean.js b/docs/.vitepress/dist/assets/template.md.1ENxh8wO.lean.js deleted file mode 100644 index d4047249d..000000000 --- a/docs/.vitepress/dist/assets/template.md.1ENxh8wO.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,c as i,o as a,V as e}from"./chunks/framework.MXVb71fM.js";const u=JSON.parse('{"title":"Create a repository","description":"","frontmatter":{},"headers":[],"relativePath":"template.md","filePath":"template.md"}'),t={name:"template.md"},n=e("",54),l=[n];function p(h,o,k,r,d,c){return a(),i("div",null,l)}const F=s(t,[["render",p]]);export{u as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.js b/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.js new file mode 100644 index 000000000..65a5320cf --- /dev/null +++ b/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.js @@ -0,0 +1,6 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Project Template","description":"","frontmatter":{"title":"Project Template"},"headers":[],"relativePath":"template.md","filePath":"template.md","lastUpdated":null}'),o={name:"template.md"};function n(p,s,l,r,h,d){return e(),a("div",null,s[0]||(s[0]=[t(`

Create a repository

Fork this repo IUSCA/<app_name> (only the org owners can do this, ask Charles.)

Turn on issues in the new repo (only repo owners can do this, ask Charles.)

Clone repository

bash
git clone <url>
+cd <project>

Add bioloop as remote

bash
git remote add bioloop git@github.com:IUSCA/bioloop.git
+
+# to merge updates from bioloop
+# git fetch bioloop
+# git merge bioloop/main

Replace the name "bioloop" with the new project name (<app_name>) in these files:

  • docker-compose.yml and docker-compose-prod.yml: Change "name"
  • ui/src/config.js - Change "appTitle"
  • api/config/default.json and api/config/production.json: Change "app_id", "auth.jwt.iss"
  • workers/workers/config/common.py: Change "app_id" and "service_user"
  • workers/workers/config/production.py and workers/workers/scripts/start_worker.sh: Change "app_id" and "base_url"
  • workers/ecosystem.config.js (line 7): change celery hostname and queues values
  • README.md and workers/README.md: replace the references to bioloop with <app_name>
  • Update content in ui/src/pages/about.vue
  • Create custom logo.svg
`,9)]))}const g=i(o,[["render",n]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.lean.js b/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.lean.js new file mode 100644 index 000000000..eedc24ac6 --- /dev/null +++ b/docs/.vitepress/dist/assets/template.md.ByWSB8Nr.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Project Template","description":"","frontmatter":{"title":"Project Template"},"headers":[],"relativePath":"template.md","filePath":"template.md","lastUpdated":null}'),o={name:"template.md"};function n(p,s,l,r,h,d){return e(),a("div",null,s[0]||(s[0]=[t("",9)]))}const g=i(o,[["render",n]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.js b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.js new file mode 100644 index 000000000..975415a5b --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.js @@ -0,0 +1 @@ +import{_ as o,c as t,o as a,ag as i}from"./chunks/framework.C9SxlbOG.js";const r="/bioloop/docs/ui/assets/auth-overview-2.png",n="/bioloop/docs/ui/assets/app-load.png",s="/bioloop/docs/ui/assets/navigation-guard-logic.png",h="/bioloop/docs/ui/assets/login-flow-before-cas.png",l="/bioloop/docs/ui/assets/login-flow-after-cas-return.png",d="/bioloop/docs/ui/assets/logged-in-flow.png",v=JSON.parse('{"title":"Auth Explained","description":"","frontmatter":{"title":"Auth Explained"},"headers":[],"relativePath":"ui/auth_explained.md","filePath":"ui/auth_explained.md","lastUpdated":null}'),c={name:"ui/auth_explained.md"};function u(p,e,f,g,m,w){return a(),t("div",null,e[0]||(e[0]=[i('

Auth Explained

Objectives for Auth module

  1. Enable users to authenticate with IU CAS, which will create a session.
  2. After logging in, redirect users to the URL they originally requested.
  3. Allow users to revisit the app without having to log in again within a certain period of time.
  4. If the session expires between visits, the user will be required to authenticate again.
  5. Ensure that the session does not expire as long as the user remains active on the app.

Overview of Auth Flow

refer to https://kb.iu.edu/d/bfpq

Vue App Code Execution Order

Route Navigation Guard Logic

Login Code flow - Before IU Login

Login Code flow - After IU Login

Code flow for a Revisiting (logged in) User

Code flow for token refresh

On login / initialize, the auth store creates a timer that will invoke refreshToken function five minutes (configurable) before the current token expires. refreshToken requests API for a new token. Once the API responds with a new token and user profile data, refreshToken invokes onLogin, which updates the user and token refs and stores them in the local storage.

Cache Invalidation

Using caches to store data can improve the speed of an application and reduce code complexity. However, if the data is modified, both the cache and the database must be updated in a single transaction; otherwise, the cached data becomes stale.

In the Auth module, there are two caches: the first is the local storage of the user's browser, which the app uses to retrieve the user's information without making an API call. The second cache is the token itself, which is included with every request and contains information that the API uses to perform role-based authorization without needing to query the database for the user's information on every incoming request.

The Write-Through cache strategy is an appropriate approach to update the user data stored in the local storage. When a component needs to update the user data, it invokes a auth store method that requests API. If the API responds with success message it updates the local storage. If the API request fails, the cache is not updated and error is propogated to the component. The component can then choose to retry the request or display an error message to the user.

When data encoded in the token is updated through an API request, a new token is created and returned to the UI as one of the headers in the response. The UI should retrieve this header and use the new token to replace the old token stored in the local storage. In this approach, data consistency is not gauarnteed, as the response may not reach the UI or the UI code encounters an error while processing. For this reason, it's important to include only minimal and seldom updated data in the token.

',23)]))}const _=o(c,[["render",u]]);export{v as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.lean.js b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.lean.js new file mode 100644 index 000000000..4ea6e841e --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gj-L3Q-6.lean.js @@ -0,0 +1 @@ +import{_ as o,c as t,o as a,ag as i}from"./chunks/framework.C9SxlbOG.js";const r="/bioloop/docs/ui/assets/auth-overview-2.png",n="/bioloop/docs/ui/assets/app-load.png",s="/bioloop/docs/ui/assets/navigation-guard-logic.png",h="/bioloop/docs/ui/assets/login-flow-before-cas.png",l="/bioloop/docs/ui/assets/login-flow-after-cas-return.png",d="/bioloop/docs/ui/assets/logged-in-flow.png",v=JSON.parse('{"title":"Auth Explained","description":"","frontmatter":{"title":"Auth Explained"},"headers":[],"relativePath":"ui/auth_explained.md","filePath":"ui/auth_explained.md","lastUpdated":null}'),c={name:"ui/auth_explained.md"};function u(p,e,f,g,m,w){return a(),t("div",null,e[0]||(e[0]=[i("",23)]))}const _=o(c,[["render",u]]);export{v as __pageData,_ as default}; diff --git a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.js b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.js deleted file mode 100644 index 4ce1749bb..000000000 --- a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as a,V as o,a6 as i,a7 as r,a8 as n,a9 as s,aa as h,ab as d}from"./chunks/framework.MXVb71fM.js";const k=JSON.parse('{"title":"Auth Explained","description":"","frontmatter":{},"headers":[],"relativePath":"ui/auth_explained.md","filePath":"ui/auth_explained.md"}'),l={name:"ui/auth_explained.md"},c=o('

Auth Explained

Objectives for Auth module

  1. Enable users to authenticate with IU CAS, which will create a session.
  2. After logging in, redirect users to the URL they originally requested.
  3. Allow users to revisit the app without having to log in again within a certain period of time.
  4. If the session expires between visits, the user will be required to authenticate again.
  5. Ensure that the session does not expire as long as the user remains active on the app.

Overview of Auth Flow

refer to https://kb.iu.edu/d/bfpq

Vue App Code Execution Order

Route Navigation Guard Logic

Login Code flow - Before IU Login

Login Code flow - After IU Login

Code flow for a Revisiting (logged in) User

Code flow for token refresh

On login / initialize, the auth store creates a timer that will invoke refreshToken function five minutes (configurable) before the current token expires. refreshToken requests API for a new token. Once the API responds with a new token and user profile data, refreshToken invokes onLogin, which updates the user and token refs and stores them in the local storage.

Cache Invalidation

Using caches to store data can improve the speed of an application and reduce code complexity. However, if the data is modified, both the cache and the database must be updated in a single transaction; otherwise, the cached data becomes stale.

In the Auth module, there are two caches: the first is the local storage of the user's browser, which the app uses to retrieve the user's information without making an API call. The second cache is the token itself, which is included with every request and contains information that the API uses to perform role-based authorization without needing to query the database for the user's information on every incoming request.

The Write-Through cache strategy is an appropriate approach to update the user data stored in the local storage. When a component needs to update the user data, it invokes a auth store method that requests API. If the API responds with success message it updates the local storage. If the API request fails, the cache is not updated and error is propogated to the component. The component can then choose to retry the request or display an error message to the user.

When data encoded in the token is updated through an API request, a new token is created and returned to the UI as one of the headers in the response. The UI should retrieve this header and use the new token to replace the old token stored in the local storage. In this approach, data consistency is not gauarnteed, as the response may not reach the UI or the UI code encounters an error while processing. For this reason, it's important to include only minimal and seldom updated data in the token.

',23),u=[c];function f(p,g,m,w,b,_){return a(),t("div",null,u)}const x=e(l,[["render",f]]);export{k as __pageData,x as default}; diff --git a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.lean.js b/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.lean.js deleted file mode 100644 index a03b702c8..000000000 --- a/docs/.vitepress/dist/assets/ui_auth_explained.md.Gr83VpDO.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,c as t,o as a,V as o,a6 as i,a7 as r,a8 as n,a9 as s,aa as h,ab as d}from"./chunks/framework.MXVb71fM.js";const k=JSON.parse('{"title":"Auth Explained","description":"","frontmatter":{},"headers":[],"relativePath":"ui/auth_explained.md","filePath":"ui/auth_explained.md"}'),l={name:"ui/auth_explained.md"},c=o("",23),u=[c];function f(p,g,m,w,b,_){return a(),t("div",null,u)}const x=e(l,[["render",f]]);export{k as __pageData,x as default}; diff --git a/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.js b/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.js new file mode 100644 index 000000000..ae5d5ef3e --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"UI","description":"","frontmatter":{"title":"UI","order":2},"headers":[],"relativePath":"ui/index.md","filePath":"ui/index.md","lastUpdated":null}'),n={name:"ui/index.md"};function r(i,o,d,s,c,p){return a(),t("div")}const m=e(n,[["render",r]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.lean.js b/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.lean.js new file mode 100644 index 000000000..ae5d5ef3e --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_index.md.C4WOtbRb.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"UI","description":"","frontmatter":{"title":"UI","order":2},"headers":[],"relativePath":"ui/index.md","filePath":"ui/index.md","lastUpdated":null}'),n={name:"ui/index.md"};function r(i,o,d,s,c,p){return a(),t("div")}const m=e(n,[["render",r]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.js b/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.js deleted file mode 100644 index 8d5dca922..000000000 --- a/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.js +++ /dev/null @@ -1,99 +0,0 @@ -import{_ as s,c as i,o as a,V as t}from"./chunks/framework.MXVb71fM.js";const c=JSON.parse('{"title":"UI","description":"","frontmatter":{},"headers":[],"relativePath":"ui/index.md","filePath":"ui/index.md"}'),n={name:"ui/index.md"},e=t(`

UI

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on VITE_API_REDIRECT_URL (ex: http://localhost:3000) as GET http://localhost:3000/users.

Running using docker

  1. Set VITE_API_REDIRECT_URL to http://api:3000
  2. From the project root run: docker composer up ui -d and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI)

Running on host machine

  1. set VITE_API_REDIRECT_URL to the API base url (ex: http://localhost:3000)
  2. Install modules: pnpm install
  3. Start dev server: pnpm dev

Features

Icons

There are multiple ways to include icons:

  • Material Icons are provided by Vuestic. Iconify icon components are auto imported.
    • usage: <va-icon name="dashboard" />
  • Iconify has a lot of third party / community icons
    • usage: <Icon icon="mdi-flask" class="text-2xl" />
    • usage: <i-mdi-flask/>

Iconify icons are installed using

Colors

Vuestic colors: https://ui.vuestic.dev/en/styles/colors

Accent

  • primary
  • secondary
  • success
  • warning
  • danger
  • info

Background

  • backgroundPrimary
  • backgroundSecondary
  • backgroundElement
  • backgroundBorder

Text

  • textPrimary
  • textInverted

Using

html
<template>
-  <!-- use javascript object -->
-  <div :style="color: {{ colorByStatus }}"></div>
-
-  <!-- use css variables -->
-  <span style="color: var(--va-warning)"> </span>
-
-  <!-- using style block -->
-  <p class="title">
-    Title
-  </p>
-
-  <!-- use builtin props -->
-  <va-button color="info"></va-button>
-</template>
-
-<script setup>
-  import { useColors } from "vuestic-ui";
-  const colors = useColors()
-
-  const colorByStatus = status == 'FAILED' ? colors.danger : color.primary
-</script>
-
-<style scoped>
-.title {
-  color: var(--va-primary)
-}
-</style>

Notable Vuestic Classes

CSS: bioloop/ui/node_modules/vuestic-ui/dist/styles/index.css

va-text-text-primary
-va-text-text-inverted
-va-text-{primary, secondary, warninig, success, danger, info}
-
-va-code-text
-va-code-snippet
-
-va-link
-va-link-secondary
-
-va-blockquote
-va-text-block
-va-text-truncate
-va-text-highlighted

Configuration

Layered and Hierarchical config system:

  • Config for multiple environment is managed through Env variables specified in .env files.
  • Based on the mode, these environment variables are automatically imported into the code by vite.
  • The values from the environment variables is merged with other static config centrally in config.js. All environment variables are backed by sensible default values.

To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable.

javascript
{
-  ...,
-  foobar: import.meta.env.FOOBAR || 120,
-}

Add the name and value of the environment variable to the .env file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called .env.example is maintained. This file contains all the variables defined in .env without the values.

Authentication

Users are authenticated using IU CAS. More on auth module.

By default any page will require user authentication

html
<route lang="yaml">
-meta:
-  title: Dashboard
-</route>

Page requires user authentication + role constrained.

html
<route lang="yaml">
-meta:
-  title: Dashboard
-  requiresRoles: ['operator', 'admin']
-</route>

Only users with either operator or admin role can access this page

No authentication, anonymous view

html
<route lang="yaml">
-meta:
-  title: Dashboard
-  requiresAuth: false
-</route>

Utility Components

Vue Components developed in house to be reused in the app. Documentation

Coding Conventions

  • Use custom component names as <CustomComponent>

Adding Additional Fonts

  • Search for fonts on https://fontsource.org/
  • Install - npm install @fontsource/audiowide
  • Add import '@fontsource/audiowide'; in main.js
  • Add 'Audiowide' to font-family: in body styles in base.css

Dates and Times

  • All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone
  • datetime module is used to consolidate the various date and time formats to use in the UI.
  • Use browser's local time zone to show date and time whenever possible.

Usage:

javascript
import * as datetime from '@/services/datetime.js'
-
-datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023"
-datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00"
-
-datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago"
-datetime.readableDuration(130*1000) // "2 minutes"
-datetime.formatDuration(12000 * 1000) // "3h 20m"

If you have a usecase to display in formats other than above in more than one component, add a function to datetime service and use it.

To set static nav links for a page /page1/page2, add nav attr to route meta config block

html
<route lang="yaml">
-meta:
-  title: Users
-  requiresRoles: ["operator", "admin"]
-  nav: [{ label: "Users" }]
-</route>

Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled.

html
<script setup>
-import { useNavStore } from "@/stores/nav";
-const nav = useNavStore();
-nav.setNavItems([], false);
-</script>

To set dynamic nav links for a page /page-dyn-1/page-dyn-2

html
<script setup>
-import { useNavStore } from "@/stores/nav";
-const nav = useNavStore();
-
-page1Promise = api.getP1()
-page2Promise = api.getP2()
-Promise.all([page1Promise, page2Promise]).then(results => {
-  const page1 = results[0]
-  const page2 = results[1]
-  nav.setNavItems([
-    {
-      label: page1.name,
-      to: "/page1"
-    },
-    {
-      label: page2.name
-    },
-  ]);
-})
-</script>

HTTP API Error Handling and Notifications

API requests are to be made with axios.

Catch the error

javascript
import toast from "@/services/toast";
-
-getRecords()
-  .then((res) => {...})
-  .catch((err) => {
-    if (err?.response?.status == 404)
-        toast.info("No datasets");
-    else toast.error("Could not fetch datatset");
-  })

or let someone else handle the dirty work

javascript
getRecords()
-  .then((res) => {...})

Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc.

`,70),l=[e];function h(p,k,r,o,d,E){return a(),i("div",null,l)}const u=s(n,[["render",h]]);export{c as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.lean.js b/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.lean.js deleted file mode 100644 index 202291c78..000000000 --- a/docs/.vitepress/dist/assets/ui_index.md.epJP0VJ3.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,c as i,o as a,V as t}from"./chunks/framework.MXVb71fM.js";const c=JSON.parse('{"title":"UI","description":"","frontmatter":{},"headers":[],"relativePath":"ui/index.md","filePath":"ui/index.md"}'),n={name:"ui/index.md"},e=t("",70),l=[e];function h(p,k,r,o,d,E){return a(),i("div",null,l)}const u=s(n,[["render",h]]);export{c as __pageData,u as default}; diff --git a/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.js b/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.js new file mode 100644 index 000000000..3ea657741 --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.js @@ -0,0 +1,117 @@ +import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C9SxlbOG.js";const E=JSON.parse('{"title":"Overview","description":"","frontmatter":{"title":"Overview","order":0},"headers":[],"relativePath":"ui/overview.md","filePath":"ui/overview.md","lastUpdated":null}'),e={name:"ui/overview.md"};function l(h,s,p,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t(`

UI Overview

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on VITE_API_REDIRECT_URL (ex: http://localhost:3000) as GET http://localhost:3000/users.

Running using docker

  1. Set VITE_API_REDIRECT_URL to http://api:3000
  2. From the project root run: docker composer up ui -d and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI)

Running on host machine

  1. set VITE_API_REDIRECT_URL to the API base url (ex: http://localhost:3000)
  2. Install modules: pnpm install
  3. Start dev server: pnpm dev

Features

Icons

There are multiple ways to include icons:

  • Material Icons are provided by Vuestic. Iconify icon components are auto imported.
    • usage: <va-icon name="dashboard" />
  • Iconify has a lot of third party / community icons
    • usage: <Icon icon="mdi-flask" class="text-2xl" />
    • usage: <i-mdi-flask/>

Iconify icons are installed using

Colors

Vuestic colors: https://ui.vuestic.dev/en/styles/colors

css
:host {
+  --va-text-selected: #b3d4fc;
+  --va-text-highlighted: #ffc5274e;
+  --va-link-color: var(--va-primary);
+  --va-link-color-secondary: var(--va-secondary);
+  --va-link-color-hover: var(--va-primary-lighten, --va-primary);
+  --va-link-color-active: var(--va-primary);
+  --va-link-color-visited: var(--va-primary-darken, --va-primary);
+  --va-muted: #7f828b;
+  --va-primary: #154ec1;
+  --va-secondary: #767c88;
+  --va-success: #3d9209;
+  --va-info: #158de3;
+  --va-danger: #e42222;
+  --va-warning: #ffd43a;
+  --va-background-primary: #f6f6f6;
+  --va-background-secondary: #ffffff;
+  --va-background-element: #ebf1f4;
+  --va-background-border: #dee5f2;
+  --va-text-primary: #262824;
+  --va-text-inverted: #ffffff;
+  --va-shadow: rgba(0, 0, 0, .12);
+  --va-focus: #49a8ff;
+}

Using

html
<template>
+  <!-- use javascript object -->
+  <div :style="color: {{ colorByStatus }}"></div>
+
+  <!-- use css variables -->
+  <span style="color: var(--va-warning)"> </span>
+
+  <!-- using style block -->
+  <p class="title">
+    Title
+  </p>
+
+  <!-- use builtin props -->
+  <va-button color="info"></va-button>
+</template>
+
+<script setup>
+  import { useColors } from "vuestic-ui";
+  const colors = useColors()
+
+  const colorByStatus = status == 'FAILED' ? colors.danger : color.primary
+</script>
+
+<style scoped>
+.title {
+  color: var(--va-primary)
+}
+</style>

Notable Vuestic Classes

CSS: bioloop/ui/node_modules/vuestic-ui/dist/styles/css-variables.css

Configuration

Configuration values are used to fine-tune the application's behavior. These values are likely to change between different environments or instances of the application.

We have a layered and hierarchical config system:

  • Config for multiple environment is managed through Env variables specified in .env files.
  • Based on the mode, these environment variables are automatically imported into the code by vite.
  • The values from the environment variables is merged with other static config centrally in config.js. All environment variables are backed by sensible default values.

To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable.

javascript
{
+  ...,
+  foobar: import.meta.env.FOOBAR || 120,
+}

Add the name and value of the environment variable to the .env file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called .env.example is maintained. This file contains all the variables defined in .env without the values.

Constants

Constants are values that remain unchanged across different environments. These can be values like the types of datasets recognized by the system, various dataset states, upload states, texts for alert messages, etc. These are stored in constants.js.


Configuration vs Constants

  1. Mutability - Configuration values may change between environments or during runtime, while constants remain fixed.
  2. Source: Configuration often comes from environment variables, while constants are hardcoded in the application.
  3. Purpose: Configuration is used to adjust application behavior, while constants define fixed aspects of the application.

Authentication

Users are authenticated using IU CAS. More on auth module.

Authentication with google OpenID Connect is implemented following this guide https://developers.google.com/identity/openid-connect/openid-connect

Authentication with CILogon OpenID Connect is implemented following this guide https://www.cilogon.org/oidc

Enable / disable login with authentication providers:

ui/src/config.js

  • "auth_enabled.google": true | false
  • "auth_enabled.cilogon": true | false

api/src/config/default.json

  • "auth.google.enabled": true | false
  • "auth.cilogon.enabled": true | false

Environment Variables:

ui/.env

api/.env

  • GOOGLE_OAUTH_CLIENT_ID=
  • GOOGLE_OAUTH_CLIENT_SECRET=
  • CILOGON_OAUTH_CLIENT_ID=
  • CILOGON_OAUTH_CLIENT_SECRET=

Authentication controls on router

By default any page will require user authentication

html
<route lang="yaml">
+meta:
+  title: Dashboard
+</route>

Page requires user authentication + role constrained.

html
<route lang="yaml">
+meta:
+  title: Dashboard
+  requiresRoles: ['operator', 'admin']
+</route>

Only users with either operator or admin role can access this page

No authentication, anonymous view

html
<route lang="yaml">
+meta:
+  title: Dashboard
+  requiresAuth: false
+</route>

Utility Components

Vue Components developed in house to be reused in the app. Documentation

Coding Conventions

  • Use custom component names as <CustomComponent>

Adding Additional Fonts

  • Search for fonts on https://fontsource.org/
  • Install - npm install @fontsource/audiowide
  • Add import '@fontsource/audiowide'; in main.js
  • Add 'Audiowide' to font-family: in body styles in base.css

Dates and Times

  • All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone
  • datetime module is used to consolidate the various date and time formats to use in the UI.
  • Use browser's local time zone to show date and time whenever possible.

Usage:

javascript
import * as datetime from '@/services/datetime.js'
+
+datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023"
+datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00"
+
+datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago"
+datetime.readableDuration(130*1000) // "2 minutes"
+datetime.formatDuration(12000 * 1000) // "3h 20m"

If you have a usecase to display in formats other than above in more than one component, add a function to datetime service and use it.

Feature Flags

Features can be enabled or disabled at the UI level. Components can determine whether a feature is enabled by reading it from ./config.js, which in turn reads this config from ./.env.

// ./config.js
+
+  ...
+  enabledFeatures: {
+    genomeBrowser: import.meta.env.VITE_ENABLED_GENOME_BROWSER === "true",
+  },
+  ...
# ./.env
+
+VITE_ENABLED_GENOME_BROWSER=true

Reading the feature flag from .env allows for features to be toggled without changing the code.

Once a feature's status has been changed in .env, the app will need to be redeployed for those changes to come into effect.

To set static nav links for a page /page1/page2, add nav attr to route meta config block

html
<route lang="yaml">
+meta:
+  title: Users
+  requiresRoles: ["operator", "admin"]
+  nav: [{ label: "Users" }]
+</route>

Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled.

html
<script setup>
+import { useNavStore } from "@/stores/nav";
+const nav = useNavStore();
+nav.setNavItems([], false);
+</script>

To set dynamic nav links for a page /page-dyn-1/page-dyn-2

html
<script setup>
+import { useNavStore } from "@/stores/nav";
+const nav = useNavStore();
+
+page1Promise = api.getP1()
+page2Promise = api.getP2()
+Promise.all([page1Promise, page2Promise]).then(results => {
+  const page1 = results[0]
+  const page2 = results[1]
+  nav.setNavItems([
+    {
+      label: page1.name,
+      to: "/page1"
+    },
+    {
+      label: page2.name
+    },
+  ]);
+})
+</script>

HTTP API Error Handling and Notifications

API requests are to be made with axios.

Catch the error

javascript
import toast from "@/services/toast";
+
+getRecords()
+  .then((res) => {...})
+  .catch((err) => {
+    if (err?.response?.status == 404)
+        toast.info("No datasets");
+    else toast.error("Could not fetch datatset");
+  })

or let someone else handle the dirty work

javascript
getRecords()
+  .then((res) => {...})

Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc.

`,89)]))}const g=i(e,[["render",l]]);export{E as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.lean.js b/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.lean.js new file mode 100644 index 000000000..3f3277244 --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_overview.md.DxPi67l2.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C9SxlbOG.js";const E=JSON.parse('{"title":"Overview","description":"","frontmatter":{"title":"Overview","order":0},"headers":[],"relativePath":"ui/overview.md","filePath":"ui/overview.md","lastUpdated":null}'),e={name:"ui/overview.md"};function l(h,s,p,k,r,o){return n(),a("div",null,s[0]||(s[0]=[t("",89)]))}const g=i(e,[["render",l]]);export{E as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.js b/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.js new file mode 100644 index 000000000..52a43a48a --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.js @@ -0,0 +1,563 @@ +import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"Utility Components","description":"","frontmatter":{"title":"Utility Components"},"headers":[],"relativePath":"ui/util_components.md","filePath":"ui/util_components.md","lastUpdated":null}'),h={name:"ui/util_components.md"};function t(k,s,p,e,E,r){return n(),a("div",null,s[0]||(s[0]=[l(`

Utility Components

AutoComplete

Basic Usage

html

+<template>
+  <AutoComplete
+    :data="datasets"
+    filter-by="name"
+    placeholder="Search datasets"
+    @select="handleDatasetSelect"
+  />
+</template>
+
+<script setup>
+  const datasets = [{name: 'dataset-1'}, {name: 'dataset-2'}, {name: 'dataset-3'}]
+
+  function handleDatasetSelect(item) {
+    console.log('selected', item)
+  }
+</script>

With Slots

html

+<template>
+  <AutoComplete
+    :data="users"
+    :filter-fn="filterFn"
+    placeholder="Search users by name, username, or email"
+    @select="handleUserSelect"
+  >
+    <template #filtered="{ item }">
+      <span> {{ item.name }} </span>
+      <span class="va-text-secondary px-1 font-bold"> &centerdot; </span>
+      <span class="va-text-secondary text-sm"> {{ item.email }} </span>
+    </template>
+  </AutoComplete>
+</template>
+
+<script setup>
+  const users = ref([]);
+  const selectedUser = ref();
+  const filterFn = (text) => (user) => {
+    const _text = text.toLowerCase();
+    return (
+      user.name.toLowerCase().includes(_text) ||
+      user.username.toLowerCase().includes(_text) ||
+      user.email.toLowerCase().includes(_text)
+    );
+  };
+
+  userService.getAll().then((data) => {
+    users.value = data;
+    console.log(users.value);
+  });
+</script>

Async

html

+<template>
+  <AutoComplete
+    :async="true"
+    v-model:search-text="searchText"
+    :placeholder="\`Search Users\`"
+    :data="retrievedUsers"
+    :display-by="'username'"
+    @clear="emit('clear')"
+    @select="
+      (user) => {
+        onSelect(user);
+      }
+    "
+    :label="'Search Users'"
+    @open="emit('open')"
+    @close="emit('close')"
+  />
+</template>
+
+<script setup>
+const searchText = ref("")
+const retrievedUsers = ref([])
+
+const onSelect = (selectedUser) => {
+  const searchTerm = selectedUser.username
+  userService.getByMatchingUsername({
+    username: searchTerm
+  }).then(res => {
+    retrievedUsers.value = res.data
+  })
+};
+</script>

Props

  • search-text: String - Can optionally be provided to show the selected value within AutoComplete. By default, selected values are not shown.
  • placeholder: String - placeholder for the input element
  • data: Array of Objects - data to search and display
  • filter-by: String - property of data object to use with case-insensitive search
  • display-by: String - property of data object to use to show search results
  • filter-fn: Function (text: String) => (item: Object) => Bool: When provided used to filter the data based on enetered text value
  • async: Boolean - Can be used in combination with search-text to enable asynchronous search for results. Defaults to false.
  • disabled: Boolean - Can be used to show the underlying va-input element in a disabled state. Defaults to false.
  • error: String - Error message to show beneath the underlying va-input element.
  • label: String - Label for AutoComplete
  • loading: Boolean - determines if AutoComplete's dropdown will show a loading indicator, or retrieved results

Events

  • select - emitted when one of the search results is clicked
  • open - emitted when the AutoComplete is opened
  • close - emitted when the AutoComplete is closed
  • clear - emitted when the selected value or the current search term is cleared via va-input's clear button
  • update:search-text - emitted when the search input is changed

Slots

  • #filtered={ item } - Named slot (filtered) with props ({item}) to render a custom search result. This slot is in v-for and called for each search result.
  • #appendInner - Named slot (appendInner) to append custom markup to AutoComplete's input field. The markup provided will be rendered inside va-input's appendInner slot.
  • #prependInner - Named slot (prependInner) to prepend custom markup to AutoComplete's input field. The markup provided will be rendered inside va-input's prependInner slot.

SearchAndSelect

The SearchAndSelect widget offers these features:

  1. Searching for entities
  2. Applying additional filters for the search
  3. Fetching results in batches via infinite-scrolling (the ability to load more results once the user has scrolled past the currently retrieved results)
  4. Selecting / unselecting individual entities, or multiple entities at once.
  5. Emitting events to make client aware of entities being selected/unselected, or of the search being reset.
  6. Load this widget with certain results pre-selected

Basic Usage

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+      }
+    "
+  />
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
+  );
+};
+
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch(searchTerm, () => {
+  resetSearchState();
+});
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm) => {
+  const filterSuffix = (searchTerm) => {
+    return searchTerm ? \`, for keyword '\${searchTerm}'\` : "";
+  };
+
+  let text = (i) => \`Result \${i + 1}\` + filterSuffix(searchTerm);
+
+  const other = (i) =>
+    \`Other val for result \${i + 1}\` + filterSuffix(searchTerm);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Filters

Filters can be shown in the search tool via slots. The reset event can be used by the client to reset its filters.

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+        selectValue = ''; // reset Filter
+      }
+    "
+  >
+    <template #filters>
+      <div class="max-w-xs">
+        <VaSelect
+          v-model="selectValue"
+          :options="selectOptions"
+          placeholder="Select an option"
+          label="Filter Dropdown"
+        />
+      </div>
+    </template>
+  </SearchAndSelect>
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectValue = ref("");
+const selectOptions = ref([1, 2, 3]);
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
+  );
+};
+
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const filterQuery = computed(() => {
+  return selectValue.value
+    ? {
+        other: selectValue.value,
+      }
+    : undefined;
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...filterQuery.value,
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch([searchTerm, filterQuery], () => {
+  resetSearchState();
+});
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit, other }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text, other),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm, filterValue) => {
+  const filterSuffix = (searchTerm, dropdownVal) => {
+    return (
+      (searchTerm ? \`, for keyword '\${searchTerm}'\` : "") +
+      (dropdownVal
+        ? \`, \${searchTerm ? "and" : "for"} dropdown \${dropdownVal}\`
+        : "")
+    );
+  };
+
+  let text = (i) => \`Result \${i + 1}\` + filterSuffix(searchTerm, filterValue);
+
+  const other = (i) =>
+    \`Other val for result \${i + 1}\` + filterSuffix(searchTerm, filterValue);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm, filterValue) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm, filterValue));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Formatting and Slots

Displayed results can be formatted via the formatFn prop. They can also be put inside slots for a more customized markup per cell.

For showing a field's value inside customized markup

  • set { slotted: true } in the field's config that is being provided via the searchResultColumns or selectedResultColumns props
  • embed the cell's value inside <template #templateName> ( example - <template #address>).
    • The name of a column's template is the same as the key of the column's config that was provided via the searchResultColumns or selectedResultColumns props.

The value of the column inside the <template> can be accessed via slotProps["value"], which is an object that contains the formatted as well as the raw value for the slotted field.

// slotProps['value]
+
+{
+  formatted: 'formatted value',
+  raw: 'raw value
+}

The below example formats the values in the text column, and embeds the values in the other column inside custom markup.

Notice how both the formatted and raw values of a slotted field can be access via slotProps["value"].

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+      }
+    "
+  >
+    <template #other="slotProps">
+    <!-- Both formatted and unFormatted values can be accessed via slotProps -->
+      <va-chip>{{ slotProps["value"].formatted }}</va-chip>
+      <va-chip>{{ slotProps["value"].raw }}</va-chip>
+    </template>
+  </SearchAndSelect>
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
+  );
+};
+
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch(searchTerm, () => {
+  resetSearchState();
+});
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm) => {
+  const filterSuffix = (searchTerm) => {
+    return searchTerm ? \`, for keyword '\${searchTerm}'\` : "";
+  };
+
+  let text = (i) => \`Result \${i + 1}\` + filterSuffix(searchTerm);
+
+  const other = (i) =>
+    \`Other val for result \${i + 1}\` + filterSuffix(searchTerm);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+    formatFn: (text) => \`Formatted \${text}\`,
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+    slotted: true,
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Notes

  1. Some props that can be either a string or a function. In such cases, if the prop is a function, it will be called with the target argument, and return the result. If it is a string, the value of the property matching the path specified by the string is looked up in the target argument, and returned.

Props

  • messages: Array - Hint message(s) to be shown below the underlying va-input
  • selectMode: String ['single' | 'multiple'] - Determines if the widget should allow selecting/unselecting multiple results at once. Use single for only allowing a single result to be selected/unselected at one time. Defaults to multiple.
  • placeholder: String - Placeholder for the search input. Default - "Type to search"- selectedResults: Array - the array of currently selected results. Can be used to load the widget with some items pre-selected. Defaults to [].
  • loading: Boolean - Shows loading indicator and disables controls when loading. Defaults to False.
  • searchResults: Array - the array of results retrieved via the current search. Defaults to [].
  • selectedResults: Array - the array of currently selected search results. Can be used to load the widget with some items pre-selected. Defaults to [].
  • selectedLabel: String - The label to show for the table of selected results. Default - "Selected Results"
  • searchResultColumns: Array - The display config for the <va-data-table> of search results. Extends the columns prop provided to <va-data-table>. A formatFn function can be provided in a column's config to format the column's value a certain way. Moreover, { slotted: true } can be added to the column's config to embed the column's value in custom markup. See the Formatting and Slots section above for details.
  • selectedResultColumns: Array - The display config for the <va-data-table> of selected results. Extends the columns prop provided to <va-data-table>. A formatFn function can be provided in a column's config to format the column's value a certain way. Moreover, { slotted: true } can be added to the column's config to embed the column's value in custom markup. See the Formatting and Slots section above for details.
  • searchResultCount: Number - Total number results retrieved from the current search (not to be confused with the number of results in the current batch)
  • searchTerm: String - The search term to be used for performing the search. Client provides this as a component v-model, via v-model:searchTerm.
  • trackBy: String | Function - Used to uniquely identity a result. Defaults to "id".
  • pageSizeSearch: Number - the number of results to be fetched in one batch. Defaults to 10.
  • resource: String - the name of the entity being searched for. Defaults to result.
  • error: String - error to be shown beneath the underlying va-input.
  • showError: Boolean - determines whether error will be shown
  • controlsMargin: String - margin between the controls and the tables
  • controlsHeight: String - height of the controls container element

Slots

  • filters - used for providing controls used for filtering results
  • dynamically-named slots, whose name is the key of the column that the slot is intended for. See the Formatting and Slots section above for an example.

Events

  • search - emitted when a list of elements are selected, with the list of selected elements provided as an argument.
  • remove - emitted when a list of elements are unselected, with the list of unselected elements provided as an argument.
  • reset - emitted when the search controls are reset
  • update:searchTerm - emitted when the searchTerm v-model is to be updated
  • scroll-end - emitted when user scrolls to the end of the current batch of results

Maybe

Show data if it is neither null or undefined, else show default (provided it is also not null or undefined)

html

+<template>
+  <Maybe :data="rowData?.metadata?.num_genome_files"/>
+</template

Props

  • data: Any
  • default: Any

CopyText

  • Show text in a read-only input attached with a copy to clipboard button.
  • Width is relative 100%.
  • Input container is x-scrollable if the text overflows
html

+<template>
+  <CopyText :text="dataset.archive_path"/>
+</template>

props

  • text: String

BinaryStatusChip

Shows a chip with icon, text, color depending on status. Useful to on/off status

html

+<template>
+  <BinaryStatusChip
+    :status="!source"
+    :icons="['mdi:account-off-outline', 'mdi:account-badge-outline']"
+  />
+</template>

Props

  • status: Boolean (0-off, 1-on)
  • icons - Array of 2 elements (off icon, on icon)
  • labels - Array of 2 element (off label, on label)
    • default: ['disbaled', 'enabled']

EnvAlert

Shows an <va-alert/> which displays the mode that the app is running in (test, CI, etc.).

The environments that this alert should be enabled for can be set in ./ui/config.js, under property alertForEnvironments.

html
<template>
+  <EnvAlert color="warning" icon="info" />
+</template>

Props

  • icon: String - icon to be included in the alert.
  • color: String - alert's color

Props are forwarded to Vuestic's <va-alert /> component.

useQueryPersistence Composable

This composition function helps you manage query parameters in the URL and keep them in sync with a reactive object in your component.

Usage:

  1. Create a ref to hold the query parameters
  2. Call this function on the ref.
javascript
import useQueryPersistence from "@/composables/useQueryPersistence";
+
+const default_query_params = () => ({
+  status: null,
+  page: 1,
+  auto_refresh: 10,
+});
+
+const query_params = ref(default_query_params());
+
+useQueryPersistence({
+  refObject: query_params,
+  defaultValue: default_query_params(),
+  key: "wq",
+  history_push: false,
+});

It will update the URL query parameters by watching the refObject and it will update the refObject when URL query parameters change.

`,68)]))}const y=i(h,[["render",t]]);export{g as __pageData,y as default}; diff --git a/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.lean.js b/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.lean.js new file mode 100644 index 000000000..06c5d6fb3 --- /dev/null +++ b/docs/.vitepress/dist/assets/ui_util_components.md.MrZXUT6Q.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.C9SxlbOG.js";const g=JSON.parse('{"title":"Utility Components","description":"","frontmatter":{"title":"Utility Components"},"headers":[],"relativePath":"ui/util_components.md","filePath":"ui/util_components.md","lastUpdated":null}'),h={name:"ui/util_components.md"};function t(k,s,p,e,E,r){return n(),a("div",null,s[0]||(s[0]=[l("",68)]))}const y=i(h,[["render",t]]);export{g as __pageData,y as default}; diff --git a/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.js b/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.js deleted file mode 100644 index 83494cb23..000000000 --- a/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.js +++ /dev/null @@ -1,70 +0,0 @@ -import{_ as s,c as i,o as a,V as t}from"./chunks/framework.MXVb71fM.js";const y=JSON.parse('{"title":"Utility Components","description":"","frontmatter":{},"headers":[],"relativePath":"ui/util_components.md","filePath":"ui/util_components.md"}'),n={name:"ui/util_components.md"},l=t(`

Utility Components

Auto Complete

Basic Usage

html
<template>
-<AutoComplete
-  :data="datasets"
-  filter-by="name"
-  placeholder="Search datasets"
-  @select="handleDatasetSelect"
-/>
-</template>
-
-<script setup>
-  const datasets = [{name: 'dataset-1'},{name: 'dataset-2'},{name: 'dataset-3'}]
-  function handleDatasetSelect(item) {
-    console.log('selected', item)
-  }
-</script>

Advanced Usage

html
<template>
-<AutoComplete
-  :data="users"
-  :filter-fn="filterFn"
-  placeholder="Search users by name, username, or email"
-  @select="handleUserSelect"
->
-  <template #filtered="{ item }">
-    <span> {{ item.name }} </span>
-    <span class="va-text-secondary px-1 font-bold"> &centerdot; </span>
-    <span class="va-text-secondary text-sm"> {{ item.email }} </span>
-  </template>
-</AutoComplete>
-</template>
-
-<script setup>
-  const users = ref([]);
-const selectedUser = ref();
-const filterFn = (text) => (user) => {
-  const _text = text.toLowerCase();
-  return (
-    user.name.toLowerCase().includes(_text) ||
-    user.username.toLowerCase().includes(_text) ||
-    user.email.toLowerCase().includes(_text)
-  );
-};
-
-userService.getAll().then((data) => {
-  users.value = data;
-  console.log(users.value);
-});
-</script>

Props

  • placeholder: String - placeholder for the input element
  • data: Array of Objects - data to search and display
  • filter-by: String - property of data object to use with case-insensitive search
  • display-by: String - property of data object to use to show search results
  • filter-fn: Function (text: String) => (item: Object) => Bool: When provided used to filter the data based on enetered text value

Events

  • select - emitted when one of the search results is clicked

Slots

  • #filtered={ item }. Named slot (filtered) with props ({item}) to render a custom search result. This slot is in v-for and called for each search result.

Maybe

Show data if it is neither null or undefined, else show default (provided it is also not null or undefined)

html
<template>
-  <Maybe :data="rowData?.metadata?.num_genome_files" />
-</template

Props

  • data: Any
  • default: Any

CopyText

  • Show text in a read-only input attached with a copy to clipboard button.
  • Width is relative 100%.
  • Input container is x-scrollable if the text overflows
html
<template>
-  <CopyText :text="dataset.archive_path" />
-</template>

props

  • text: String

BinaryStatusChip

Shows a chip with icon, text, color depending on status. Useful to on/off status

html
<template>
-  <BinaryStatusChip
-    :status="!source"
-    :icons="['mdi:account-off-outline', 'mdi:account-badge-outline']"
-  />
-</template>

Props

  • status: Boolean (0-off, 1-on)
  • icons - Array of 2 elements (off icon, on icon)
  • labels - Array of 2 element (off lable, on label)
    • default: ['disbaled', 'enabled']

useQueryPersistence Composable

This composition function helps you manage query parameters in the URL and keep them in sync with a reactive object in your component.

Usage:

  1. Create a ref to hold the query parameters
  2. Call this function on the ref.
javascript
import useQueryPersistence from "@/composables/useQueryPersistence";
-
-const default_query_params = () => ({
-  status: null,
-  page: 1,
-  auto_refresh: 10,
-});
-
-const query_params = ref(default_query_params());
-
-useQueryPersistence({
-  refObject: query_params,
-  defaultValue: default_query_params(),
-  key: "wq",
-  history_push: false,
-});

It will update the URL query parameters by watching the refObject and it will update the refObject when URL query parameters change.

`,33),h=[l];function e(p,k,E,r,d,o){return a(),i("div",null,h)}const c=s(n,[["render",e]]);export{y as __pageData,c as default}; diff --git a/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.lean.js b/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.lean.js deleted file mode 100644 index e32726494..000000000 --- a/docs/.vitepress/dist/assets/ui_util_components.md.wuKa-Jls.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,c as i,o as a,V as t}from"./chunks/framework.MXVb71fM.js";const y=JSON.parse('{"title":"Utility Components","description":"","frontmatter":{},"headers":[],"relativePath":"ui/util_components.md","filePath":"ui/util_components.md"}'),n={name:"ui/util_components.md"},l=t("",33),h=[l];function e(p,k,E,r,d,o){return a(),i("div",null,h)}const c=s(n,[["render",e]]);export{y as __pageData,c as default}; diff --git a/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.js b/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.js new file mode 100644 index 000000000..7b7a50752 --- /dev/null +++ b/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.js @@ -0,0 +1 @@ +import{_ as t,c as a,o,ag as i}from"./chunks/framework.C9SxlbOG.js";const p=JSON.parse('{"title":"Upload Architecture","description":"","frontmatter":{},"headers":[],"relativePath":"upload.md","filePath":"upload.md","lastUpdated":null}'),s={name:"upload.md"};function r(l,e,d,n,h,c){return o(),a("div",null,e[0]||(e[0]=[i('

Upload Architecture

1. Introduction

Bioloop allows uploading one or more files through the browser, while ensuring strict access control to prevent unauthorized access. These chunks are then merged into corresponding files through a worker.

2. Requirements and Limitations

Authorized users should be able to upload files directly from their web browsers. The uploaded files should be protected from unauthorized users who have access to Slate-Scratch.

Network timeouts, data corruption and other problems that arise from uploading large files must be avoided. To achieve this, our file upload architecture uploads files in chunks of 2 MB. Uploading files in chunks also gives us a more granular view into the upload state.

Since our workers run on Colo nodes, it is convenient to upload files to Colo nodes, so they can be processed by workers, instead of uploading files to the main API server, and then transferring them to Colo nodes.

3. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the appropriate page to initiate file uploads.
  • API: This node serves the user interface and API endpoints with user and metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Rhythm API: This node is used to trigger workflows from the UI.
  • Workers: Processing of uploaded file chunks occurs on this node via a Celery task. These worker run on Colo nodes.
  • Signet: An OAuth server supporting client credential flow with the appropriate scope for uploading files to create secure tokens.
  • File Upload Server (Nginx): A server on the worker node which receives requests from users to upload files. Files are uploaded to this server, in chunks.
  • Secure Upload API: A lightweight app hosted on the File Upload Server that validates the incoming requests (i.e. checking for the appropriate scope) to the file upload API.

4. The Upload

4.1 Logging

For each upload, information is logged to the following relational tables (PostgreSQL):

  1. upload_log - contains metadata about the upload itself. Is linked to one or more file_upload_log objects.
  2. dataset_upload_log - contains metadata specific to a dataset's upload. Is linked to one upload_log object, and one dataset object.
  3. file_upload_log - contains metadata about a file being uploaded.

4.2 Steps

  1. Before an upload begins, the following things happen sequentially:
    • Checksum evaluation - MD5 checksums are evaluated for each file being uploaded, as well as for each chunk per file.
    • Any metadata associated with the upload (like the source raw data, the file type, the names/checksums/relative paths of files being uploaded, etc.), are logged into persistent storage.
  2. The actual upload begins once the above steps are successful.
  3. Chunks are uploaded sequentially. If a chunk upload fails, the client-side retries the upload upto 5 times before failing.
  4. The client sends an HTTP request to upload a chunk to our File Upload Server, which writes the received chunk to the filesystem, after validating its checksum (this is the first stage of checksum validation).
  5. After all files' chunks are uploaded successfully, the client-side makes a request to the Rhythm API to trigger the process_dataset_upload worker, which merges each file's uploaded chunks into the corresponding file.

4.3 Directory structure

The following directories on the File Upload Server are used for uploads:

  1. Directory that Data Products are uploaded to:
upload_dir = config['paths']['DATA_PRODUCT']['upload']
  1. The individual chunks for an uploaded file are stored in:
[upload_dir] / [dataset_id] / uploaded_chunks / [file_upload_log_id]`

Here, dataset_id is the unique id created for the dataset before the upload began, and file_upload_log_id is the unique id for the record of this file's upload. 3. Within this directory, individual chunks are named as [file_md5]-[i] where i serves as an index for this chunk among all of the file's sequentially-uploaded chunks, identifying the order of this chunk in the file. When merging a file's chunks into the corresponding file, chunks will be processed sequentially based on this index. 4. Once a file has been processed, and it's chunks have been merged into the corresponding file, the recreated file is stored at:

[upload_dir] / [dataset_id] / processed

4.4 Access Control

  1. To verify that a user is authorized to initiate an upload, we perform role-based checking when our /upload API receives a request to initiate an upload. This validation cannot be performed on the secure_upload NGINX server, since we only maintain access-control data on the API server.
  2. After verifying that the user is authorized to upload datasets, the client requests a Bearer token from the Signet service, and attaches it to the Authorization header before sending a request to the /upload API to upload a file chunk.
    • The scope included in the granted token contains the name of the file prefixed with the scope prefix upload_file:.
    • If the name of the file being uploaded has spaces in it, the spaces are replaced by hyphens in the granted token's scope.
  3. Before the /upload endpoint accepts a file chunk that needs to be uploaded, it verifies that the scope contained in the Bearer token is the same as the expected scope. If these scopes do not match, the /upload API rejects the HTTP request.

Example

As an example, to upload file my file.json, the Bearer token that is used to call the /upload API file will be expected to have scope upload_file:my-file.json.

4.5 Status

The status of the upload, as well the status of each file in the upload goes through the following values:

StatusDescription
COMPUTING_CHECKSUMSChecksums are being computed for the file(s) to be uploaded
CHECKSUM_COMPUTATION_FAILEDChecksums computation failed for the file(s) to be uploaded
UPLOADINGUpload initiated through the browser
UPLOAD_FAILEDUpload could not be completed (network errors)
UPLOADEDAll files successfully uploaded
PROCESSINGUpload currently being processed
PROCESSING_FAILEDEncountered errors while processing a file in this upload
COMPLETEAll files in the upload processed successfully
FAILEDUpload was failing processing (i.e. status == PROCESSING_FAILED) for more than 72 hours, and was therefore marked as FAILED and its filesystem resources deleted.

5. Processing

Uploaded file chunks are merged into the corresponding file by the worker process_dataset_upload. After the file has been recreated from its chunks, the MD5 checksum of the recreated file is matched with the expected MD5 checksum of the file that was persisted to the database before the upload (this is the second stage of checksum validation).

After all uploaded files have been processed successfully, the resources (uploaded file chunks) associated with them are deleted.

6. Data Integrity

The uploaded data goes through two stages of checksum validation:

  1. Validating MD5 checksum of an uploaded file chunk before writing it to the filesystem.
  2. Validating MD5 checksum of the file, once it has been recreated from its chunks by the worker.

7. Retry

  1. Upon encountering retryable exceptions, the process_dataset_upload worker retries itself 3 times before failing.
  2. The script manage_pending_dataset_uploads.py, which is scheduled to run every 24 hours, looks for uploads that are failing (status == PROCESSING_FAILED), and retries to process the ones which have been failing for less than 72 hours. If some uploads have been failing for more than 72 hours, they are marked as FAILED and their filesystem resources (uploaded file chunks, and any processed files) are purged.
',39)]))}const f=t(s,[["render",r]]);export{p as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.lean.js b/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.lean.js new file mode 100644 index 000000000..194fa15b7 --- /dev/null +++ b/docs/.vitepress/dist/assets/upload.md.zCMbS6HI.lean.js @@ -0,0 +1 @@ +import{_ as t,c as a,o,ag as i}from"./chunks/framework.C9SxlbOG.js";const p=JSON.parse('{"title":"Upload Architecture","description":"","frontmatter":{},"headers":[],"relativePath":"upload.md","filePath":"upload.md","lastUpdated":null}'),s={name:"upload.md"};function r(l,e,d,n,h,c){return o(),a("div",null,e[0]||(e[0]=[i("",39)]))}const f=t(s,[["render",r]]);export{p as __pageData,f as default}; diff --git a/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.js b/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.js new file mode 100644 index 000000000..ed18d1364 --- /dev/null +++ b/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.js @@ -0,0 +1 @@ +import{_ as t,c as o,o as a,ag as s}from"./chunks/framework.C9SxlbOG.js";const d=JSON.parse('{"title":"Welcome Message","description":"","frontmatter":{"title":"Welcome Message"},"headers":[],"relativePath":"welcome-message.md","filePath":"welcome-message.md","lastUpdated":null}'),r={name:"welcome-message.md"};function n(i,e,l,c,u,p){return a(),o("div",null,e[0]||(e[0]=[s("

Subject: Your Research Data is Now Available

Dear [Researcher’s Name],

Your recent data request is now complete and available for access.

To retrieve your data:

  1. Visit the data portal: [Portal URL]
  2. Click on the "Log In" button on the homepage.
  3. Sign in using your [institution credentials or other login method].
  4. Navigate to the "Projects" section to locate your data.
  5. If your data is actively on disk, use the "Download" button under "Associated Datasets" to access it.
  6. If your data is archived on tape, use the "Stage" button to request retrieval. Once the staging process is complete, the "Download" button will become available.

If you have any questions, feedback, or require assistance, please feel free to contact us at [Support Email or Contact Information].

Best regards,
[Your Name]
[Your Position]
[Institution or Research Core Name]

",7)]))}const m=t(r,[["render",n]]);export{d as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.lean.js b/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.lean.js new file mode 100644 index 000000000..2c07d8dd5 --- /dev/null +++ b/docs/.vitepress/dist/assets/welcome-message.md.B_3Ps02e.lean.js @@ -0,0 +1 @@ +import{_ as t,c as o,o as a,ag as s}from"./chunks/framework.C9SxlbOG.js";const d=JSON.parse('{"title":"Welcome Message","description":"","frontmatter":{"title":"Welcome Message"},"headers":[],"relativePath":"welcome-message.md","filePath":"welcome-message.md","lastUpdated":null}'),r={name:"welcome-message.md"};function n(i,e,l,c,u,p){return a(),o("div",null,e[0]||(e[0]=[s("",7)]))}const m=t(r,[["render",n]]);export{d as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.js b/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.js new file mode 100644 index 000000000..37c919d31 --- /dev/null +++ b/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.js @@ -0,0 +1 @@ +import{_ as e,c as r,o as t}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Workers","description":"","frontmatter":{"title":"Workers","order":4},"headers":[],"relativePath":"worker/index.md","filePath":"worker/index.md","lastUpdated":null}'),o={name:"worker/index.md"};function a(n,s,d,c,i,p){return t(),r("div")}const m=e(o,[["render",a]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.lean.js b/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.lean.js new file mode 100644 index 000000000..37c919d31 --- /dev/null +++ b/docs/.vitepress/dist/assets/worker_index.md.BPwBHMMF.lean.js @@ -0,0 +1 @@ +import{_ as e,c as r,o as t}from"./chunks/framework.C9SxlbOG.js";const _=JSON.parse('{"title":"Workers","description":"","frontmatter":{"title":"Workers","order":4},"headers":[],"relativePath":"worker/index.md","filePath":"worker/index.md","lastUpdated":null}'),o={name:"worker/index.md"};function a(n,s,d,c,i,p){return t(),r("div")}const m=e(o,[["render",a]]);export{_ as __pageData,m as default}; diff --git a/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.js b/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.js deleted file mode 100644 index 555c733a6..000000000 --- a/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.js +++ /dev/null @@ -1,21 +0,0 @@ -import{_ as s,c as i,o as e,V as a}from"./chunks/framework.MXVb71fM.js";const u=JSON.parse('{"title":"Workers","description":"","frontmatter":{},"headers":[],"relativePath":"worker/index.md","filePath":"worker/index.md"}'),t={name:"worker/index.md"},l=a(`

Workers

Coding Guidelines

Hierarchical Config

  • The default & dev config goes into workers/config/common.py
  • The overrides for production goes into workers/config/production.py
  • Based on the environment variable APP_ENV, config from that file is imported and merged with the common config.
    • Add APP_ENV=production to .env file which load_dotenv reads or
    • directly set it as export APP_ENV=production.
  • In project files, import config as from workers.config import config
  • Imported config is a DotMap object, which supports both config[] and config. access.
  • To add a new environment (for example "stage"), create a new file inside workers/config called stage.py and have the overriding config as a dict assigned to a variable named config.

Celery config

  • config specific to Celery is in workers/config/celeryconfig.py
  • Config is in python values, instead of a dict
  • Env specific values and secrets are loaded from .env file

Code Organization

  • Celery Tasks: workers/tasks/*.py
  • Scheduled job and other scripts: workers/scripts/*.py
  • Helper code: workers/*.py
  • Config / settings are in workers/config/*.py and .env
  • Test code is in tests/

Hot Module Replacement

Worker automatically run with updated code except for the code in

  • workers.config.*
  • workers.utils
  • workers.celery_app
  • workers.task.declaration

Deployment

  • Add module load python/3.10.5 to ~/.modules
  • Update .env (make a copy of .env.example and add values)
  • Install dependencies
bash
poetry export --without-hashes --format=requirements.txt > requirements.txt
-pip install -r requirements.txt
bash
cd ~/app/workers
-pm2 start ecosystem.config.js
-# optional
-pm2 save

Testing with workers running on local machine

Start mongo and queue

bash
cd <rhythm_api>
-docker-compose up queue mongo -d

Start Workers

bash
python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q'

--concurrency 1: number of worker processed to pre-fork

-O fair: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks when they are actually available.

Use --hostname '<app_name>-celery-<worker_name>@%h' to distinguish multiple workers running on the same machine either for the same app or different apps.

  • replace <app_name> with app name (ex: bioloop)
  • replace <worker_name> with worker name (ex: w1)

Auto-scaling - max_concurrency,min_concurrency --autoscale=10,3 (always keep 3 processes, but grow to 10 if necessary).

--queues '<app_name>-dev.sca.iu.edu' comma separated queue names. worker will subscribe to these queues for accepting tasks. Configured in workers/config/celeryconfig.py with task_routes, task_default_queue

Run test

bash
python -m tests.test

Testing with workers running on COLO node and Rhythm API

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

  • start postgres locally using docker
bash
cd <app_name>
-docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
-docker-compose up queue mongo -d
-poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
-pnpm start
bash
cd <app_name>/ui
-pnpm dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \\
-  -A \\
-  -R 3130:localhost:3030 \\
-  -R 28017:localhost:27017 \\
-  -R 5772:localhost:5672 \\
-  bioloopuser@workers.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
-colo23> git checkout dev
-colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
-colo23> poetry install
-colo23> poetry shell
-colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1

Dataset Name:

  • taken from the name of the directory ingested
  • used in watch.py to filter out registered datasets
  • used to compute the staging path staging_dir / alias / dataset['name']
  • used to compute the qc path Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'
  • used to compute the scratch tar path while downloading the tar file from SDA Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')
`,45),n=[l];function o(p,h,r,d,c,k){return e(),i("div",null,n)}const F=s(t,[["render",o]]);export{u as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.lean.js b/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.lean.js deleted file mode 100644 index 5af9bd6a3..000000000 --- a/docs/.vitepress/dist/assets/worker_index.md.r1Oa1Oe7.lean.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as s,c as i,o as e,V as a}from"./chunks/framework.MXVb71fM.js";const u=JSON.parse('{"title":"Workers","description":"","frontmatter":{},"headers":[],"relativePath":"worker/index.md","filePath":"worker/index.md"}'),t={name:"worker/index.md"},l=a("",45),n=[l];function o(p,h,r,d,c,k){return e(),i("div",null,n)}const F=s(t,[["render",o]]);export{u as __pageData,F as default}; diff --git a/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.js b/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.js new file mode 100644 index 000000000..46ea9c9e4 --- /dev/null +++ b/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.js @@ -0,0 +1,21 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Overview","description":"","frontmatter":{"title":"Overview","order":0},"headers":[],"relativePath":"worker/overview.md","filePath":"worker/overview.md","lastUpdated":null}'),l={name:"worker/overview.md"};function n(h,s,p,o,r,k){return e(),a("div",null,s[0]||(s[0]=[t(`

Worker Overview

Coding Guidelines

Hierarchical Config

  • The default & dev config goes into workers/config/common.py
  • The overrides for production goes into workers/config/production.py
  • Based on the environment variable APP_ENV, config from that file is imported and merged with the common config.
    • Add APP_ENV=production to .env file which load_dotenv reads or
    • directly set it as export APP_ENV=production.
  • In project files, import config as from workers.config import config
  • Imported config is a DotMap object, which supports both config[] and config. access.
  • To add a new environment (for example "stage"), create a new file inside workers/config called stage.py and have the overriding config as a dict assigned to a variable named config.

Celery config

  • config specific to Celery is in workers/config/celeryconfig.py
  • Config is in python values, instead of a dict
  • Env specific values and secrets are loaded from .env file

Code Organization

  • Celery Tasks: workers/tasks/*.py
  • Scheduled job and other scripts: workers/scripts/*.py
  • Helper code: workers/*.py
  • Config / settings are in workers/config/*.py and .env
  • Test code is in tests/

Parallel tasks limit

The maximum number of active (i.e. not 'PENDING') tasks that can run at a time is determined by the number of Celery workers, which is currently set to 8.

This config can be found in ecosystem.config.js, under app celery_worker:

-m celery -A workers.celery_app worker ... --autoscale=8,2

Hot Module Replacement

Worker automatically run with updated code except for the code in

  • workers.config.*
  • workers.utils
  • workers.celery_app
  • workers.task.declaration

Deployment

  • Add module load python/3.10.5 to ~/.modules
  • Update .env (make a copy of .env.example and add values)
  • Install dependencies
bash
poetry export --without-hashes --format=requirements.txt > requirements.txt
+pip install -r requirements.txt
bash
cd ~/app/workers
+pm2 start ecosystem.config.js
+# optional
+pm2 save

Testing with workers running on local machine

Start mongo and queue

bash
cd <rhythm_api>
+docker-compose up queue mongo -d

Start Workers

bash
python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q'

--concurrency 1: number of worker processed to pre-fork

-O fair: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks when they are actually available.

Use --hostname '<app_name>-celery-<worker_name>@%h' to distinguish multiple workers running on the same machine either for the same app or different apps.

  • replace <app_name> with app name (ex: bioloop)
  • replace <worker_name> with worker name (ex: w1)

Auto-scaling - max_concurrency,min_concurrency --autoscale=10,3 (always keep 3 processes, but grow to 10 if necessary).

--queues '<app_name>-dev.sca.iu.edu' comma separated queue names. worker will subscribe to these queues for accepting tasks. Configured in workers/config/celeryconfig.py with task_routes, task_default_queue

Run test

bash
python -m tests.test

Testing with workers running on COLO node and Rhythm API

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

  • start postgres locally using docker
bash
cd <app_name>
+docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
+docker-compose up queue mongo -d
+poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
+pnpm start
bash
cd <app_name>/ui
+pnpm dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \\
+  -A \\
+  -R 3130:localhost:3030 \\
+  -R 28017:localhost:27017 \\
+  -R 5772:localhost:5672 \\
+  bioloopuser@workers.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
+colo23> git checkout dev
+colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
+colo23> poetry install
+colo23> poetry shell
+colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1

Dataset Name:

  • taken from the name of the directory ingested
  • used in watch.py to filter out registered datasets
  • used to compute the staging path staging_dir / alias / dataset['name']
  • used to compute the qc path Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'
  • used to compute the scratch tar path while downloading the tar file from SDA Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')
`,49)]))}const g=i(l,[["render",n]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.lean.js b/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.lean.js new file mode 100644 index 000000000..ec2fa0bb7 --- /dev/null +++ b/docs/.vitepress/dist/assets/worker_overview.md.MU1il3VS.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.C9SxlbOG.js";const c=JSON.parse('{"title":"Overview","description":"","frontmatter":{"title":"Overview","order":0},"headers":[],"relativePath":"worker/overview.md","filePath":"worker/overview.md","lastUpdated":null}'),l={name:"worker/overview.md"};function n(h,s,p,o,r,k){return e(),a("div",null,s[0]||(s[0]=[t("",49)]))}const g=i(l,[["render",n]]);export{c as __pageData,g as default}; diff --git a/docs/.vitepress/dist/favicon.ico b/docs/.vitepress/dist/favicon.ico new file mode 100644 index 000000000..162675351 Binary files /dev/null and b/docs/.vitepress/dist/favicon.ico differ diff --git a/docs/.vitepress/dist/hashmap.json b/docs/.vitepress/dist/hashmap.json index d6a0eb272..3f4f085d1 100644 --- a/docs/.vitepress/dist/hashmap.json +++ b/docs/.vitepress/dist/hashmap.json @@ -1 +1 @@ -{"index.md":"RtVxx9Um","architecture.md":"6bDaH79s","pull_request_template.md":"Du47Iz7_","secure_download.md":"3INgcYxh","worker_index.md":"r1Oa1Oe7","install-docker.md":"-cK3qjUI","ui_index.md":"epJP0VJ3","template.md":"1ENxh8wO","ui_auth_explained.md":"Gr83VpDO","ui_util_components.md":"wuKa-Jls","api_index.md":"IzjkZa69"} +{"api_01-core_cluster.md":"BgictSao","api_01-core_configuration.md":"CKqmrnT_","api_01-core_cron-task-scheduling.md":"Dm3QHNiN","api_01-core_error-handling.md":"C54wHFwA","api_01-core_events.md":"CZaq614g","api_01-core_index.md":"C4HMl5sN","api_01-core_lifecycle-hooks.md":"ByaWgTP1","api_01-core_queue.md":"DrnDJfm4","api_01-core_validation.md":"CumxzlHO","api_02-data_cache.md":"BTpvCcGr","api_02-data_index.md":"Boa6RJkG","api_02-data_prisma.md":"BUPmvYLB","api_03-security_authentication.md":"C9rOmhTI","api_03-security_authorization.md":"BzhJzT5t","api_03-security_cookies.md":"DdGBHNzP","api_03-security_cors.md":"D0kQQrT6","api_03-security_index.md":"iFnvHb70","api_04-deployment_docker-image-design.md":"C59MbThu","api_04-deployment_index.md":"jH9Gr9hU","api_05-performance_compression.md":"fzQEzXTQ","api_05-performance_http-caching.md":"B8lPsk0r","api_05-performance_index.md":"BSpkGXQl","api_05-performance_instrumentation.md":"DG3ieIXj","api_05-performance_nodejs_metrics.md":"BOsU2dK9","api_06-integrations_api-clients.md":"Dsrc0cL2","api_06-integrations_index.md":"DTDGGRYD","api_06-integrations_swagger-openapi.md":"D81zKQ3O","api_07-development_auto-reload-server.md":"BS8klrx_","api_07-development_db-seed-data.md":"DqPXHVVw","api_07-development_index.md":"DLjX-kjo","api_07-development_linting.md":"ScDHITP5","api_index.md":"DLHSSN37","api_introduction.md":"DohIzYo2","architecture.md":"PvRzkUtL","index.md":"DsfENmNR","installation_index.md":"DgJxg3Lo","installation_install-docker.md":"C33pKkyI","installation_install-local.md":"DZiWe0sy","metrics.md":"mr45A369","pull_request_template.md":"DoTLC-PI","secure_download.md":"CSwnByz7","template.md":"ByWSB8Nr","ui_auth_explained.md":"Gj-L3Q-6","ui_index.md":"C4WOtbRb","ui_overview.md":"DxPi67l2","ui_util_components.md":"MrZXUT6Q","upload.md":"zCMbS6HI","welcome-message.md":"B_3Ps02e","worker_index.md":"BPwBHMMF","worker_overview.md":"MU1il3VS"} diff --git a/docs/.vitepress/dist/index.html b/docs/.vitepress/dist/index.html index 681cf3e6d..4408b69e2 100644 --- a/docs/.vitepress/dist/index.html +++ b/docs/.vitepress/dist/index.html @@ -5,20 +5,22 @@ Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Bioloop

Data Management and Archival

Simplify the process of data archival.

BioloopBioloop
- +
Skip to content

BioloopData Management and Archival

Simplify the process of data archival.

BioloopBioloop
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/install-docker.html b/docs/.vitepress/dist/install-docker.html deleted file mode 100644 index 31b6c8686..000000000 --- a/docs/.vitepress/dist/install-docker.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Run via docker | Bioloop - - - - - - - - - - - - - -
Skip to content

Run via docker

Docker standardizes the server environment and makes it easier to get a local development environment set up and running.

Setup

Requires docker. Docker desktop should work too.

For development purposes, shared volumes are used in docker-compose.yml to ensure container node_modules are not confused with host level node_modules. This approach also keeps node_modules folders out of the local directory to make it easier to find and grep.

To make adjustments to the way the application runs, edit and review docker-compose.yml.

The UI and API containers have been set to run on start up to install / update dependencies.

Set up the front-end ui client or back-end api server as needed.

.env files

ui/, api/ and workers/ all contain .env.example files. Copy these to a corresponding .env file and update values accordingly.

cp ui/.env.example ui/.env
-cp api/.env.example api/.env
-cp workers/.env.example workers/.env

OpenSSL

cd ui/
-mkdir .cert
-openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
cd api/keys
-./genkeys.sh

Note: when running under Windows, it may be necessary to run the openssl commands via Cygwin

Setup Migration and Seed Database

Run the initial migration:

docker compose exec api bash
-npx prisma migrate dev

Add any usernames you need to work with in api/prisma/data.js then seed the db:

npx prisma db seed

Starting / Stopping

Use Docker Compose

Bring up the containers:

docker compose up -d

Make sure everything is running (no Exit 1 States):

docker compose ps

To shut down the containers:

docker compose down -v

To see what is going on in a specific container:

docker compose logs -f api

Linting

To use linting with the docker setup you must have the dev dependencies of the api and ui installed locally as well as the VSCode extention ESLint (dbaeumer.vscode-eslint). You will need to run the install command for both api and ui:

npm install --save-dev

You can also install Dev Containers (ms-vscode-remote.remote-containers) to remote into both the api and ui containers separately. You'd have to have two instances of vscode running, but if you don't want to install dependencies locally this is the best way to run with automatic linting.

Testing

Try it out how it is. Open a browser and go to:

https://localhost

You may need to specify the https:// prefix manually. You may also have to accept a warning about an insecure connection since it's a self-signed certificate. The default configuration should be enough to get up and running.

Test the API with:

http://127.0.0.1:3030/health

To make more complex requests, use an API development tool like Hoppscotch or Insomnia:

https://hoppscotch.io/

To POST a request, choose POST, specify the URL, and in Body choose application/x-www-form-urlencoded for the Content Type

Queue

This application makes use of the Rhythm API for managing worker queues.

Queue folders need to belong to docker group

db/queue/
-chown -R ${USER}:docker db/queue/

Quick start

Getting the user permissions set correctly is an important step in making the application run.

bash
bin/dev.sh

Run this script from the project root.

  • It creates a .env file in the project root which has the user id (uid) and group id (gid) of the project root directory's owner. The processes inside the api and worker_api docker containers are run as a user with this UID and GID.
  • It builds both the api and worker_api images
  • It runs all the containers (ui, api, worker_api, queue, postgres, mongo_db)

Troubleshooting

Most containers have curl available. Connect to one and then try making requests to the service you're having issue with.

docker compose exec web bash
-curl -X GET http://api:3030/

(in this case, we don't need the /api suffix since we're behind the nginx proxy that normally adds /api for us)

Also, you can always insert console.log() statements in the code to see what values are at any given point.

You can check which ports are available locally and find something unique.

netstat -panl | grep " LISTEN "

Docker-compose

-f

If you have a compose file named something other than docker-compose.yml, you can specify the name with a -f flag:

docker compose -f docker-compose-prod.yml up -d

TIP: Shortcuts

Create bash aliases

The above commands can get tiring to type every time you want to take action with a compose environment. These shortcuts help.

Add the following to your .bashrc file (or equivalent)

alias dcu='docker compose up -d'
-alias dcd='docker compose down --remove-orphans'
-alias dcp='docker compose ps'
-alias dce='docker compose exec'
-alias dcl='docker compose logs'

via https://charlesbrandt.com/system/virtualization/docker-compose.html#shell-shortcuts

- - - - \ No newline at end of file diff --git a/docs/.vitepress/dist/installation/index.html b/docs/.vitepress/dist/installation/index.html new file mode 100644 index 000000000..6be031605 --- /dev/null +++ b/docs/.vitepress/dist/installation/index.html @@ -0,0 +1,26 @@ + + + + + + Installation | Bioloop + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/installation/install-docker.html b/docs/.vitepress/dist/installation/install-docker.html new file mode 100644 index 000000000..5080d8a89 --- /dev/null +++ b/docs/.vitepress/dist/installation/install-docker.html @@ -0,0 +1,62 @@ + + + + + + Docker | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Installation Guide

This guide will help you set up Bioloop using Docker for local development or production deployment.

Prerequisites

  • Docker Engine or Docker Desktop
  • Node.js 16+ (for local development without Docker)
  • OpenSSL (for generating certificates)
  • Git (for cloning the repository)

Quick Start

  1. Clone the repository and navigate to the project directory
bash
git clone https://github.com/IUSCA/bioloop.git
+cd bioloop
  1. Set up environment files
bash
cp ui/.env.example ui/.env
+cp api/.env.example api/.env
+cp workers/.env.example workers/.env
  1. Generate required certificates
bash
# For UI HTTPS
+cd ui/
+mkdir .cert
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem 
+cd ..
+
+# For API JWT
+cd api/keys
+./genkeys.sh
+cd ../..
  1. Start the services
bash
docker compose up -d

Development Setup Details

The project uses Docker Compose for development with some key features:

  • Shared volumes for node_modules to avoid conflicts with host
  • Hot-reload enabled for UI and API
  • Automatic dependency installation on container startup
  • PostgreSQL database with automatic migrations

Configuration

Environment Variables

Each component requires specific environment variables to be set:

  • UI: Authentication endpoints, API URL
  • API: Database connection, JWT secrets
  • Workers: Queue settings, processing parameters

See the .env.example files in each directory for required variables.

Docker Configuration

The application behavior can be customized by editing:

  • docker-compose.yml - Development setup
  • docker-compose-prod.yml - Production configuration

Database Setup

  1. Run initial migrations:
bash
docker compose exec api bash
+npx prisma migrate dev
  1. Seed the database:
bash
# Edit api/prisma/data.js to add required users first
+npx prisma db seed

Common Operations

Starting Services

bash
docker compose up -d      # Start all services
+docker compose up ui api  # Start specific services

Checking Status

bash
docker compose ps         # List container status
+docker compose logs -f    # Follow all logs
+docker compose logs api   # View API logs

Stopping Services

bash
docker compose down       # Stop and remove containers
+docker compose down -v    # Also remove volumes

Development Tools

Code Linting

Two options for ESLint integration:

  1. Local Installation:
bash
# Install dev dependencies locally
+cd api && npm install --save-dev
+cd ../ui && npm install --save-dev
+
+# Install VSCode ESLint extension
+code --install-extension dbaeumer.vscode-eslint
  1. Using Dev Containers:
  • Install VSCode Dev Containers extension
  • Open API and UI folders in separate VSCode windows
  • Reopen in container when prompted

Testing

  1. UI Testing:
bash
# Access the UI
+open https://localhost

Note: Accept the self-signed certificate warning

  1. API Testing:
bash
# Check API health
+curl http://127.0.0.1:3030/health
+
+# For complex API testing, use:
+- Hoppscotch (https://hoppscotch.io)
+- Insomnia
+- Postman

Queue System

The application uses Rhythm API for task queues.

Setup queue permissions:

bash
sudo chown -R ${USER}:docker db/queue/

Troubleshooting Guide

Common Issues

  1. Container Access:
bash
docker compose exec web bash
+curl -X GET http://api:3030/
  1. Port Conflicts:
bash
netstat -panl | grep " LISTEN "
  1. Logs:
bash
docker compose logs -f service_name

Development Tips

  1. Use the quick start script:
bash
bin/dev.sh

This handles:

  • User permissions setup
  • Image building
  • Container orchestration
  1. Useful Docker Compose Aliases:
bash
# Add to ~/.bashrc
+alias dcu='docker compose up -d'
+alias dcd='docker compose down --remove-orphans'
+alias dcp='docker compose ps'
+alias dce='docker compose exec'
+alias dcl='docker compose logs'
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/installation/install-local.html b/docs/.vitepress/dist/installation/install-local.html new file mode 100644 index 000000000..e385165e7 --- /dev/null +++ b/docs/.vitepress/dist/installation/install-local.html @@ -0,0 +1,63 @@ + + + + + + Local | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Local Installation Guide

This guide provides step-by-step instructions for setting up and running the Bioloop application on your local machine.

Overview

The Bioloop application consists of several components:

  • API server
  • Web UI
  • Worker processes
  • PostgreSQL database

This guide covers the installation and configuration of all these components in a local development environment.


Steps to setup API and run natively on development machine (not using docker)

  • Create .env file
  • Generate token signing key pair
  • Install dependencies
  • Generate API Doc
bash
cd api/
+cp .env.example .env
+
+cd keys
+./genkeys.sh
+cd ..
+
+npm install && npm install --save-dev
+npm run swagger

This step is required only if you are working with workflows, otherwise you can set WORKFLOW_AUTH_TOKEN to any value and API calls to Rhythm API will fail but the App will still run.

Generate an access token to connect to the Rhythm API.

  • Go to Rhythm API instance (local or deployed)- cd <rhythm_api>
  • If rhythm api is running locally: python -m rhythm_api.scripts.issue_token --sub <app-id>
  • If rhythm api is running in docker: sudo docker compose -f "docker-compose-prod.yml" exec api python -m rhythm_api.scripts.issue_token --sub <app-id>

Make these changes to the api/.env file:

bash
NODE_ENV=default
+WORKFLOW_AUTH_TOKEN=<token>
+DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"
  • Initialize database
  • Set up schema
  • Populate with dummy data
bash
docker-compose up -d postgres
+cd api/
+npx prisma db push
+npx prisma db seed

Start the server: npm run start

Steps to setup UI and run natively on development machine (not using docker)

  • Create .env file
  • Create self-signed certificate for https://localhost
  • install dependencies
bash
cd ui/
+cp .env.example .env
+
+mkdir .cert
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
+
+npm install && npm install --save-dev

Make these changes to ui/.env file:

  • change the hostname in VITE_API_REDIRECT_URL to localhost
bash
VITE_API_REDIRECT_URL=http://localhost:3030

Start the vite server: npm run dev

Set Up Workers locally

Running workers on your dev machine has limitations:

  • Cannot work with SDA - cannot install hsi on dev machine.
  • Difficult to test with large files (~100GB)
  • Workers run external commands - tar, fastqc, multiqc whose interface and behavior may change between OS platforms.
  • Working with mounted file systems (Slate Scratch, and others) has its own quirks which you will not encounter on your dev machine.

Steps:

  • Install miniconda

  • Create a virtual environment: conda create -n colo python=3.9

    • The production servers colo23, colo25 have python version 3.9.8 installed (as of June 2023). If the default python version in the production servers change, update it in your development machine too.
  • Activate it: conda activate colo

  • Install poetry - pip install -U poetry

  • Install dependencies - poetry install

    • Poetry will detect it is running in a virtual environment and won't create another/
  • Create .env

bash
cd workers
+cp .env.example .env
  • Generate an auth token to access the app api and add it to .env against AUTH_TOKEN.
bash
cd api/
+node src/scripts/issue_token.js svc_tasks
  • Workers connect to the mongodb and rabbitmq of a Rhythm API instance. You can either setup a Rhythm API instance locally or connect to core-dev1.sca.iu.edu using Group VPN (This option is not recommended as it is used for production now. We plan to use core.sca.iu.edu for production in the future.)

  • Update paths in config for local development: TODO

  • Stat celery:

bash
cd workers
+workers/scripts/start_celery.sh

Setup a Test Instance of Workers in colo nodes

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

image

  • start postgres locally using docker
bash
cd <app_name>
+docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
+docker-compose up queue mongo -d
+poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
+npm run start
bash
cd <app_name>/ui
+npm run dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \
+  -A \
+  -R 3130:localhost:3030 \
+  -R 28017:localhost:27017 \
+  -R 5772:localhost:5672 \
+  bioloopuser@colo23.carbonate.uits.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
+colo23> git checkout dev
+colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
+colo23> poetry install
+colo23> poetry shell
+colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/metrics.html b/docs/.vitepress/dist/metrics.html new file mode 100644 index 000000000..6fff1de9d --- /dev/null +++ b/docs/.vitepress/dist/metrics.html @@ -0,0 +1,26 @@ + + + + + + Metrics and Monitoring | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Metrics and Monitoring

This project uses Prometheus and Grafana for monitoring and visualizing application performance metrics. These tools are essential for understanding system behavior, identifying bottlenecks, and ensuring the application runs smoothly in production.

Prometheus

Prometheus is a powerful monitoring system that collects and stores metrics from various sources. It is configured to scrape metrics from the following targets:

  • Database (Postgres): Metrics are exposed via postgres_exporter, which is defined in docker-compose.yml.
  • Node.js and Express.js app: Metrics are exposed via the prom-client library.

Configuration

  • Service Definition: The Prometheus service is defined in docker-compose.yml.
  • Configuration File: Located at metrics/prometheus/config/prometheus.yml.

Without Prometheus

Without Prometheus, there would be no centralized system to collect and store metrics, making it difficult to monitor application performance or detect issues in real-time.

Integration

Prometheus runs in the same Docker network as the Node.js app and Postgres database, allowing it to scrape metrics directly from their endpoints.

Grafana

Grafana is used to visualize the metrics collected by Prometheus. It provides dashboards that make it easy to analyze system performance and identify trends.

Features

  • Pre-configured Dashboards:
    • Node.js app metrics
    • Postgres metrics
  • Datasource: Automatically configured to use Prometheus as the datasource.
  • Access: Accessible at https://localhost/grafana/. Only users with the admin role can access it.

Configuration

  • Service Definition: The Grafana service is defined in docker-compose.yml.
  • Configuration Files:
    • metrics/grafana/config/grafana.ini: Contains Grafana server and authentication settings.
    • metrics/grafana/provisioning/datasources: Configures Prometheus as the datasource.
    • metrics/grafana/provisioning/dashboards: Defines the dashboards to be imported.

Authentication and Authorization

  • JWT Authentication: Grafana uses JWT tokens for authentication.
    • Admin users receive a secure, HTTPS-only cookie containing the JWT token.
    • The reverse proxy forwards this token to Grafana as a header (X-JWT-Assertion).
  • Reverse Proxy:
    • In development: Vite is used as the reverse proxy (ui/vite.config.js).
    • In production: Nginx is used as the reverse proxy (nginx/conf/app.conf).

Integration

Grafana is integrated into the same Docker network as Prometheus, ensuring seamless access to metrics.

How This Setup Helps

  • Centralized Monitoring: Prometheus collects metrics from multiple sources, while Grafana visualizes them in a single interface.
  • Maintainability: The configuration files are modular and well-organized, making it easy to update or extend the setup.
  • Real-time Insights: Developers can monitor application performance in real-time, enabling faster debugging and optimization.

Usage Instructions

  1. Start the Services: Ensure Docker is running and start the services using:

    bash
    docker-compose up -d
  2. View Dashboards:

    • Log into bioloop with the admin credentials.
    • In the sidebar, click on Metrics to access the Grafana dashboards screen.
    • Navigate to the pre-configured dashboards for Node.js and Postgres metrics.
  3. Add New Metrics:

    • For Node.js: See Instrumentation to add custom metrics.
    • For Postgres: Update the queries.yml file in metrics/postgres_exporter/.
  4. Restart Services: After making changes to configurations, restart the affected services:

    bash
    docker-compose restart <service_name>

This setup ensures a robust monitoring system that is easy to maintain and extend as the application grows.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/pull_request_template.html b/docs/.vitepress/dist/pull_request_template.html index 3a4eb3821..5ec37c7c0 100644 --- a/docs/.vitepress/dist/pull_request_template.html +++ b/docs/.vitepress/dist/pull_request_template.html @@ -3,22 +3,24 @@ - Bioloop + Contributing | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Description

Please provide a brief description of the changes made in this PR.

Related Issue(s)

Closes #[Issue Number]

If applicable, please reference the issue(s) that this PR addresses. If the PR does not address any specific issue, you can remove this section.

Changes Made

List the main changes made in this PR. Be as specific as possible.

  • [ ] Feature added
  • [ ] Bug fixed
  • [ ] Code refactored
  • [ ] Documentation updated
  • [ ] Other changes: [describe]

Screenshots (if applicable)

Provide screenshots or GIFs that visually represent the changes. If not applicable, you can remove this section.

Checklist

Before submitting this PR, please make sure that:

  • [ ] Your code passes linting and coding style checks.
  • [ ] Documentation has been updated to reflect the changes.
  • [ ] You have reviewed your own code and resolved any merge conflicts.
  • [ ] You have requested a review from at least one team member.
  • [ ] Any relevant issue(s) have been linked to this PR.

Additional Information

Add any other information or context that may be relevant to this PR. This can include potential impacts, known issues, or future work related to this change.

- +
Skip to content

Description

Please provide a brief description of the changes made in this PR.

Related Issue(s)

Closes #[Issue Number]

If applicable, please reference the issue(s) that this PR addresses. If the PR does not address any specific issue, you can remove this section.

Changes Made

List the main changes made in this PR. Be as specific as possible.

  • [ ] Feature added
  • [ ] Bug fixed
  • [ ] Code refactored
  • [ ] Tests changed
  • [ ] Documentation updated
  • [ ] Other changes: [describe]

Screenshots (if applicable)

Provide screenshots or GIFs that visually represent the changes. If not applicable, you can remove this section.

Checklist

Before submitting this PR, please make sure that:

  • [ ] Your code passes linting and coding style checks.
  • [ ] Documentation has been updated to reflect the changes.
  • [ ] You have reviewed your own code and resolved any merge conflicts.
  • [ ] You have requested a review from at least one team member.
  • [ ] Any relevant issue(s) have been linked to this PR.

Additional Information

Add any other information or context that may be relevant to this PR. This can include potential impacts, known issues, or future work related to this change.

+ \ No newline at end of file diff --git a/docs/.vitepress/dist/secure_download.html b/docs/.vitepress/dist/secure_download.html index c82d89625..429b5ed1a 100644 --- a/docs/.vitepress/dist/secure_download.html +++ b/docs/.vitepress/dist/secure_download.html @@ -3,22 +3,25 @@ - Secure Download Documentation | Bioloop + Secure Download | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Secure Download Documentation

Table of Contents

  • Introduction
  • Requirements
  • Staging the Dataset
  • Access Control
  • Architecture Overview
  • Downloading a Dataset File

1. Introduction

This document provides an overview of the Secure Download functionality, detailing the requirements and architecture of the system. The primary goal is to allow authorized users to download dataset files both directly from the browser and from Slate Scratch while ensuring strict access control to prevent unauthorized access.

2. Requirements and Limitations

Users with access to the dataset should be able to download dataset files directly from their web browsers. The file download link should be accessible only to users with the necessary permissions.

Dataset files must be staged before attempting to download.

The staged files should be protected from unauthorizzed users who have access to slate scratch from navigating and/or downloading dataset files.

2.1 Limitations

The UI and API are running on a node where the Slate Scratch path is not mounted, preventing direct file access. An Nginx server is hosted on the colo node, which has access to the staged files. However, configuring Nginx as a simple file server would allow anyone to access any files. However, the download file server cannot determine users'access because the access control data, specifying which users have access to which datasets and files, is maintained by the API in a PostgreSQL database.

3. Staging the Dataset

The Staging Dataset functionality allows users to request the download of dataset files through the UI or API. Staging a dataset involves the transfer of the requested dataset file from the SDA (Source Data Archive) to a temporary location known as the Slate Scratch path. This intermediate step ensures efficient and secure access to the dataset while preventing unauthorized access through path enumeration.

Staging Process:

  • User Request: Users initiate a dataset file download request through either the user interface (UI) or the application programming interface (API).

  • Rhythm Workflow: The request triggers a rhythm workflow "Stage" on the colo node where the celery tasks are registered. "stage_dataset", "validate_dataset", "setup_dataset_download" are the celery tasks involved in this workflow.

  • Path Randomization: During dataset staging, a random string, in the form of a universally unique identifier (UUID) called stage_alias, is incorporated into the path. This UUID is generated to limit access to datasets through path enumeration. The staging path follows the pattern: <stage_directory>/<dataset_stage_alias>/<dataset_name>. "stage_dataset" celery task downloads the dataset tar from SDA and extarcts to this randomized path.

Example:

/stage_directory/6a5b1734-98e8-4e47-ae5f-1a9e5d8d9f7c/dataset_name
  • Symlink Creation: A symlink <download_path>/<dataset_stage_alias> is created to point to <stage_directory>/<dataset_stage_alias>/<dataset_name>. This will be path given to the users who want to download the data from the Slate Scratch directly. The file download nginx server is configured to server files from the <download_path> directory.

To enhance security, the access control list (ACL) of the <stage_directory>/<dataset_stage_alias> directory is carefully configured. It is limited to granting only the "execute" permission bit (--x) for the "others" group. This permission setup ensures that users with access to the Slate Scratch path cannot browse or navigate through all datasets present in the directory. Instead, only users who possess the complete path <stage_directory>/<dataset_stage_alias>/<dataset_name> are allowed to read the contents of the staged dataset.

UUID Generation

The UUID used in the staging path is generated deterministically. It is a function of the dataset type, dataset name, and a salt string. This deterministic approach ensures consistency and allows authorized users to access the staged dataset when needed, while making it computationally infeasible for users to guess the path of other datasets.

By implementing these measures, the Staging Dataset functionality maintains data security and privacy, preventing unauthorized access and ensuring the integrity of the staged datasets.

4. Access Control

Access control is managed through the project membership and user roles by the API. Users of "operator" and "admin" roles can access any dataset and its files. Users with "user" can only access the datasets that are associated with projects the user is a member of. This role-based access control ensures that sensitive dataset information is only accessible to those who have been explicitly granted access rights.

To further enhance security and prevent unauthorized access to dataset files, a secure ephemeral resource bound URL is constructed. This URL contains critical components to ensure its validity and security: Components of the Secure URL:

  • File Path: The URL contains the file path relative to the download directory of the dataset file that needs to be downloaded.
  • JWT (JSON Web Token): A very short-lived JWT is included as part of the URL. The JWT payload includes the file path as one of its components.

URL Validation:

For the URL to be considered valid, the following conditions must be met:

  • JWT Validity: The JWT included in the URL must be valid, meaning it must have a valid signature and must not be expired.
  • Path Match: The file path inside the JWT payload should match the file path specified in the path section of the URL.

By enforcing these conditions, the system ensures that even if an unauthorized user somehow obtains a link to a dataset file sometime later, they cannot access it. It also makes sure that a token cannot be used to download other files even if it is unexpired and has a valid signature.

5. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the file browser view to initiate file downloads.
  • API: This node serves the user interface and API endpoints with user and dataset metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Workers / Colo Node: Staging of dataset files occurs on this node via a Celery task. The node also hosts an Nginx server with access to the staged files.
  • Signet: An OAuth server supporting client credential flow with download file scope to create secure tokens.
  • File Download Server (Nginx): A file server on the colo adjacent to data which recieves requests from users to download large dataset files.
  • Secure Download API: A lightweight app with one endpoint that validates the incoming requests to the file download server

6. Downloading a Dataset File

To download a dataset file:

  • Step 1. An authorized user logs in through the UI and requests to download a dataset file.
  • Steps 2,3. The API checks if the user has access to the requested file by querying the database and constructs the download file path.
  • Step 4,5. Requests the Signet oauth server for a download token with the file path
  • Step 6. Responds to UI with the download file url and download token.
  • Step 7. UI Client constructs a URL by including the download token as a query parameter and requests the file download server.
  • Step 8. File server forwards the request to SecureDownloadAPI for validation.
  • Step 9.10. SecureDownloadAPI fetches the public JWT verfication keys from the Signet Oauth server in the form of JWKS and perform URL validation as described in section 4.
  • Step 11. If it is valid, it responds to file server (Nginx) with a special header x-accel-redirect with a value of the path of the requested file.
  • Step 12. Nginx performs internal redirect and sends the requested file to the UIclient with headers invoking a browser file download.
- +
Skip to content

Secure Download

Table of Contents

  • Introduction
  • Requirements
  • Staging the Dataset
  • Access Control
  • Architecture Overview
  • Downloading a Dataset File

1. Introduction

This document provides an overview of the Secure Download functionality, detailing the requirements and architecture of the system. The primary goal is to allow authorized users to download dataset files both directly from the browser and from Slate Scratch while ensuring strict access control to prevent unauthorized access.

2. Requirements and Limitations

Users with access to the dataset should be able to download dataset files directly from their web browsers. The file download link should be accessible only to users with the necessary permissions.

Dataset files must be staged before attempting to download.

The staged files should be protected from unauthorized users who have access to slate scratch from navigating and/or downloading dataset files.

2.1 Limitations

The UI and API are running on a node where the Slate Scratch path is not mounted, preventing direct file access. An Nginx server is hosted on the colo node, which has access to the staged files. However, configuring Nginx as a simple file server would allow anyone to access any files. However, the download file server cannot determine users'access because the access control data, specifying which users have access to which datasets and files, is maintained by the API in a PostgreSQL database.

3. Staging the Dataset

The Staging Dataset functionality allows users to request the download of individual dataset files (or the entire dataset, as a bundled file) through the UI or API. Staging a dataset involves the transfer of the corresponding archived bundle from the SDA (Source Data Archive) to a temporary location known as the Slate Scratch path. This intermediate step ensures efficient and secure access to the dataset while preventing unauthorized access through path enumeration.

Staging Process:

  • User Request: Users initiate a dataset file/bundle download request through either the user interface (UI) or the application programming interface (API).

  • Rhythm Workflow: The request triggers a rhythm workflow "Stage" on the colo node where the celery tasks are registered. "stage_dataset", "validate_dataset", "setup_dataset_download" are the celery tasks involved in this workflow.

  • Path Randomization: During dataset staging, the path of the staged dataset files/bundle are obfuscated through the means of a random universally unique identifier (UUID) called stage_alias. This UUID is generated to limit access to datasets through path enumeration.

    • The stage_dataset celery task downloads the dataset bundle from SDA, and extracts the dataset files and the bundle to their corresponding randomized paths.
      • The dataset's staging path follows the pattern: <stage_directory>/<dataset_stage_alias>/<dataset_name>.
      • The bundle's staging path follows the pattern: <bundle_stage_directory>/<dataset_bundle_name>.

Example:

/stage_directory/6a5b1734-98e8-4e47-ae5f-1a9e5d8d9f7c/dataset_name
+/bundle_stage_directory/bundle_name
  • Symlink Creation: Two symlinks are created to facilitate downloads.
    • <download_path>/<dataset_stage_alias>. This points to <stage_directory>/<dataset_stage_alias>/<dataset_name>. This will be path given to the users who want to download the dataset files from the Slate Scratch directly.
    • <download_path>/<dataset_bundle_name>. This points to <bundle_stage_directory>/<dataset_name>. This will be path given to the users who want to download the dataset as a bundle from the Slate Scratch directly.

The file download nginx server is configured to serve files from the <download_path> directory.

To enhance security, the access control list (ACL) of the <stage_directory>/<dataset_stage_alias> directory is carefully configured. It is limited to granting only the "execute" permission bit (--x) for the "others" group. This permission setup ensures that users with access to the Slate Scratch path cannot browse or navigate through all datasets present in the directory. Instead, only users who possess the complete path <stage_directory>/<dataset_stage_alias>/<dataset_name> are allowed to read the contents of the staged dataset.

UUID Generation

The UUIDs used in the staging paths of the dataset and the bundle are generated deterministically. They are a function of the dataset type, dataset (or bundle) name, and a salt string. This deterministic approach ensures consistency and allows authorized users to access the staged dataset/bundle when needed, while making it computationally infeasible for users to guess the path of other datasets.

By implementing these measures, the Staging Dataset functionality maintains data security and privacy, preventing unauthorized access and ensuring the integrity of the staged datasets.

4. Access Control

Access control is managed through the project membership and user roles by the API. Users of "operator" and "admin" roles can access any dataset and its files. Users with "user" can only access the datasets that are associated with projects the user is a member of. This role-based access control ensures that sensitive dataset information is only accessible to those who have been explicitly granted access rights.

To further enhance security and prevent unauthorized access to dataset files, a secure ephemeral resource bound URL is constructed. This URL contains critical components to ensure its validity and security: Components of the Secure URL:

  • File Path: The URL contains the file path relative to the download directory of the dataset file that needs to be downloaded.
  • JWT (JSON Web Token): A very short-lived JWT is included as part of the URL. The JWT payload includes the file path as one of its components.

URL Validation:

For the URL to be considered valid, the following conditions must be met:

  • JWT Validity: The JWT included in the URL must be valid, meaning it must have a valid signature and must not be expired.
  • Path Match: The file path inside the JWT payload should match the file path specified in the path section of the URL.

By enforcing these conditions, the system ensures that even if an unauthorized user somehow obtains a link to a dataset file sometime later, they cannot access it. It also makes sure that a token cannot be used to download other files even if it is unexpired and has a valid signature.

5. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the file browser view to initiate file downloads.
  • API: This node serves the user interface and API endpoints with user and dataset metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Workers / Colo Node: Staging of dataset files occurs on this node via a Celery task. The node also hosts an Nginx server with access to the staged files.
  • Signet: An OAuth server supporting client credential flow with download file scope to create secure tokens.
  • File Download Server (Nginx): A file server on the colo adjacent to data which receives requests from users to download large dataset files.
  • Secure Download API: A lightweight app with one endpoint that validates the incoming requests to the file download server

6. Downloading a Dataset File

To download a dataset file:

  • Step 1. An authorized user logs in through the UI and requests to download a dataset file.
  • Steps 2,3. The API checks if the user has access to the requested file by querying the database and constructs the download file path.
  • Step 4,5. Requests the Signet oauth server for a download token with the file path
  • Step 6. Responds to UI with the download file url and download token.
  • Step 7. UI Client constructs a URL by including the download token as a query parameter and requests the file download server.
  • Step 8. File server forwards the request to SecureDownloadAPI for validation.
  • Step 9.10. SecureDownloadAPI fetches the public JWT verification keys from the Signet Oauth server in the form of JWKS and perform URL validation as described in section 4.
  • Step 11. If it is valid, it responds to file server (Nginx) with a special header x-accel-redirect with a value of the path of the requested file.
  • Step 12. Nginx performs internal redirect and sends the requested file to the UIclient with headers invoking a browser file download.
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/template.html b/docs/.vitepress/dist/template.html index 9bdfd602a..fc2a4ca33 100644 --- a/docs/.vitepress/dist/template.html +++ b/docs/.vitepress/dist/template.html @@ -3,64 +3,29 @@ - Create a repository | Bioloop + Project Template | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Create a repository

Fork this repo IUSCA/<app_name> (only the org owners can do this, ask Charles.)

Turn on issues in the new repo (only repo owners can do this, ask Charles.)

Clone repository

bash
git clone <url>
-cd <project>

Add bioloop as remote

bash
git remote add bioloop git@github.com:IUSCA/bioloop.git
+    
Skip to content

Create a repository

Fork this repo IUSCA/<app_name> (only the org owners can do this, ask Charles.)

Turn on issues in the new repo (only repo owners can do this, ask Charles.)

Clone repository

bash
git clone <url>
+cd <project>

Add bioloop as remote

bash
git remote add bioloop git@github.com:IUSCA/bioloop.git
 
 # to merge updates from bioloop
 # git fetch bioloop
-# git merge bioloop/main

Replace the name "bioloop" with the new project name (<app_name>) in these files:

  • docker-compose.yml and docker-compose-prod.yml: Change "name"
  • ui/src/config.js - Change "appTitle"
  • api/config/default.json and api/config/production.json: Change "app_id", "auth.jwt.iss"
  • workers/workers/config/common.py: Change "app_id" and "service_user"
  • workers/workers/config/production.py and workers/workers/scripts/start_worker.sh: Change "app_id" and "base_url"
  • workers/ecosystem.config.js (line 7): change celery hostname and queues values
  • README.md and workers/README.md: replace the references to bioloop with <app_name>
  • Update content in ui/src/pages/about.vue
  • Create custom logo.svg

Steps to setup API and run natively on development machine (not using docker)

  • Create .env file
  • Generate token signing key pair
  • Install dependencies
  • Generate API Doc
bash
cd api/
-cp .env.example .env
-
-cd keys
-./genkeys.sh
-cd ..
-
-npm install && npm install --save-dev
-npm run swagger

This step is required only if you are working with workflows, otherwise you can set WORKFLOW_AUTH_TOKEN to any value and API calls to Rhythm API will fail but the App will still run.

Generate an access token to connect to the Rhythm API.

  • Go to Rhythm API instance (local or deployed)- cd <rhythm_api>
  • If rhythm api is running locally: python -m rhythm_api.scripts.issue_token --sub <app-id>
  • If rhythm api is running in docker: sudo docker compose -f "docker-compose-prod.yml" exec api python -m rhythm_api.scripts.issue_token --sub <app-id>

Make these changes to the api/.env file:

bash
NODE_ENV=default
-WORKFLOW_AUTH_TOKEN=<token>
-DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public"
  • Initialize database
  • Set up schema
  • Populate with dummy data
bash
docker-compose up -d postgres
-cd api/
-npx prisma db push
-npx prisma db seed

Start the server: npm run start

Steps to setup UI and run natively on development machine (not using docker)

  • Create .env file
  • Create self-signed certificate for https://localhost
  • install dependencies
bash
cd ui/
-cp .env.example .env
-
-mkdir .cert
-openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ./.cert/key.pem -out ./.cert/cert.pem
-
-npm install && npm install --save-dev

Make these changes to ui/.env file:

  • change the hostname in VITE_API_REDIRECT_URL to localhost
bash
VITE_API_REDIRECT_URL=http://localhost:3030

Start the vite server: npm run dev

Set Up Workers locally

Running workers on your dev machine has limitations:

  • Cannot work with SDA - cannot install hsi on dev machine.
  • Difficult to test with large files (~100GB)
  • Workers run external commands - tar, fastqc, multiqc whose interface and behavior may change between OS platforms.
  • Working with mounted file systems (Slate Scratch, and others) has its own quirks which you will not encounter on your dev machine.

Steps:

  • Install miniconda

  • Create a virtual environment: conda create -n colo python=3.9

    • The production servers colo23, colo25 have python version 3.9.8 installed (as of June 2023). If the default python version in the production servers change, update it in your development machine too.
  • Activate it: conda activate colo

  • Install poetry - pip install -U poetry

  • Install dependencies - poetry install

    • Poetry will detect it is running in a virtual environment and won't create another/
  • Create .env

bash
cd workers
-cp .env.example .env
  • Generate an auth token to access the app api and add it to .env against AUTH_TOKEN.
bash
cd api/
-node src/scripts/issue_token.js svc_tasks
  • Workers connect to the mongodb and rabbitmq of a Rhythm API instance. You can either setup a Rhythm API instance locally or connect to core-dev1.sca.iu.edu using Group VPN (This option is not recommended as it is used for production now. We plan to use core.sca.iu.edu for production in the future.)

  • Update paths in config for local development: TODO

  • Stat celery:

bash
cd workers
-workers/scripts/start_celery.sh

Setup a Test Instance of Workers in colo nodes

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

image

  • start postgres locally using docker
bash
cd <app_name>
-docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
-docker-compose up queue mongo -d
-poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
-npm run start
bash
cd <app_name>/ui
-npm run dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \
-  -A \
-  -R 3130:localhost:3030 \
-  -R 28017:localhost:27017 \
-  -R 5772:localhost:5672 \
-  bioloopuser@colo23.carbonate.uits.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
-colo23> git checkout dev
-colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
-colo23> poetry install
-colo23> poetry shell
-colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1
- +# git merge bioloop/main

Replace the name "bioloop" with the new project name (<app_name>) in these files:

  • docker-compose.yml and docker-compose-prod.yml: Change "name"
  • ui/src/config.js - Change "appTitle"
  • api/config/default.json and api/config/production.json: Change "app_id", "auth.jwt.iss"
  • workers/workers/config/common.py: Change "app_id" and "service_user"
  • workers/workers/config/production.py and workers/workers/scripts/start_worker.sh: Change "app_id" and "base_url"
  • workers/ecosystem.config.js (line 7): change celery hostname and queues values
  • README.md and workers/README.md: replace the references to bioloop with <app_name>
  • Update content in ui/src/pages/about.vue
  • Create custom logo.svg
+ \ No newline at end of file diff --git a/docs/.vitepress/dist/ui/auth_explained.html b/docs/.vitepress/dist/ui/auth_explained.html index 1b5f1ef54..98ead93d1 100644 --- a/docs/.vitepress/dist/ui/auth_explained.html +++ b/docs/.vitepress/dist/ui/auth_explained.html @@ -5,20 +5,22 @@ Auth Explained | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Auth Explained

Objectives for Auth module

  1. Enable users to authenticate with IU CAS, which will create a session.
  2. After logging in, redirect users to the URL they originally requested.
  3. Allow users to revisit the app without having to log in again within a certain period of time.
  4. If the session expires between visits, the user will be required to authenticate again.
  5. Ensure that the session does not expire as long as the user remains active on the app.

Overview of Auth Flow

refer to https://kb.iu.edu/d/bfpq

Vue App Code Execution Order

Route Navigation Guard Logic

Login Code flow - Before IU Login

Login Code flow - After IU Login

Code flow for a Revisiting (logged in) User

Code flow for token refresh

On login / initialize, the auth store creates a timer that will invoke refreshToken function five minutes (configurable) before the current token expires. refreshToken requests API for a new token. Once the API responds with a new token and user profile data, refreshToken invokes onLogin, which updates the user and token refs and stores them in the local storage.

Cache Invalidation

Using caches to store data can improve the speed of an application and reduce code complexity. However, if the data is modified, both the cache and the database must be updated in a single transaction; otherwise, the cached data becomes stale.

In the Auth module, there are two caches: the first is the local storage of the user's browser, which the app uses to retrieve the user's information without making an API call. The second cache is the token itself, which is included with every request and contains information that the API uses to perform role-based authorization without needing to query the database for the user's information on every incoming request.

The Write-Through cache strategy is an appropriate approach to update the user data stored in the local storage. When a component needs to update the user data, it invokes a auth store method that requests API. If the API responds with success message it updates the local storage. If the API request fails, the cache is not updated and error is propogated to the component. The component can then choose to retry the request or display an error message to the user.

When data encoded in the token is updated through an API request, a new token is created and returned to the UI as one of the headers in the response. The UI should retrieve this header and use the new token to replace the old token stored in the local storage. In this approach, data consistency is not gauarnteed, as the response may not reach the UI or the UI code encounters an error while processing. For this reason, it's important to include only minimal and seldom updated data in the token.

- +
Skip to content

Auth Explained

Objectives for Auth module

  1. Enable users to authenticate with IU CAS, which will create a session.
  2. After logging in, redirect users to the URL they originally requested.
  3. Allow users to revisit the app without having to log in again within a certain period of time.
  4. If the session expires between visits, the user will be required to authenticate again.
  5. Ensure that the session does not expire as long as the user remains active on the app.

Overview of Auth Flow

refer to https://kb.iu.edu/d/bfpq

Vue App Code Execution Order

Route Navigation Guard Logic

Login Code flow - Before IU Login

Login Code flow - After IU Login

Code flow for a Revisiting (logged in) User

Code flow for token refresh

On login / initialize, the auth store creates a timer that will invoke refreshToken function five minutes (configurable) before the current token expires. refreshToken requests API for a new token. Once the API responds with a new token and user profile data, refreshToken invokes onLogin, which updates the user and token refs and stores them in the local storage.

Cache Invalidation

Using caches to store data can improve the speed of an application and reduce code complexity. However, if the data is modified, both the cache and the database must be updated in a single transaction; otherwise, the cached data becomes stale.

In the Auth module, there are two caches: the first is the local storage of the user's browser, which the app uses to retrieve the user's information without making an API call. The second cache is the token itself, which is included with every request and contains information that the API uses to perform role-based authorization without needing to query the database for the user's information on every incoming request.

The Write-Through cache strategy is an appropriate approach to update the user data stored in the local storage. When a component needs to update the user data, it invokes a auth store method that requests API. If the API responds with success message it updates the local storage. If the API request fails, the cache is not updated and error is propogated to the component. The component can then choose to retry the request or display an error message to the user.

When data encoded in the token is updated through an API request, a new token is created and returned to the UI as one of the headers in the response. The UI should retrieve this header and use the new token to replace the old token stored in the local storage. In this approach, data consistency is not gauarnteed, as the response may not reach the UI or the UI code encounters an error while processing. For this reason, it's important to include only minimal and seldom updated data in the token.

+ \ No newline at end of file diff --git a/docs/.vitepress/dist/ui/index.html b/docs/.vitepress/dist/ui/index.html index 655bbf881..71e2a93d3 100644 --- a/docs/.vitepress/dist/ui/index.html +++ b/docs/.vitepress/dist/ui/index.html @@ -5,118 +5,22 @@ UI | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

UI

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on VITE_API_REDIRECT_URL (ex: http://localhost:3000) as GET http://localhost:3000/users.

Running using docker

  1. Set VITE_API_REDIRECT_URL to http://api:3000
  2. From the project root run: docker composer up ui -d and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI)

Running on host machine

  1. set VITE_API_REDIRECT_URL to the API base url (ex: http://localhost:3000)
  2. Install modules: pnpm install
  3. Start dev server: pnpm dev

Features

Icons

There are multiple ways to include icons:

  • Material Icons are provided by Vuestic. Iconify icon components are auto imported.
    • usage: <va-icon name="dashboard" />
  • Iconify has a lot of third party / community icons
    • usage: <Icon icon="mdi-flask" class="text-2xl" />
    • usage: <i-mdi-flask/>

Iconify icons are installed using

Colors

Vuestic colors: https://ui.vuestic.dev/en/styles/colors

Accent

  • primary
  • secondary
  • success
  • warning
  • danger
  • info

Background

  • backgroundPrimary
  • backgroundSecondary
  • backgroundElement
  • backgroundBorder

Text

  • textPrimary
  • textInverted

Using

html
<template>
-  <!-- use javascript object -->
-  <div :style="color: {{ colorByStatus }}"></div>
-
-  <!-- use css variables -->
-  <span style="color: var(--va-warning)"> </span>
-
-  <!-- using style block -->
-  <p class="title">
-    Title
-  </p>
-
-  <!-- use builtin props -->
-  <va-button color="info"></va-button>
-</template>
-
-<script setup>
-  import { useColors } from "vuestic-ui";
-  const colors = useColors()
-
-  const colorByStatus = status == 'FAILED' ? colors.danger : color.primary
-</script>
-
-<style scoped>
-.title {
-  color: var(--va-primary)
-}
-</style>

Notable Vuestic Classes

CSS: bioloop/ui/node_modules/vuestic-ui/dist/styles/index.css

va-text-text-primary
-va-text-text-inverted
-va-text-{primary, secondary, warninig, success, danger, info}
-
-va-code-text
-va-code-snippet
-
-va-link
-va-link-secondary
-
-va-blockquote
-va-text-block
-va-text-truncate
-va-text-highlighted

Configuration

Layered and Hierarchical config system:

  • Config for multiple environment is managed through Env variables specified in .env files.
  • Based on the mode, these environment variables are automatically imported into the code by vite.
  • The values from the environment variables is merged with other static config centrally in config.js. All environment variables are backed by sensible default values.

To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable.

javascript
{
-  ...,
-  foobar: import.meta.env.FOOBAR || 120,
-}

Add the name and value of the environment variable to the .env file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called .env.example is maintained. This file contains all the variables defined in .env without the values.

Authentication

Users are authenticated using IU CAS. More on auth module.

By default any page will require user authentication

html
<route lang="yaml">
-meta:
-  title: Dashboard
-</route>

Page requires user authentication + role constrained.

html
<route lang="yaml">
-meta:
-  title: Dashboard
-  requiresRoles: ['operator', 'admin']
-</route>

Only users with either operator or admin role can access this page

No authentication, anonymous view

html
<route lang="yaml">
-meta:
-  title: Dashboard
-  requiresAuth: false
-</route>

Utility Components

Vue Components developed in house to be reused in the app. Documentation

Coding Conventions

  • Use custom component names as <CustomComponent>

Adding Additional Fonts

  • Search for fonts on https://fontsource.org/
  • Install - npm install @fontsource/audiowide
  • Add import '@fontsource/audiowide'; in main.js
  • Add 'Audiowide' to font-family: in body styles in base.css

Dates and Times

  • All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone
  • datetime module is used to consolidate the various date and time formats to use in the UI.
  • Use browser's local time zone to show date and time whenever possible.

Usage:

javascript
import * as datetime from '@/services/datetime.js'
-
-datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023"
-datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00"
-
-datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago"
-datetime.readableDuration(130*1000) // "2 minutes"
-datetime.formatDuration(12000 * 1000) // "3h 20m"

If you have a usecase to display in formats other than above in more than one component, add a function to datetime service and use it.

To set static nav links for a page /page1/page2, add nav attr to route meta config block

html
<route lang="yaml">
-meta:
-  title: Users
-  requiresRoles: ["operator", "admin"]
-  nav: [{ label: "Users" }]
-</route>

Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled.

html
<script setup>
-import { useNavStore } from "@/stores/nav";
-const nav = useNavStore();
-nav.setNavItems([], false);
-</script>

To set dynamic nav links for a page /page-dyn-1/page-dyn-2

html
<script setup>
-import { useNavStore } from "@/stores/nav";
-const nav = useNavStore();
-
-page1Promise = api.getP1()
-page2Promise = api.getP2()
-Promise.all([page1Promise, page2Promise]).then(results => {
-  const page1 = results[0]
-  const page2 = results[1]
-  nav.setNavItems([
-    {
-      label: page1.name,
-      to: "/page1"
-    },
-    {
-      label: page2.name
-    },
-  ]);
-})
-</script>

HTTP API Error Handling and Notifications

API requests are to be made with axios.

Catch the error

javascript
import toast from "@/services/toast";
-
-getRecords()
-  .then((res) => {...})
-  .catch((err) => {
-    if (err?.response?.status == 404)
-        toast.info("No datasets");
-    else toast.error("Could not fetch datatset");
-  })

or let someone else handle the dirty work

javascript
getRecords()
-  .then((res) => {...})

Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc.

- + + \ No newline at end of file diff --git a/docs/.vitepress/dist/ui/overview.html b/docs/.vitepress/dist/ui/overview.html new file mode 100644 index 000000000..e861b2dda --- /dev/null +++ b/docs/.vitepress/dist/ui/overview.html @@ -0,0 +1,142 @@ + + + + + + Overview | Bioloop + + + + + + + + + + + + + + + +
Skip to content

UI Overview

Getting Started

Create a .env file from the template .env.example:

bash
cp .env.example .env

and populate the config values to all the keys.

In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on VITE_API_REDIRECT_URL (ex: http://localhost:3000) as GET http://localhost:3000/users.

Running using docker

  1. Set VITE_API_REDIRECT_URL to http://api:3000
  2. From the project root run: docker composer up ui -d and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI)

Running on host machine

  1. set VITE_API_REDIRECT_URL to the API base url (ex: http://localhost:3000)
  2. Install modules: pnpm install
  3. Start dev server: pnpm dev

Features

Icons

There are multiple ways to include icons:

  • Material Icons are provided by Vuestic. Iconify icon components are auto imported.
    • usage: <va-icon name="dashboard" />
  • Iconify has a lot of third party / community icons
    • usage: <Icon icon="mdi-flask" class="text-2xl" />
    • usage: <i-mdi-flask/>

Iconify icons are installed using

Colors

Vuestic colors: https://ui.vuestic.dev/en/styles/colors

css
:host {
+  --va-text-selected: #b3d4fc;
+  --va-text-highlighted: #ffc5274e;
+  --va-link-color: var(--va-primary);
+  --va-link-color-secondary: var(--va-secondary);
+  --va-link-color-hover: var(--va-primary-lighten, --va-primary);
+  --va-link-color-active: var(--va-primary);
+  --va-link-color-visited: var(--va-primary-darken, --va-primary);
+  --va-muted: #7f828b;
+  --va-primary: #154ec1;
+  --va-secondary: #767c88;
+  --va-success: #3d9209;
+  --va-info: #158de3;
+  --va-danger: #e42222;
+  --va-warning: #ffd43a;
+  --va-background-primary: #f6f6f6;
+  --va-background-secondary: #ffffff;
+  --va-background-element: #ebf1f4;
+  --va-background-border: #dee5f2;
+  --va-text-primary: #262824;
+  --va-text-inverted: #ffffff;
+  --va-shadow: rgba(0, 0, 0, .12);
+  --va-focus: #49a8ff;
+}

Using

html
<template>
+  <!-- use javascript object -->
+  <div :style="color: {{ colorByStatus }}"></div>
+
+  <!-- use css variables -->
+  <span style="color: var(--va-warning)"> </span>
+
+  <!-- using style block -->
+  <p class="title">
+    Title
+  </p>
+
+  <!-- use builtin props -->
+  <va-button color="info"></va-button>
+</template>
+
+<script setup>
+  import { useColors } from "vuestic-ui";
+  const colors = useColors()
+
+  const colorByStatus = status == 'FAILED' ? colors.danger : color.primary
+</script>
+
+<style scoped>
+.title {
+  color: var(--va-primary)
+}
+</style>

Notable Vuestic Classes

CSS: bioloop/ui/node_modules/vuestic-ui/dist/styles/css-variables.css

Configuration

Configuration values are used to fine-tune the application's behavior. These values are likely to change between different environments or instances of the application.

We have a layered and hierarchical config system:

  • Config for multiple environment is managed through Env variables specified in .env files.
  • Based on the mode, these environment variables are automatically imported into the code by vite.
  • The values from the environment variables is merged with other static config centrally in config.js. All environment variables are backed by sensible default values.

To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable.

javascript
{
+  ...,
+  foobar: import.meta.env.FOOBAR || 120,
+}

Add the name and value of the environment variable to the .env file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called .env.example is maintained. This file contains all the variables defined in .env without the values.

Constants

Constants are values that remain unchanged across different environments. These can be values like the types of datasets recognized by the system, various dataset states, upload states, texts for alert messages, etc. These are stored in constants.js.


Configuration vs Constants

  1. Mutability - Configuration values may change between environments or during runtime, while constants remain fixed.
  2. Source: Configuration often comes from environment variables, while constants are hardcoded in the application.
  3. Purpose: Configuration is used to adjust application behavior, while constants define fixed aspects of the application.

Authentication

Users are authenticated using IU CAS. More on auth module.

Authentication with google OpenID Connect is implemented following this guide https://developers.google.com/identity/openid-connect/openid-connect

Authentication with CILogon OpenID Connect is implemented following this guide https://www.cilogon.org/oidc

Enable / disable login with authentication providers:

ui/src/config.js

  • "auth_enabled.google": true | false
  • "auth_enabled.cilogon": true | false

api/src/config/default.json

  • "auth.google.enabled": true | false
  • "auth.cilogon.enabled": true | false

Environment Variables:

ui/.env

api/.env

  • GOOGLE_OAUTH_CLIENT_ID=
  • GOOGLE_OAUTH_CLIENT_SECRET=
  • CILOGON_OAUTH_CLIENT_ID=
  • CILOGON_OAUTH_CLIENT_SECRET=

Authentication controls on router

By default any page will require user authentication

html
<route lang="yaml">
+meta:
+  title: Dashboard
+</route>

Page requires user authentication + role constrained.

html
<route lang="yaml">
+meta:
+  title: Dashboard
+  requiresRoles: ['operator', 'admin']
+</route>

Only users with either operator or admin role can access this page

No authentication, anonymous view

html
<route lang="yaml">
+meta:
+  title: Dashboard
+  requiresAuth: false
+</route>

Utility Components

Vue Components developed in house to be reused in the app. Documentation

Coding Conventions

  • Use custom component names as <CustomComponent>

Adding Additional Fonts

  • Search for fonts on https://fontsource.org/
  • Install - npm install @fontsource/audiowide
  • Add import '@fontsource/audiowide'; in main.js
  • Add 'Audiowide' to font-family: in body styles in base.css

Dates and Times

  • All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone
  • datetime module is used to consolidate the various date and time formats to use in the UI.
  • Use browser's local time zone to show date and time whenever possible.

Usage:

javascript
import * as datetime from '@/services/datetime.js'
+
+datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023"
+datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00"
+
+datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago"
+datetime.readableDuration(130*1000) // "2 minutes"
+datetime.formatDuration(12000 * 1000) // "3h 20m"

If you have a usecase to display in formats other than above in more than one component, add a function to datetime service and use it.

Feature Flags

Features can be enabled or disabled at the UI level. Components can determine whether a feature is enabled by reading it from ./config.js, which in turn reads this config from ./.env.

// ./config.js
+
+  ...
+  enabledFeatures: {
+    genomeBrowser: import.meta.env.VITE_ENABLED_GENOME_BROWSER === "true",
+  },
+  ...
# ./.env
+
+VITE_ENABLED_GENOME_BROWSER=true

Reading the feature flag from .env allows for features to be toggled without changing the code.

Once a feature's status has been changed in .env, the app will need to be redeployed for those changes to come into effect.

To set static nav links for a page /page1/page2, add nav attr to route meta config block

html
<route lang="yaml">
+meta:
+  title: Users
+  requiresRoles: ["operator", "admin"]
+  nav: [{ label: "Users" }]
+</route>

Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled.

html
<script setup>
+import { useNavStore } from "@/stores/nav";
+const nav = useNavStore();
+nav.setNavItems([], false);
+</script>

To set dynamic nav links for a page /page-dyn-1/page-dyn-2

html
<script setup>
+import { useNavStore } from "@/stores/nav";
+const nav = useNavStore();
+
+page1Promise = api.getP1()
+page2Promise = api.getP2()
+Promise.all([page1Promise, page2Promise]).then(results => {
+  const page1 = results[0]
+  const page2 = results[1]
+  nav.setNavItems([
+    {
+      label: page1.name,
+      to: "/page1"
+    },
+    {
+      label: page2.name
+    },
+  ]);
+})
+</script>

HTTP API Error Handling and Notifications

API requests are to be made with axios.

Catch the error

javascript
import toast from "@/services/toast";
+
+getRecords()
+  .then((res) => {...})
+  .catch((err) => {
+    if (err?.response?.status == 404)
+        toast.info("No datasets");
+    else toast.error("Could not fetch datatset");
+  })

or let someone else handle the dirty work

javascript
getRecords()
+  .then((res) => {...})

Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc.

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/ui/util_components.html b/docs/.vitepress/dist/ui/util_components.html index 7359feec0..a29bf0e0e 100644 --- a/docs/.vitepress/dist/ui/util_components.html +++ b/docs/.vitepress/dist/ui/util_components.html @@ -5,73 +5,568 @@ Utility Components | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Utility Components

Auto Complete

Basic Usage

html
<template>
-<AutoComplete
-  :data="datasets"
-  filter-by="name"
-  placeholder="Search datasets"
-  @select="handleDatasetSelect"
-/>
+    
Skip to content

Utility Components

AutoComplete

Basic Usage

html

+<template>
+  <AutoComplete
+    :data="datasets"
+    filter-by="name"
+    placeholder="Search datasets"
+    @select="handleDatasetSelect"
+  />
 </template>
 
 <script setup>
-  const datasets = [{name: 'dataset-1'},{name: 'dataset-2'},{name: 'dataset-3'}]
+  const datasets = [{name: 'dataset-1'}, {name: 'dataset-2'}, {name: 'dataset-3'}]
+
   function handleDatasetSelect(item) {
     console.log('selected', item)
   }
-</script>

Advanced Usage

html
<template>
-<AutoComplete
-  :data="users"
-  :filter-fn="filterFn"
-  placeholder="Search users by name, username, or email"
-  @select="handleUserSelect"
->
-  <template #filtered="{ item }">
-    <span> {{ item.name }} </span>
-    <span class="va-text-secondary px-1 font-bold"> &centerdot; </span>
-    <span class="va-text-secondary text-sm"> {{ item.email }} </span>
-  </template>
-</AutoComplete>
+</script>

With Slots

html

+<template>
+  <AutoComplete
+    :data="users"
+    :filter-fn="filterFn"
+    placeholder="Search users by name, username, or email"
+    @select="handleUserSelect"
+  >
+    <template #filtered="{ item }">
+      <span> {{ item.name }} </span>
+      <span class="va-text-secondary px-1 font-bold"> &centerdot; </span>
+      <span class="va-text-secondary text-sm"> {{ item.email }} </span>
+    </template>
+  </AutoComplete>
 </template>
 
 <script setup>
   const users = ref([]);
-const selectedUser = ref();
-const filterFn = (text) => (user) => {
-  const _text = text.toLowerCase();
-  return (
-    user.name.toLowerCase().includes(_text) ||
-    user.username.toLowerCase().includes(_text) ||
-    user.email.toLowerCase().includes(_text)
+  const selectedUser = ref();
+  const filterFn = (text) => (user) => {
+    const _text = text.toLowerCase();
+    return (
+      user.name.toLowerCase().includes(_text) ||
+      user.username.toLowerCase().includes(_text) ||
+      user.email.toLowerCase().includes(_text)
+    );
+  };
+
+  userService.getAll().then((data) => {
+    users.value = data;
+    console.log(users.value);
+  });
+</script>

Async

html

+<template>
+  <AutoComplete
+    :async="true"
+    v-model:search-text="searchText"
+    :placeholder="`Search Users`"
+    :data="retrievedUsers"
+    :display-by="'username'"
+    @clear="emit('clear')"
+    @select="
+      (user) => {
+        onSelect(user);
+      }
+    "
+    :label="'Search Users'"
+    @open="emit('open')"
+    @close="emit('close')"
+  />
+</template>
+
+<script setup>
+const searchText = ref("")
+const retrievedUsers = ref([])
+
+const onSelect = (selectedUser) => {
+  const searchTerm = selectedUser.username
+  userService.getByMatchingUsername({
+    username: searchTerm
+  }).then(res => {
+    retrievedUsers.value = res.data
+  })
+};
+</script>

Props

  • search-text: String - Can optionally be provided to show the selected value within AutoComplete. By default, selected values are not shown.
  • placeholder: String - placeholder for the input element
  • data: Array of Objects - data to search and display
  • filter-by: String - property of data object to use with case-insensitive search
  • display-by: String - property of data object to use to show search results
  • filter-fn: Function (text: String) => (item: Object) => Bool: When provided used to filter the data based on enetered text value
  • async: Boolean - Can be used in combination with search-text to enable asynchronous search for results. Defaults to false.
  • disabled: Boolean - Can be used to show the underlying va-input element in a disabled state. Defaults to false.
  • error: String - Error message to show beneath the underlying va-input element.
  • label: String - Label for AutoComplete
  • loading: Boolean - determines if AutoComplete's dropdown will show a loading indicator, or retrieved results

Events

  • select - emitted when one of the search results is clicked
  • open - emitted when the AutoComplete is opened
  • close - emitted when the AutoComplete is closed
  • clear - emitted when the selected value or the current search term is cleared via va-input's clear button
  • update:search-text - emitted when the search input is changed

Slots

  • #filtered={ item } - Named slot (filtered) with props ({item}) to render a custom search result. This slot is in v-for and called for each search result.
  • #appendInner - Named slot (appendInner) to append custom markup to AutoComplete's input field. The markup provided will be rendered inside va-input's appendInner slot.
  • #prependInner - Named slot (prependInner) to prepend custom markup to AutoComplete's input field. The markup provided will be rendered inside va-input's prependInner slot.

SearchAndSelect

The SearchAndSelect widget offers these features:

  1. Searching for entities
  2. Applying additional filters for the search
  3. Fetching results in batches via infinite-scrolling (the ability to load more results once the user has scrolled past the currently retrieved results)
  4. Selecting / unselecting individual entities, or multiple entities at once.
  5. Emitting events to make client aware of entities being selected/unselected, or of the search being reset.
  6. Load this widget with certain results pre-selected

Basic Usage

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+      }
+    "
+  />
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
+  );
+};
+
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch(searchTerm, () => {
+  resetSearchState();
+});
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm) => {
+  const filterSuffix = (searchTerm) => {
+    return searchTerm ? `, for keyword '${searchTerm}'` : "";
+  };
+
+  let text = (i) => `Result ${i + 1}` + filterSuffix(searchTerm);
+
+  const other = (i) =>
+    `Other val for result ${i + 1}` + filterSuffix(searchTerm);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Filters

Filters can be shown in the search tool via slots. The reset event can be used by the client to reset its filters.

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+        selectValue = ''; // reset Filter
+      }
+    "
+  >
+    <template #filters>
+      <div class="max-w-xs">
+        <VaSelect
+          v-model="selectValue"
+          :options="selectOptions"
+          placeholder="Select an option"
+          label="Filter Dropdown"
+        />
+      </div>
+    </template>
+  </SearchAndSelect>
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectValue = ref("");
+const selectOptions = ref([1, 2, 3]);
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
+  );
+};
+
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const filterQuery = computed(() => {
+  return selectValue.value
+    ? {
+        other: selectValue.value,
+      }
+    : undefined;
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...filterQuery.value,
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch([searchTerm, filterQuery], () => {
+  resetSearchState();
+});
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit, other }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text, other),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm, filterValue) => {
+  const filterSuffix = (searchTerm, dropdownVal) => {
+    return (
+      (searchTerm ? `, for keyword '${searchTerm}'` : "") +
+      (dropdownVal
+        ? `, ${searchTerm ? "and" : "for"} dropdown ${dropdownVal}`
+        : "")
+    );
+  };
+
+  let text = (i) => `Result ${i + 1}` + filterSuffix(searchTerm, filterValue);
+
+  const other = (i) =>
+    `Other val for result ${i + 1}` + filterSuffix(searchTerm, filterValue);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm, filterValue) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm, filterValue));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Formatting and Slots

Displayed results can be formatted via the formatFn prop. They can also be put inside slots for a more customized markup per cell.

For showing a field's value inside customized markup

  • set { slotted: true } in the field's config that is being provided via the searchResultColumns or selectedResultColumns props
  • embed the cell's value inside <template #templateName> ( example - <template #address>).
    • The name of a column's template is the same as the key of the column's config that was provided via the searchResultColumns or selectedResultColumns props.

The value of the column inside the <template> can be accessed via slotProps["value"], which is an object that contains the formatted as well as the raw value for the slotted field.

// slotProps['value]
+
+{
+  formatted: 'formatted value',
+  raw: 'raw value
+}

The below example formats the values in the text column, and embeds the values in the other column inside custom markup.

Notice how both the formatted and raw values of a slotted field can be access via slotProps["value"].

html
<template>
+  <SearchAndSelect
+    v-model:searchTerm="searchTerm"
+    :search-results="searchResults"
+    :selected-results="selectedResults"
+    :search-result-count="totalResultCount"
+    @scroll-end="loadNextPage"
+    :search-result-columns="searchColumnsConfig"
+    :selected-result-columns="selectedColumnsConfig"
+    track-by="text"
+    @select="handleSelect"
+    @remove="handleRemove"
+    @reset="
+      () => {
+        searchTerm = ''; // watcher on searchTerm takes care of resetting the search state
+      }
+    "
+  >
+    <template #other="slotProps">
+    <!-- Both formatted and unFormatted values can be accessed via slotProps -->
+      <va-chip>{{ slotProps["value"].formatted }}</va-chip>
+      <va-chip>{{ slotProps["value"].raw }}</va-chip>
+    </template>
+  </SearchAndSelect>
+</template>
+
+<script setup>
+import _ from "lodash";
+
+const PAGE_SIZE = 10;
+
+const selectedResults = ref([]);
+const searchResults = ref([]);
+
+const page = ref(1);
+const skip = computed(() => {
+  return PAGE_SIZE * (page.value - 1);
+});
+
+const totalResultCount = ref(0);
+
+const searchTerm = ref("");
+
+const handleSelect = (selections) => {
+  selections.forEach((selection) => {
+    if (!selectedResults.value.includes(selection)) {
+      selectedResults.value.push(selection);
+    }
+  });
+};
+
+const handleRemove = (removals) => {
+  removals.forEach((e) =>
+    selectedResults.value.splice(selectedResults.value.indexOf(e), 1),
   );
 };
 
-userService.getAll().then((data) => {
-  users.value = data;
-  console.log(users.value);
+const loadNextPage = () => {
+  page.value += 1; // increase page value for offset recalculation
+  return loadResults();
+};
+
+const batchingQuery = computed(() => {
+  return {
+    offset: skip.value,
+    limit: PAGE_SIZE,
+  };
+});
+
+const fetchQuery = computed(() => {
+  return {
+    ...(searchTerm.value && { text: searchTerm.value }),
+    ...batchingQuery.value,
+  };
+});
+
+const loadResults = () => {
+  return fetchFn(fetchQuery.value).then((res) => {
+    searchResults.value = searchResults.value.concat(res.currentResults);
+    totalResultCount.value = res.totalResultCount;
+  });
+};
+
+watch(searchTerm, () => {
+  resetSearchState();
 });
-</script>

Props

  • placeholder: String - placeholder for the input element
  • data: Array of Objects - data to search and display
  • filter-by: String - property of data object to use with case-insensitive search
  • display-by: String - property of data object to use to show search results
  • filter-fn: Function (text: String) => (item: Object) => Bool: When provided used to filter the data based on enetered text value

Events

  • select - emitted when one of the search results is clicked

Slots

  • #filtered={ item }. Named slot (filtered) with props ({item}) to render a custom search result. This slot is in v-for and called for each search result.

Maybe

Show data if it is neither null or undefined, else show default (provided it is also not null or undefined)

html
<template>
-  <Maybe :data="rowData?.metadata?.num_genome_files" />
-</template

Props

  • data: Any
  • default: Any

CopyText

  • Show text in a read-only input attached with a copy to clipboard button.
  • Width is relative 100%.
  • Input container is x-scrollable if the text overflows
html
<template>
-  <CopyText :text="dataset.archive_path" />
-</template>

props

  • text: String

BinaryStatusChip

Shows a chip with icon, text, color depending on status. Useful to on/off status

html
<template>
-  <BinaryStatusChip
+
+const resetSearchState = () => {
+  // reset search results
+  searchResults.value = [];
+  // reset page value
+  page.value = 1;
+  // load initial set of search results
+  loadResults();
+};
+
+onMounted(() => {
+  loadResults();
+});
+
+const fetchFn = ({ text, offset, limit }) => {
+  return new Promise((resolve) => {
+    resolve({
+      currentResults: mockResults(offset, offset + limit, text),
+      totalResultCount: 50,
+    });
+  });
+};
+
+const mockRow = (i, searchTerm) => {
+  const filterSuffix = (searchTerm) => {
+    return searchTerm ? `, for keyword '${searchTerm}'` : "";
+  };
+
+  let text = (i) => `Result ${i + 1}` + filterSuffix(searchTerm);
+
+  const other = (i) =>
+    `Other val for result ${i + 1}` + filterSuffix(searchTerm);
+
+  return {
+    text: text(i),
+    other: other(i),
+  };
+};
+
+const mockResults = (start, end, searchTerm) => {
+  return _.range(start, end).map((i) => mockRow(i, searchTerm));
+};
+
+const searchColumnsConfig = [
+  {
+    key: "text",
+    label: "Text",
+    width: "350px",
+    formatFn: (text) => `Formatted ${text}`,
+  },
+  {
+    key: "other",
+    label: "Other Field",
+    width: "320px",
+    slotted: true,
+  },
+];
+
+const selectedColumnsConfig = [searchColumnsConfig[0]];
+</script>

Notes

  1. Some props that can be either a string or a function. In such cases, if the prop is a function, it will be called with the target argument, and return the result. If it is a string, the value of the property matching the path specified by the string is looked up in the target argument, and returned.

Props

  • messages: Array - Hint message(s) to be shown below the underlying va-input
  • selectMode: String ['single' | 'multiple'] - Determines if the widget should allow selecting/unselecting multiple results at once. Use single for only allowing a single result to be selected/unselected at one time. Defaults to multiple.
  • placeholder: String - Placeholder for the search input. Default - "Type to search"- selectedResults: Array - the array of currently selected results. Can be used to load the widget with some items pre-selected. Defaults to [].
  • loading: Boolean - Shows loading indicator and disables controls when loading. Defaults to False.
  • searchResults: Array - the array of results retrieved via the current search. Defaults to [].
  • selectedResults: Array - the array of currently selected search results. Can be used to load the widget with some items pre-selected. Defaults to [].
  • selectedLabel: String - The label to show for the table of selected results. Default - "Selected Results"
  • searchResultColumns: Array - The display config for the <va-data-table> of search results. Extends the columns prop provided to <va-data-table>. A formatFn function can be provided in a column's config to format the column's value a certain way. Moreover, { slotted: true } can be added to the column's config to embed the column's value in custom markup. See the Formatting and Slots section above for details.
  • selectedResultColumns: Array - The display config for the <va-data-table> of selected results. Extends the columns prop provided to <va-data-table>. A formatFn function can be provided in a column's config to format the column's value a certain way. Moreover, { slotted: true } can be added to the column's config to embed the column's value in custom markup. See the Formatting and Slots section above for details.
  • searchResultCount: Number - Total number results retrieved from the current search (not to be confused with the number of results in the current batch)
  • searchTerm: String - The search term to be used for performing the search. Client provides this as a component v-model, via v-model:searchTerm.
  • trackBy: String | Function - Used to uniquely identity a result. Defaults to "id".
  • pageSizeSearch: Number - the number of results to be fetched in one batch. Defaults to 10.
  • resource: String - the name of the entity being searched for. Defaults to result.
  • error: String - error to be shown beneath the underlying va-input.
  • showError: Boolean - determines whether error will be shown
  • controlsMargin: String - margin between the controls and the tables
  • controlsHeight: String - height of the controls container element

Slots

  • filters - used for providing controls used for filtering results
  • dynamically-named slots, whose name is the key of the column that the slot is intended for. See the Formatting and Slots section above for an example.

Events

  • search - emitted when a list of elements are selected, with the list of selected elements provided as an argument.
  • remove - emitted when a list of elements are unselected, with the list of unselected elements provided as an argument.
  • reset - emitted when the search controls are reset
  • update:searchTerm - emitted when the searchTerm v-model is to be updated
  • scroll-end - emitted when user scrolls to the end of the current batch of results

Maybe

Show data if it is neither null or undefined, else show default (provided it is also not null or undefined)

html

+<template>
+  <Maybe :data="rowData?.metadata?.num_genome_files"/>
+</template

Props

  • data: Any
  • default: Any

CopyText

  • Show text in a read-only input attached with a copy to clipboard button.
  • Width is relative 100%.
  • Input container is x-scrollable if the text overflows
html

+<template>
+  <CopyText :text="dataset.archive_path"/>
+</template>

props

  • text: String

BinaryStatusChip

Shows a chip with icon, text, color depending on status. Useful to on/off status

html

+<template>
+  <BinaryStatusChip
     :status="!source"
     :icons="['mdi:account-off-outline', 'mdi:account-badge-outline']"
   />
-</template>

Props

  • status: Boolean (0-off, 1-on)
  • icons - Array of 2 elements (off icon, on icon)
  • labels - Array of 2 element (off lable, on label)
    • default: ['disbaled', 'enabled']

useQueryPersistence Composable

This composition function helps you manage query parameters in the URL and keep them in sync with a reactive object in your component.

Usage:

  1. Create a ref to hold the query parameters
  2. Call this function on the ref.
javascript
import useQueryPersistence from "@/composables/useQueryPersistence";
+</template>

Props

  • status: Boolean (0-off, 1-on)
  • icons - Array of 2 elements (off icon, on icon)
  • labels - Array of 2 element (off label, on label)
    • default: ['disbaled', 'enabled']

EnvAlert

Shows an <va-alert/> which displays the mode that the app is running in (test, CI, etc.).

The environments that this alert should be enabled for can be set in ./ui/config.js, under property alertForEnvironments.

html
<template>
+  <EnvAlert color="warning" icon="info" />
+</template>

Props

  • icon: String - icon to be included in the alert.
  • color: String - alert's color

Props are forwarded to Vuestic's <va-alert /> component.

useQueryPersistence Composable

This composition function helps you manage query parameters in the URL and keep them in sync with a reactive object in your component.

Usage:

  1. Create a ref to hold the query parameters
  2. Call this function on the ref.
javascript
import useQueryPersistence from "@/composables/useQueryPersistence";
 
 const default_query_params = () => ({
   status: null,
@@ -86,8 +581,8 @@
   defaultValue: default_query_params(),
   key: "wq",
   history_push: false,
-});

It will update the URL query parameters by watching the refObject and it will update the refObject when URL query parameters change.

- +});

It will update the URL query parameters by watching the refObject and it will update the refObject when URL query parameters change.

+ \ No newline at end of file diff --git a/docs/.vitepress/dist/upload.html b/docs/.vitepress/dist/upload.html new file mode 100644 index 000000000..50aede5a5 --- /dev/null +++ b/docs/.vitepress/dist/upload.html @@ -0,0 +1,26 @@ + + + + + + Upload Architecture | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Upload Architecture

1. Introduction

Bioloop allows uploading one or more files through the browser, while ensuring strict access control to prevent unauthorized access. These chunks are then merged into corresponding files through a worker.

2. Requirements and Limitations

Authorized users should be able to upload files directly from their web browsers. The uploaded files should be protected from unauthorized users who have access to Slate-Scratch.

Network timeouts, data corruption and other problems that arise from uploading large files must be avoided. To achieve this, our file upload architecture uploads files in chunks of 2 MB. Uploading files in chunks also gives us a more granular view into the upload state.

Since our workers run on Colo nodes, it is convenient to upload files to Colo nodes, so they can be processed by workers, instead of uploading files to the main API server, and then transferring them to Colo nodes.

3. Architecture Overview

To meet the requirements outlined above, a distributed architecture is employed.

  • UI Client: Users logs into the bioloop application through their web browsers and navigate to the appropriate page to initiate file uploads.
  • API: This node serves the user interface and API endpoints with user and metadata stored in a PostgreSQL database but does not have direct access to the dataset files.
  • Rhythm API: This node is used to trigger workflows from the UI.
  • Workers: Processing of uploaded file chunks occurs on this node via a Celery task. These worker run on Colo nodes.
  • Signet: An OAuth server supporting client credential flow with the appropriate scope for uploading files to create secure tokens.
  • File Upload Server (Nginx): A server on the worker node which receives requests from users to upload files. Files are uploaded to this server, in chunks.
  • Secure Upload API: A lightweight app hosted on the File Upload Server that validates the incoming requests (i.e. checking for the appropriate scope) to the file upload API.

4. The Upload

4.1 Logging

For each upload, information is logged to the following relational tables (PostgreSQL):

  1. upload_log - contains metadata about the upload itself. Is linked to one or more file_upload_log objects.
  2. dataset_upload_log - contains metadata specific to a dataset's upload. Is linked to one upload_log object, and one dataset object.
  3. file_upload_log - contains metadata about a file being uploaded.

4.2 Steps

  1. Before an upload begins, the following things happen sequentially:
    • Checksum evaluation - MD5 checksums are evaluated for each file being uploaded, as well as for each chunk per file.
    • Any metadata associated with the upload (like the source raw data, the file type, the names/checksums/relative paths of files being uploaded, etc.), are logged into persistent storage.
  2. The actual upload begins once the above steps are successful.
  3. Chunks are uploaded sequentially. If a chunk upload fails, the client-side retries the upload upto 5 times before failing.
  4. The client sends an HTTP request to upload a chunk to our File Upload Server, which writes the received chunk to the filesystem, after validating its checksum (this is the first stage of checksum validation).
  5. After all files' chunks are uploaded successfully, the client-side makes a request to the Rhythm API to trigger the process_dataset_upload worker, which merges each file's uploaded chunks into the corresponding file.

4.3 Directory structure

The following directories on the File Upload Server are used for uploads:

  1. Directory that Data Products are uploaded to:
upload_dir = config['paths']['DATA_PRODUCT']['upload']
  1. The individual chunks for an uploaded file are stored in:
[upload_dir] / [dataset_id] / uploaded_chunks / [file_upload_log_id]`

Here, dataset_id is the unique id created for the dataset before the upload began, and file_upload_log_id is the unique id for the record of this file's upload. 3. Within this directory, individual chunks are named as [file_md5]-[i] where i serves as an index for this chunk among all of the file's sequentially-uploaded chunks, identifying the order of this chunk in the file. When merging a file's chunks into the corresponding file, chunks will be processed sequentially based on this index. 4. Once a file has been processed, and it's chunks have been merged into the corresponding file, the recreated file is stored at:

[upload_dir] / [dataset_id] / processed

4.4 Access Control

  1. To verify that a user is authorized to initiate an upload, we perform role-based checking when our /upload API receives a request to initiate an upload. This validation cannot be performed on the secure_upload NGINX server, since we only maintain access-control data on the API server.
  2. After verifying that the user is authorized to upload datasets, the client requests a Bearer token from the Signet service, and attaches it to the Authorization header before sending a request to the /upload API to upload a file chunk.
    • The scope included in the granted token contains the name of the file prefixed with the scope prefix upload_file:.
    • If the name of the file being uploaded has spaces in it, the spaces are replaced by hyphens in the granted token's scope.
  3. Before the /upload endpoint accepts a file chunk that needs to be uploaded, it verifies that the scope contained in the Bearer token is the same as the expected scope. If these scopes do not match, the /upload API rejects the HTTP request.

Example

As an example, to upload file my file.json, the Bearer token that is used to call the /upload API file will be expected to have scope upload_file:my-file.json.

4.5 Status

The status of the upload, as well the status of each file in the upload goes through the following values:

StatusDescription
COMPUTING_CHECKSUMSChecksums are being computed for the file(s) to be uploaded
CHECKSUM_COMPUTATION_FAILEDChecksums computation failed for the file(s) to be uploaded
UPLOADINGUpload initiated through the browser
UPLOAD_FAILEDUpload could not be completed (network errors)
UPLOADEDAll files successfully uploaded
PROCESSINGUpload currently being processed
PROCESSING_FAILEDEncountered errors while processing a file in this upload
COMPLETEAll files in the upload processed successfully
FAILEDUpload was failing processing (i.e. status == PROCESSING_FAILED) for more than 72 hours, and was therefore marked as FAILED and its filesystem resources deleted.

5. Processing

Uploaded file chunks are merged into the corresponding file by the worker process_dataset_upload. After the file has been recreated from its chunks, the MD5 checksum of the recreated file is matched with the expected MD5 checksum of the file that was persisted to the database before the upload (this is the second stage of checksum validation).

After all uploaded files have been processed successfully, the resources (uploaded file chunks) associated with them are deleted.

6. Data Integrity

The uploaded data goes through two stages of checksum validation:

  1. Validating MD5 checksum of an uploaded file chunk before writing it to the filesystem.
  2. Validating MD5 checksum of the file, once it has been recreated from its chunks by the worker.

7. Retry

  1. Upon encountering retryable exceptions, the process_dataset_upload worker retries itself 3 times before failing.
  2. The script manage_pending_dataset_uploads.py, which is scheduled to run every 24 hours, looks for uploads that are failing (status == PROCESSING_FAILED), and retries to process the ones which have been failing for less than 72 hours. If some uploads have been failing for more than 72 hours, they are marked as FAILED and their filesystem resources (uploaded file chunks, and any processed files) are purged.
+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/vp-icons.css b/docs/.vitepress/dist/vp-icons.css new file mode 100644 index 000000000..ddc5bd8ed --- /dev/null +++ b/docs/.vitepress/dist/vp-icons.css @@ -0,0 +1 @@ +.vpi-social-github{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")} \ No newline at end of file diff --git a/docs/.vitepress/dist/welcome-message.html b/docs/.vitepress/dist/welcome-message.html new file mode 100644 index 000000000..a7ea01798 --- /dev/null +++ b/docs/.vitepress/dist/welcome-message.html @@ -0,0 +1,26 @@ + + + + + + Welcome Message | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Subject: Your Research Data is Now Available

Dear [Researcher’s Name],

Your recent data request is now complete and available for access.

To retrieve your data:

  1. Visit the data portal: [Portal URL]
  2. Click on the "Log In" button on the homepage.
  3. Sign in using your [institution credentials or other login method].
  4. Navigate to the "Projects" section to locate your data.
  5. If your data is actively on disk, use the "Download" button under "Associated Datasets" to access it.
  6. If your data is archived on tape, use the "Stage" button to request retrieval. Once the staging process is complete, the "Download" button will become available.

If you have any questions, feedback, or require assistance, please feel free to contact us at [Support Email or Contact Information].

Best regards,
[Your Name]
[Your Position]
[Institution or Research Core Name]

+ + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/worker/index.html b/docs/.vitepress/dist/worker/index.html index 91264f184..77dd72dcf 100644 --- a/docs/.vitepress/dist/worker/index.html +++ b/docs/.vitepress/dist/worker/index.html @@ -5,40 +5,22 @@ Workers | Bioloop - - + + + - - - - - + + + + + + -
Skip to content

Workers

Coding Guidelines

Hierarchical Config

  • The default & dev config goes into workers/config/common.py
  • The overrides for production goes into workers/config/production.py
  • Based on the environment variable APP_ENV, config from that file is imported and merged with the common config.
    • Add APP_ENV=production to .env file which load_dotenv reads or
    • directly set it as export APP_ENV=production.
  • In project files, import config as from workers.config import config
  • Imported config is a DotMap object, which supports both config[] and config. access.
  • To add a new environment (for example "stage"), create a new file inside workers/config called stage.py and have the overriding config as a dict assigned to a variable named config.

Celery config

  • config specific to Celery is in workers/config/celeryconfig.py
  • Config is in python values, instead of a dict
  • Env specific values and secrets are loaded from .env file

Code Organization

  • Celery Tasks: workers/tasks/*.py
  • Scheduled job and other scripts: workers/scripts/*.py
  • Helper code: workers/*.py
  • Config / settings are in workers/config/*.py and .env
  • Test code is in tests/

Hot Module Replacement

Worker automatically run with updated code except for the code in

  • workers.config.*
  • workers.utils
  • workers.celery_app
  • workers.task.declaration

Deployment

  • Add module load python/3.10.5 to ~/.modules
  • Update .env (make a copy of .env.example and add values)
  • Install dependencies
bash
poetry export --without-hashes --format=requirements.txt > requirements.txt
-pip install -r requirements.txt
bash
cd ~/app/workers
-pm2 start ecosystem.config.js
-# optional
-pm2 save

Testing with workers running on local machine

Start mongo and queue

bash
cd <rhythm_api>
-docker-compose up queue mongo -d

Start Workers

bash
python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q'

--concurrency 1: number of worker processed to pre-fork

-O fair: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks when they are actually available.

Use --hostname '<app_name>-celery-<worker_name>@%h' to distinguish multiple workers running on the same machine either for the same app or different apps.

  • replace <app_name> with app name (ex: bioloop)
  • replace <worker_name> with worker name (ex: w1)

Auto-scaling - max_concurrency,min_concurrency --autoscale=10,3 (always keep 3 processes, but grow to 10 if necessary).

--queues '<app_name>-dev.sca.iu.edu' comma separated queue names. worker will subscribe to these queues for accepting tasks. Configured in workers/config/celeryconfig.py with task_routes, task_default_queue

Run test

bash
python -m tests.test

Testing with workers running on COLO node and Rhythm API

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

  • start postgres locally using docker
bash
cd <app_name>
-docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
-docker-compose up queue mongo -d
-poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
-pnpm start
bash
cd <app_name>/ui
-pnpm dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \
-  -A \
-  -R 3130:localhost:3030 \
-  -R 28017:localhost:27017 \
-  -R 5772:localhost:5672 \
-  bioloopuser@workers.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
-colo23> git checkout dev
-colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
-colo23> poetry install
-colo23> poetry shell
-colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1

Dataset Name:

  • taken from the name of the directory ingested
  • used in watch.py to filter out registered datasets
  • used to compute the staging path staging_dir / alias / dataset['name']
  • used to compute the qc path Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'
  • used to compute the scratch tar path while downloading the tar file from SDA Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')
- + + \ No newline at end of file diff --git a/docs/.vitepress/dist/worker/overview.html b/docs/.vitepress/dist/worker/overview.html new file mode 100644 index 000000000..9360814c9 --- /dev/null +++ b/docs/.vitepress/dist/worker/overview.html @@ -0,0 +1,46 @@ + + + + + + Overview | Bioloop + + + + + + + + + + + + + + + +
Skip to content

Worker Overview

Coding Guidelines

Hierarchical Config

  • The default & dev config goes into workers/config/common.py
  • The overrides for production goes into workers/config/production.py
  • Based on the environment variable APP_ENV, config from that file is imported and merged with the common config.
    • Add APP_ENV=production to .env file which load_dotenv reads or
    • directly set it as export APP_ENV=production.
  • In project files, import config as from workers.config import config
  • Imported config is a DotMap object, which supports both config[] and config. access.
  • To add a new environment (for example "stage"), create a new file inside workers/config called stage.py and have the overriding config as a dict assigned to a variable named config.

Celery config

  • config specific to Celery is in workers/config/celeryconfig.py
  • Config is in python values, instead of a dict
  • Env specific values and secrets are loaded from .env file

Code Organization

  • Celery Tasks: workers/tasks/*.py
  • Scheduled job and other scripts: workers/scripts/*.py
  • Helper code: workers/*.py
  • Config / settings are in workers/config/*.py and .env
  • Test code is in tests/

Parallel tasks limit

The maximum number of active (i.e. not 'PENDING') tasks that can run at a time is determined by the number of Celery workers, which is currently set to 8.

This config can be found in ecosystem.config.js, under app celery_worker:

-m celery -A workers.celery_app worker ... --autoscale=8,2

Hot Module Replacement

Worker automatically run with updated code except for the code in

  • workers.config.*
  • workers.utils
  • workers.celery_app
  • workers.task.declaration

Deployment

  • Add module load python/3.10.5 to ~/.modules
  • Update .env (make a copy of .env.example and add values)
  • Install dependencies
bash
poetry export --without-hashes --format=requirements.txt > requirements.txt
+pip install -r requirements.txt
bash
cd ~/app/workers
+pm2 start ecosystem.config.js
+# optional
+pm2 save

Testing with workers running on local machine

Start mongo and queue

bash
cd <rhythm_api>
+docker-compose up queue mongo -d

Start Workers

bash
python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q'

--concurrency 1: number of worker processed to pre-fork

-O fair: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks when they are actually available.

Use --hostname '<app_name>-celery-<worker_name>@%h' to distinguish multiple workers running on the same machine either for the same app or different apps.

  • replace <app_name> with app name (ex: bioloop)
  • replace <worker_name> with worker name (ex: w1)

Auto-scaling - max_concurrency,min_concurrency --autoscale=10,3 (always keep 3 processes, but grow to 10 if necessary).

--queues '<app_name>-dev.sca.iu.edu' comma separated queue names. worker will subscribe to these queues for accepting tasks. Configured in workers/config/celeryconfig.py with task_routes, task_default_queue

Run test

bash
python -m tests.test

Testing with workers running on COLO node and Rhythm API

There are no test instances of API, rhythm_api, mongo, postgres, queue running. These need to be run in local and port forwarded through ssh.

  • start postgres locally using docker
bash
cd <app_name>
+docker-compose up postgres -d
  • start rhythm_api locally
bash
cd <rhythm_api>
+docker-compose up queue mongo -d
+poetry run dev
  • start UI and API locally
bash
cd <app_name>/api
+pnpm start
bash
cd <app_name>/ui
+pnpm dev
  • Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server running on the local machine.
    • API - local port - 3030, remote port - 3130
    • Mongo - local port - 27017, remote port - 28017
    • queue - local port - 5672, remote port - 5772
bash
ssh \
+  -A \
+  -R 3130:localhost:3030 \
+  -R 28017:localhost:27017 \
+  -R 5772:localhost:5672 \
+  bioloopuser@workers.iu.edu
  • pull latest changes in dev branch to <bioloop_dev>
bash
colo23> cd <app_dev>
+colo23> git checkout dev
+colo23> git pull
  • create / update <app_dev>/workers/.env

  • create an auth token to communicate with the express server (postgres db)

    • cd <app>/api
    • node src/scripts/issue_token.js <service_account>
    • ex: node src/scripts/issue_token.js svc_tasks
    • docker ex: sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks
  • install dependencies using poetry and start celery workers

bash
colo23> cd workers
+colo23> poetry install
+colo23> poetry shell
+colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1

Dataset Name:

  • taken from the name of the directory ingested
  • used in watch.py to filter out registered datasets
  • used to compute the staging path staging_dir / alias / dataset['name']
  • used to compute the qc path Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'
  • used to compute the scratch tar path while downloading the tar file from SDA Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')
+ + + + \ No newline at end of file diff --git a/docs/api/01-core/cluster.md b/docs/api/01-core/cluster.md new file mode 100644 index 000000000..4ec9b073e --- /dev/null +++ b/docs/api/01-core/cluster.md @@ -0,0 +1,58 @@ +# Cluster Management + +The `manage_cluster` function is a utility for managing a cluster of worker processes in a Node.js application. It provides features such as scaling, restart limits, and graceful shutdowns, making it easier to build robust and scalable applications. + +This system is designed to efficiently manage multiple worker processes in a Node.js application. It leverages the `node:cluster` module to distribute workloads across available CPU cores, ensuring optimal utilization of system resources. This feature is particularly useful for high-performance applications that need to handle a large number of concurrent requests. + +Without this system, the application would run as a single process, potentially underutilizing multi-core CPUs and becoming a bottleneck under heavy load. + +## Overview + +The `manage_cluster` function allows you to: + +- Run a master process to manage worker processes. +- Define custom logic for both master and worker processes. +- Automatically restart workers within configurable limits. +- Gracefully shut down workers on receiving termination signals. + +## Configuration Options + +The function accepts an options object with the following properties: + +- **`master`**: A callback function to execute in the master process (optional). +- **`worker`**: A callback function to execute in each worker process (required). +- **`beforeApplicationFork`**: A callback function to execute before forking workers (optional). +- **`count`**: The number of worker processes to spawn (default: 2). +- **`max_restarts`**: Maximum number of worker restarts allowed within the interval (default: 3). +- **`max_restarts_interval`**: Time interval (in milliseconds) for the restart limit (default: 10000). +- **`grace`**: Grace period (in milliseconds) for workers to shut down gracefully (default: 5000). +- **`signals`**: List of signals to listen for to trigger shutdown (default: `['SIGINT', 'SIGTERM']`). + +## Example Usage + +```javascript +const manage_cluster = require('./core/cluster-manager'); + +manage_cluster({ + master: () => console.log('Master process running'), + worker: () => setInterval(() => console.log('Worker process running'), 1000), + count: 4, + max_restarts: 5, + max_restarts_interval: 15000, + grace: 3000, + signals: ['SIGINT', 'SIGTERM', 'SIGHUP'], +}); +``` + +In this example: +- The master process logs a message when it starts. +- Each worker process logs a message every second. +- The cluster spawns 4 workers and allows up to 5 restarts within a 15-second interval. +- Workers are given 3 seconds to shut down gracefully when a termination signal is received. + +## Metrics Integration + +The master process can also expose aggregated metrics using the `prom-client` library. This is demonstrated in the `cluster.js` file, where a metrics server is set up to listen on a configurable port. + +Refer to the `cluster.js` file for a complete example of integrating metrics with the cluster manager. + diff --git a/docs/api/01-core/configuration.md b/docs/api/01-core/configuration.md new file mode 100644 index 000000000..bb581717c --- /dev/null +++ b/docs/api/01-core/configuration.md @@ -0,0 +1,110 @@ +# Configuration + +The configuration system is a critical part of the application, enabling developers to manage environment-specific settings and maintain clean, maintainable code. It uses the [config module](https://github.com/node-config/node-config) to load and manage configuration files, ensuring that the application behaves consistently across different environments. + +## Purpose of the Configuration System + +The configuration system exists to centralize and standardize the way application settings are managed. Without it, developers would need to hardcode settings or rely on ad-hoc methods to manage environment-specific configurations, leading to brittle and error-prone code. + +By using this system: +- Developers can easily override settings for different environments (e.g., development, production). +- Sensitive information, such as API keys and database credentials, can be securely managed using environment variables. +- The application becomes easier to maintain and extend, as configuration logic is decoupled from the application logic. + +## How It Fits Into the System + +The configuration system integrates seamlessly with the application by: +- Loading settings from JSON files located in the `./config/` directory. +- Allowing overrides via environment variables, command-line arguments, or external sources. +- Providing a consistent API for accessing configuration values throughout the application. + +This ensures that all parts of the application use the same source of truth for configuration, reducing duplication and potential inconsistencies. + +## Configuration Files + +The following configuration files are used: +- `default.json`: Contains default settings for the application. +- `{NODE_ENV}.json` (e.g., `production.json`): Contains environment-specific overrides. +- `custom-environment-variables.json`: Maps configuration properties to environment variables. + +### Precedence of Configuration + +The configuration system resolves settings in the following order of precedence: +1. Command-line arguments +2. Environment variables +3. `{NODE_ENV}.json` (e.g., `production.json`) +4. `default.json` + +This precedence ensures that the most specific settings are applied, while falling back to defaults when necessary. + +## Environment Variables + +The application uses the `dotenv-safe` module to load environment variables from a `.env` file. This ensures that all required variables are defined and prevents runtime errors caused by missing configurations. + +### Example `.env` File + +```env +DATABASE_PASSWORD=your_database_password +WORKFLOW_AUTH_TOKEN=your_auth_token +OAUTH_BASE_URL=https://example.com/oauth +``` + +### Loading Environment Variables + +To load environment variables, the following code is used: + +```javascript +require('dotenv-safe').config(); +``` + +Ensure that a `.env.example` file exists to document required variables and their expected format. + +## Step-by-Step Instructions for Usage + +1. **Define Default Settings**: + Add default settings in `default.json` under the `./config/` directory. For example: + ```json + { + "express": { + "port": 3030, + "host": "localhost" + } + } + ``` + +2. **Add Environment-Specific Overrides**: + Create a file named `{NODE_ENV}.json` (e.g., `production.json`) and override specific settings: + ```json + { + "express": { + "port": 8080 + } + } + ``` + +3. **Map Environment Variables**: + Use `custom-environment-variables.json` to map sensitive settings to environment variables: + ```json + { + "express": { + "port": "EXPRESS_PORT" + } + } + ``` + +4. **Create a `.env` File**: + Define the required environment variables in a `.env` file: + ```env + EXPRESS_PORT=3030 + ``` + +5. **Load Configuration in Code**: + Access configuration values in your application using the `config` module: + ```javascript + require('dotenv-safe').config(); + const config = require('config'); + const port = config.get('express.port'); + console.log(`Server running on port ${port}`); + ``` + +foo bar \ No newline at end of file diff --git a/docs/api/01-core/cron-task-scheduling.md b/docs/api/01-core/cron-task-scheduling.md new file mode 100644 index 000000000..73919b3f7 --- /dev/null +++ b/docs/api/01-core/cron-task-scheduling.md @@ -0,0 +1,51 @@ +# Scheduling Tasks + +The cron task scheduling feature allows you to define and execute recurring tasks at specified intervals in the primary process of the cluster. This feature is useful for automating maintenance, cleanup, and other background operations in your application. + +## How It Works + +The `registerCronJobs` function in the `/api/src/core/cron.js` file is responsible for registering all cron jobs. It uses the `node-cron` library to define and schedule tasks. Each task is implemented as a separate module, ensuring modularity and reusability. + +### Usage Instructions + +1. **Define a New Task**: + - Create a new file in the `cron` directory (e.g., `exampleTask.cron.js`). + - Export a `run` async function that contains the task logic. + + ```javascript + module.exports.run = async (logger) => { + logger.info('Running example task...'); + // Task logic here + }; + ``` + +2. **Register the Task**: + - Open the `cron.js` file. + - Use the `cron.schedule` method to define the schedule and link the task. + + ```javascript + // filepath: /Users/deduggi/Documents/SCA/bioloop/api/src/core/cron.js + const cron = require('node-cron'); + const { createTaskLogger } = require('./logger'); + + function registerCronJobs() { + cron.schedule('* * * * *', () => { + const task = require('../cron/exampleTask.cron'); + const taskLogger = createTaskLogger('exampleTask'); + task.run(taskLogger); + }); + } + + module.exports = registerCronJobs; + ``` + +Cron tasks are registered using the `beforeApplicationFork` lifecycle hook ensuring that they run in the primary process of the cluster. + +The `createTaskLogger` function creates a logger instance for each task, allowing you to log task-specific messages. The logger is passed to the task function as an argument. The log file for each task is stored in the `logs` directory with the task name as the filename. + +### Best Practices + +- Use meaningful names for tasks and loggers to simplify debugging. +- Avoid long-running tasks in cron jobs; delegate heavy processing to worker queues if needed. +- Test tasks thoroughly to ensure they don’t interfere with application performance. + diff --git a/docs/api/01-core/error-handling.md b/docs/api/01-core/error-handling.md new file mode 100644 index 000000000..3f8c7d53b --- /dev/null +++ b/docs/api/01-core/error-handling.md @@ -0,0 +1,206 @@ +# Error Handling +Source: [Express Error Handling](https://expressjs.com/en/guide/error-handling.html) + +Error handling is a critical part of any robust application. This feature ensures that errors are properly caught, logged, and handled in a way that provides meaningful feedback to the client while maintaining the integrity of the server. Without proper error handling, unexpected issues could crash the server or expose sensitive information to clients. + +This document explains the error-handling mechanisms implemented in this codebase, how they fit into the overall system, and how they help maintain clean, maintainable code. + +## Asynchronous Error Handler +Express (versions below 5) does not automatically catch errors thrown in asynchronous code. To address this, an `asyncHandler` middleware is used to wrap asynchronous route handlers. This middleware ensures that any errors are passed to the default error handler. + +### Why It Exists +Without this middleware, developers would need to manually wrap every asynchronous route handler in a `try-catch` block, leading to repetitive and error-prone code. + +### Usage +Wrap your asynchronous route handlers with `asyncHandler` to automatically catch errors and pass them to the error handler. + +```javascript +const asyncHandler = require('../middleware/asyncHandler'); + +router.get('/user', asyncHandler(async (req, res, next) => { + const user = await userService.findActiveUserBy( + 'username', req.query.username + ); + res.json(user); +})); +``` + +This replaces the need for manual `try-catch` blocks: + +```javascript +router.get('/user', async (req, res, next) => { + try { + const user = await userService.findActiveUserBy('username', req.query.username); + res.json(user); + } catch (err) { + next(err); + } +}); +``` + +## The Default Error Handler +The default error handler is the last middleware in the stack. It ensures that all unhandled errors are processed and a proper response is sent to the client. + +### Key Features +- Sets `res.statusCode` based on `err.status` or defaults to 500. +- Sends a generic error message in production or the stack trace in development. +- Prevents sensitive information from being exposed to clients. + +### Custom Default Error Handler +The custom error handler (`errorHandler`) logs errors to the console and sends appropriate responses to clients: +- For HTTP errors (e.g., `createError(400, 'foo bar')`), the client receives the message and status code. +- For non-HTTP errors (e.g., `new Error('business logic error')`), a generic message is sent to the client. + +### Example +```javascript +app.use(errorHandler); +``` + +## Custom Error Handlers +Custom error handlers are used to handle specific types of errors before they reach the default error handler. + +### 404 Handler +The `notFound` middleware catches requests to unknown routes and forwards a 404 error. + +```javascript +app.use(notFound); +``` + +### Prisma Not Found Error Handler +Prisma's query engine may throw opaque errors when a resource is not found. The `prismaNotFoundHandler` middleware intercepts these errors and converts them into HTTP 404 responses. + +#### Example +Before refactoring: +```javascript +router.delete('/:username', asyncHandler(async (req, res, next) => { + try { + const deletedUser = await userService.softDeleteUser(req.params.username); + res.json(deletedUser); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e?.meta?.cause?.includes('not found')) { + return next(createError.NotFound()); + } + return next(e); + } +})); +``` + +After refactoring: +```javascript +app.use(prismaNotFoundHandler); + +router.delete('/:username', asyncHandler(async (req, res, next) => { + const deletedUser = await userService.softDeleteUser(req.params.username); + res.json(deletedUser); +})); +``` + +### Prisma Constraint Violation Handler +Handles database constraint violations (e.g., unique constraints) and sends appropriate HTTP responses. + +#### Example +Before refactoring: +```javascript +router.post('/', asyncHandler(async (req, res, next) => { + try { + const newUser = await userService.createUser(req.body); + res.json(newUser); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + return next(createError.Conflict('User already exists')); + } + return next(e); + } +})); +``` + +After refactoring: +```javascript +app.use(prismaConstraintFailedHandler); + +router.post('/', asyncHandler(async (req, res, next) => { + const newUser = await userService.createUser(req.body); + res.json(newUser); +})); +``` + +### Assertion Error Handler +Catches `AssertionError` instances and sends a 400 Bad Request response. + +#### Example +Before refactoring: +```javascript +const assert = require('assert'); +router.post('/', asyncHandler(async (req, res, next) => { + try { + assert(req.body.username, 'Username is required'); + const newUser = await userService.createUser(req.body); + res.json(newUser); + } catch (e) { + if (e instanceof assert.AssertionError) { + return next(createError.BadRequest(e.message)); + } + return next(e); + } +})); +``` + +After refactoring: +```javascript +app.use(assertionErrorHandler); + +router.post('/', asyncHandler(async (req, res, next) => { + assert(req.body.username, 'Username is required'); + const newUser = await userService.createUser(req.body); + res.json(newUser); +})); +``` + +### Axios Error Handler +Handles errors from Axios HTTP requests, logs them, and sends a 500 Internal Server Error response. + +#### Example +Before refactoring: +```javascript +const axios = require('axios'); + +router.get('/user', asyncHandler(async (req, res, next) => { + try { + const response = await axios.get('https://api.example.com/user'); + res.json(response.data); + } catch (e) { + if (e.response) { + return next(createError(e.response.status, e.response.statusText)); + } + return next(e); + } +})); +``` + +After refactoring: +```javascript +app.use(axiosErrorHandler); + +router.get('/user', asyncHandler(async (req, res, next) => { + const response = await axios.get('https://api.example.com/user'); + res.json(response.data); +})); +``` + +## Integration into the System +Error-handling middleware is registered in `app.js` in the following order: +1. `notFound` for unknown routes. +2. Prisma-specific handlers (`prismaNotFoundHandler`, `prismaConstraintFailedHandler`). +3. Other custom handlers (`assertionErrorHandler`, `axiosErrorHandler`). +4. `errorHandler` as the final fallback. + +This layered approach ensures that errors are handled at the appropriate level, keeping the codebase clean and maintainable. + +## Summary +By centralizing error handling, this system: +- Reduces repetitive code. +- Improves maintainability. +- Ensures consistent error responses. +- Protects sensitive information. + +Follow the examples above to integrate error handling into your routes and middleware. diff --git a/docs/api/01-core/events.md b/docs/api/01-core/events.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/01-core/index.md b/docs/api/01-core/index.md new file mode 100644 index 000000000..6779ece11 --- /dev/null +++ b/docs/api/01-core/index.md @@ -0,0 +1,3 @@ +--- +title: Core +--- \ No newline at end of file diff --git a/docs/api/01-core/lifecycle-hooks.md b/docs/api/01-core/lifecycle-hooks.md new file mode 100644 index 000000000..4c8a7ea22 --- /dev/null +++ b/docs/api/01-core/lifecycle-hooks.md @@ -0,0 +1,65 @@ +# Lifecycle Hooks + +The lifecycle hooks in this project are designed to manage specific tasks during the application's lifecycle. These hooks ensure that critical operations are performed at the right time, such as during startup, shutdown, or before forking worker processes. + +## Location of Lifecycle Hooks + +The lifecycle hooks are implemented in the file located at: +``` +src/core/lifecycle.js +``` + +### Etiquette for Editing/Updating Lifecycle Hooks + +- **Do not add your function body to `lifecycle.js`.** +- Instead, define new functions or logic in separate files/modules and call them from the respective lifecycle hook in `lifecycle.js`. +- This approach ensures modularity, readability, and easier testing of individual components. + +For example: +```javascript +// Define your logic in a separate file +async function customLogic() { + // ...custom logic... +} + +// Call it in the lifecycle hook +async function onApplicationBootstrap() { + await customLogic(); +} +``` + +## Hooks Overview + +### `beforeApplicationFork` +- **Purpose**: Executes tasks that need to run in the master process before forking worker processes. +- **Use Case**: Perform one-time setup tasks such as generating Swagger documentation or registering cron jobs. +- **Error Handling**: Errors in this hook are logged, and the process exits if critical tasks fail. + + +### `onApplicationBootstrap` +- **Purpose**: Executes tasks during the application bootstrap phase, typically after the application has started. +- **Use Case**: Log warnings or perform checks based on configuration settings. +- **Error Handling**: Errors in this hook are logged, and the process exits if critical tasks fail. + + +### `beforeApplicationShutdown` +- **Purpose**: Executes tasks in worker processes before the server shuts down. +- **Use Case**: Perform cleanup tasks or prepare the application for shutdown. +- **Error Handling**: Errors are logged, but the shutdown process continues. + + +### `onApplicationShutdown` +- **Purpose**: Executes tasks in worker processes after the server has shut down. +- **Use Case**: Final cleanup or logging after the application has fully stopped. +- **Error Handling**: Errors are logged, but the process exits regardless. + + +## Integration + +These hooks are used in the application lifecycle to ensure proper initialization and cleanup. For example: +- `beforeApplicationFork` is called in the master process before forking workers. +- `onApplicationBootstrap` is invoked during the startup phase. +- `beforeApplicationShutdown` and `onApplicationShutdown` are used during the shutdown process to handle cleanup tasks. + +By leveraging these hooks and following the guidelines for editing them, the application ensures a clean and predictable lifecycle management process. + diff --git a/docs/api/01-core/queue.md b/docs/api/01-core/queue.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/01-core/validation.md b/docs/api/01-core/validation.md new file mode 100644 index 000000000..ff48aa271 --- /dev/null +++ b/docs/api/01-core/validation.md @@ -0,0 +1,158 @@ +# Request Validation + +Request validation ensures that incoming HTTP requests contain the expected data in the correct format. This feature uses [express-validator](https://express-validator.github.io/docs/) to validate the request's query parameters, route parameters, or body content. It helps enforce data integrity and prevents invalid or malicious data from propagating through the system. `express-validator` wraps the extensive collection of validators and sanitizers offered by [validator.js](https://github.com/validatorjs/validator.js). + +Without this validation layer, developers would need to write repetitive and error-prone checks in every route handler, leading to cluttered and less maintainable code. By centralizing validation logic, this feature promotes clean, declarative, and reusable code. + + +## Benefits + +- **Improved Code Quality**: Reduces repetitive validation code in route handlers. +- **Error Handling**: Centralizes validation error handling, making it easier to maintain. +- **Declarative Syntax**: Encourages a declarative approach to validation, improving readability. +- **Scalability**: Simplifies adding new routes with consistent validation logic. + +## Usage Instructions + +To use the validation feature, follow these steps: + +1. Import the `validate` function and the necessary validation methods from `express-validator`. +2. Define the validation rules for the request's body, query, or params. +3. Wrap the validation rules with the `validate` function and include it as middleware in your route definition. +4. Handle the request in the route handler, assuming the data is already validated. + +see the example [here](#using-validate). + +## Comparison with different approaches + +### Manual Validation + +Manual validation involves writing custom logic directly in the route handler to check and sanitize incoming data. While this approach provides flexibility, it often leads to repetitive, error-prone code that can clutter route handlers and make them harder to maintain. + +```javascript +app.post( + '/user', + (req, res) => { + if (!req.body.username || !req.body.password) { + return res.status(400).json({ error: 'Username and password are required' }); + } + + if (!utils.isEmail(req.body.username)) { + return res.status(400).json({ error: 'Invalid email address' }); + } + + if (req.body.password.length < 5) { + return res.status(400).json({ error: 'Password must be at least 5 characters long' }); + } + + const age = parseInt(req.body.age, 10); + if (isNaN(age) || age < 18) { + return res.status(400).json({ error: 'Age must be a number greater than or equal to 18' }); + } + + User.create({ + username: req.body.username, + password: req.body.password, + }).then(user => res.json(user)); + }, +); +``` + +### Using `express-validator` + +By leveraging `express-validator`, you can define validation rules declaratively, reducing boilerplate code and improving readability. + +```javascript +app.post( + '/user', + body('username').isEmail(), + body('password').isLength({ min: 5 }), + body('age').isInt({ min: 18 }).toInt(), + (req, res) => { + // Finds the validation errors in this request and wraps them in an object with handy functions + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + User.create({ + username: req.body.username, + password: req.body.password, + age: req.body.age, + }).then(user => res.json(user)); + }, +); +``` + +### Using `validate` + +The `validate` function further simplifies the use of `express-validator` by wrapping validation rules and handling errors automatically. It returns a `400 Bad Request` response if validation fails, reducing the need for manual error handling in route handlers. + +```javascript +const { validate } = require('middleware/validators'); +const asyncHandler = require('middleware/asyncHandler'); + +app.post( + '/user', + validate([ + body('username').isEmail(), + body('password').isLength({ min: 5 }), + body('age').isInt({ min: 18 }).toInt() + ]), + asyncHandler(async (req, res) => { + const user = await User.create({ + username: req.body.username, + password: req.body.password, + age: req.body.age, + }); + res.json(user); + }), +); +``` + +### Explanation of the Code + +1. **Validation Rules**: The `body('username').isEmail()`, `body('password').isLength({ min: 5 })`, `body('age').isInt({ min: 18 }).toInt()` define the validation logic for the `username`, `password`,and `age` fields. +2. **`validate` Middleware**: Wraps the validation and sanitization rules and handles errors automatically, returning a `400 Bad Request` response if validation fails. +3. **Async Handler**: The `asyncHandler` middleware ensures proper error handling for asynchronous operations in the route handler. + +By using the `validate` function, you can focus on implementing business logic in your route handlers while ensuring that all incoming data is valid and secure. Also ensures that correct error responses are sent back to the client when validation fails. + +## More Examples + +```javascript +const { validate } = require('middleware/validators'); +const { + query, param, body, checkSchema, +} = require('express-validator'); + + +validate([ + // integer between 1 and 100 + query('limit').isInt({ min: 1, max: 100 }).toInt(), + + // list of allowed values + query('type').isIn(['RAW_DATA', 'DATA_PRODUCT']).optional(), + query('sort_order').default('desc').isIn(['asc', 'desc']) + + // boolean value with default + // converts 'true' and 'false' strings to boolean + query('deleted').toBoolean().default(false), + + // date in ISO8601 format + query('created_at_start').isISO8601() + + // convert to BigInt + body('du_size').optional().notEmpty().customSanitizer(BigInt) + + // Array & size limits + body('datasets').isArray({ min: 1, max: 100 }), + + // validate objects in array + body('datasets.*.name').notEmpty(), + + // String length + body('name').optional().isLength({ min: 5 }), + +]), +``` \ No newline at end of file diff --git a/docs/api/02-data/cache.md b/docs/api/02-data/cache.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/02-data/index.md b/docs/api/02-data/index.md new file mode 100644 index 000000000..a417e82d9 --- /dev/null +++ b/docs/api/02-data/index.md @@ -0,0 +1,3 @@ +--- +title: Data +--- \ No newline at end of file diff --git a/docs/api/02-data/prisma.md b/docs/api/02-data/prisma.md new file mode 100644 index 000000000..09c9eef61 --- /dev/null +++ b/docs/api/02-data/prisma.md @@ -0,0 +1,114 @@ +# ORM + +[Prisma.js](https://www.prisma.io/) is used as the ORM (Object-Relational Mapping) and schema migration tool in this project. It simplifies database interactions by providing a type-safe API and automates schema migrations, ensuring consistency across environments. + +The `DATABASE_URL` environment variable is used to connect to the database. This is configured in the `.env` file. + +- **Database Schema**: Defined in `prisma/schema.prisma`. +- **Seeding**: Handled by `prisma/seed.js` with additional scripts in the `seed_data` directory. +- **Migration Commands**: + - `npx prisma migrate dev`: Creates a new migration during development. + - `npx prisma migrate deploy`: Applies migrations in production. +- **Seeding Command**: + - `npx prisma db seed`: Seeds the database with initial data. +- **Initialization**: The `db.js` file contains the setup for the Prisma client. + +## Gotchas + +### Auto Timestamp + +`now()` sets the current timestamp in the UTC time zone. Both `createdAt` and `updatedAt` fields are in UTC timezone. If the database is set to a different timezone, you may see future timestamps when connected to the db using a tool like dbeaver. + +This is not a problem when using prisma queries, as prisma automatically converts the timestamps to the local timezone. + +```prisma +model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean? @default(false) + createdAt DateTime @default(now()) @db.Timestamp(6) + updatedAt DateTime @updatedAt @default(now()) @db.Timestamp(6) +} +``` + +- **Important Note**: When performing raw SQL queries, avoid using `createdAt < now()`. Instead, use `createdAt < timezone('utc', now())` to ensure consistent time zone handling. + +### Aliasing and Excluding Columns + +Prisma does not support aliasing columns (like SQL's `AS` keyword) or excluding specific columns directly. Instead, you can transform the returned objects in JavaScript. + +Example: + +```javascript +const _ = require('lodash'); + +function transformUser(user) { + return _(user) + .set('roles', user.user_role.map((obj) => obj.roles.name)) // Transform and set new keys + .omit(['password', 'id', 'user_role']) // Remove unwanted keys + .value(); +} +``` + +For more details, refer to: +- [GitHub Discussion](https://github.com/prisma/prisma/discussions/14316) +- [Prisma Documentation](https://www.prisma.io/docs/concepts/components/prisma-client/excluding-fields) + +### Avoid `SELECT *` Queries + +Fetching all columns can negatively impact performance due to deserialization overhead, increased network transmission, and inefficiencies with non-inline columns (e.g., blobs, JSON). + +**Bad Example**: +```javascript +const users = await prisma.user.findMany(); +``` + +**Good Example**: +```javascript +const users = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + }, +}); +``` + +For more information, see [this source](https://glasp.co/youtube/p/a-deep-dive-in-how-slow-select-is). + +### Sorting with `nulls last` + +Prisma's sorting behavior depends on whether a column is nullable. For nullable fields, you must explicitly specify `nulls: 'last'` or `nulls: 'first'`. For non-nullable fields, including this option will throw an error. + +Example: + +```javascript +const buildOrderByObject = (field, sortOrder, nullsLast = true) => { + const nullable_order_by_fields = ['du_size', 'size']; + + if (!field || !sortOrder) { + return {}; + } + if (nullable_order_by_fields.includes(field)) { + return { + [field]: { sort: sortOrder, nulls: nullsLast ? 'last' : 'first' }, + }; + } + return { + [field]: sortOrder, + }; +}; +``` + +### Logging Queries + +Enable logging to debug and monitor Prisma queries: + +```javascript +const prisma = new PrismaClient({ + log: ['query', 'info', 'warn', 'error'], +}); +``` + +This will log all queries, warnings, and errors generated by the Prisma client. \ No newline at end of file diff --git a/docs/api/03-security/authentication.md b/docs/api/03-security/authentication.md new file mode 100644 index 000000000..1e8bb7827 --- /dev/null +++ b/docs/api/03-security/authentication.md @@ -0,0 +1,19 @@ +# Authentication + +The API uses IU CAS authnetication model. + + + +All the routes and sub-routers added after the [`authenticate`](src/middleware/auth.js) middleware in [index router](src/routes/index.js) require authentication. The routes that do not require authentication such as [auth routes](src/routes/auth.js) are added before this. + +The [`authenticate`](src/middleware/auth.js) middleware, parses the `Authorization` header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as `req.user` + +To add authentication to a single route: +```javascript +const { authenticate } = require('../middleware/auth'); + +router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => { + const user = await userService.findActiveUserBy('username', req.user.username); + // ... +})) +``` \ No newline at end of file diff --git a/docs/api/03-security/authorization.md b/docs/api/03-security/authorization.md new file mode 100644 index 000000000..0db7d8696 --- /dev/null +++ b/docs/api/03-security/authorization.md @@ -0,0 +1,105 @@ +# Authorization + +Role Based Access Control + +[accesscontrol](https://www.npmjs.com/package/accesscontrol) library is used to provide role based authorization to routes (resources). + +Roles in this application: +- user +- operator +- admin +- superadmin + +Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in [services/accesscontrols.js](src/services/accesscontrols.js). + +The goal of the [accessControl](src/middleware/auth.js) middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource. + +### A simple use case: + +**Objective**: Users with `user` role are only permitted to read and update thier own profile. Whereas, users with `admin` role can create new users, read & update any user's profile, and delete any user. + +**Role design**: +- roles: `admin`, `user` +- actions: CRUD +- resource: `user` + +```javascript +{ + admin: { + user: { + 'create:any': ['*'], + 'read:any': ['*'], + 'update:any': ['*'], + 'delete:any': ['*'], + }, + }, + user: { + user: { + 'read:own': ['*'], + 'update:own': ['*'], + }, + }, +} +``` + +**Permission check**: + +Code to check if the requester to is authorized to `GET /users/dduck`. This route is protected by `authenticate` middleware which attaches the requester profile to `req.user` if the token is valid. + +```javascript +const { authenticate } = require('../middleware/auth'); + +router.get('/:username', + authenticate, + asyncHandler(async (req, res, next) => { + + const roles = req.user.roles; + const resourceOwner = req.params.username; + const requester = req.user?.username; + + const permission = (requester === resourceOwner) + ? ac.can(roles).readOwn('user') + : ac.can(roles).readAny('user'); + + if (!permission.granted) { + return next(createError(403)); // Forbidden + } + else { + const user = await userService.findActiveUserBy('username', req.params.username); + if (user) { return res.json(user); } + return next(createError.NotFound()); + } + }), +); +``` + +readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only `user` role and is requesting the profile of other users, the request will be denied. + +### AccessControl Middleware Usage + +[accessControl](src/middleware/auth.js) middleware is a generic function to handle authorization for any action or resource with optional ownership checking. + +The above code can be written consicely with the help of accessControl middleware. + +`routes/*.js` +```javascript +// import middleware +const { authenticate, accessControl } = require('../middleware/auth'); + +// configre the middleware to authorize requests to user resource +// resource ownership is checked by default +// throws 403 if not authorized +const isPermittedTo = accessControl('user'); + +// +router.get( + '/:username', + authenticate, + isPermittedTo('read', { checkOwnerShip: true }), + asyncHandler(async (req, res, next) => { + const user = await userService.findActiveUserBy('username', req.params.username); + if (user) { return res.json(user); } + return next(createError.NotFound()); + }), +); +``` \ No newline at end of file diff --git a/docs/api/03-security/cookies.md b/docs/api/03-security/cookies.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/03-security/cors.md b/docs/api/03-security/cors.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/03-security/index.md b/docs/api/03-security/index.md new file mode 100644 index 000000000..6302d7be6 --- /dev/null +++ b/docs/api/03-security/index.md @@ -0,0 +1,3 @@ +--- +title: Security +--- \ No newline at end of file diff --git a/docs/api/04-deployment/docker-image-design.md b/docs/api/04-deployment/docker-image-design.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/04-deployment/index.md b/docs/api/04-deployment/index.md new file mode 100644 index 000000000..5aa5422b6 --- /dev/null +++ b/docs/api/04-deployment/index.md @@ -0,0 +1,3 @@ +--- +title: Deployment +--- \ No newline at end of file diff --git a/docs/api/05-performance/compression.md b/docs/api/05-performance/compression.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/05-performance/http-caching.md b/docs/api/05-performance/http-caching.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/05-performance/index.md b/docs/api/05-performance/index.md new file mode 100644 index 000000000..f60f941b5 --- /dev/null +++ b/docs/api/05-performance/index.md @@ -0,0 +1,3 @@ +--- +title: Performance +--- \ No newline at end of file diff --git a/docs/api/05-performance/instrumentation.md b/docs/api/05-performance/instrumentation.md new file mode 100644 index 000000000..503ea06a7 --- /dev/null +++ b/docs/api/05-performance/instrumentation.md @@ -0,0 +1,113 @@ +# Instrumentation + +Instrumentation is the process of collecting and storing data about the performance of your application. This data can be used to identify performance bottlenecks, monitor the health of your application, and make informed decisions about how to improve performance. + +`prom-client` is a popular library for instrumenting Node.js applications with Prometheus metrics. It provides a simple and efficient way to collect metrics and expose them for monitoring and alerting. + +## Metrics Middleware + +The `metricsMiddleware` is a middleware function provided by the `express-prom-bundle` library. It is responsible for collecting HTTP request metrics, such as response times, status codes, and request paths. These metrics are exposed in a format compatible with Prometheus, enabling easy integration with monitoring systems. + +### Configuration + +The `metricsMiddleware` is configured in the `core/metrics.js` file. Key configurations include: + +- **Autoregister**: Automatically registers metrics unless clustering is enabled. +- **Include Method**: Captures the HTTP method (e.g., GET, POST). +- **Include Path**: Captures the request path. +- **Normalize Path**: Normalizes paths to avoid high cardinality (e.g., `/users/:id` instead of `/users/123`). +- **Buckets**: Defines histogram buckets for response times, ranging from 30ms to 30s. + +### Metrics Collected + +The middleware collects the following metrics: +- **HTTP Request Duration**: Measures the time taken to process requests in a histogram format. +- **HTTP Status Codes**: Aggregates status codes into categories (e.g., `2xx`, `4xx`). +- **Request Paths**: Tracks metrics per normalized path. + +```plaintext +# HELP http_request_duration_seconds duration histogram of http responses labeled with: status_code, method, path +# TYPE http_request_duration_seconds histogram +http_request_duration_seconds_bucket{le="0.03",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="0.1",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="0.3",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="1",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="3",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="10",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="30",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_bucket{le="+Inf",status_code="2xx",method="GET",path="/datasets/"} 2 +http_request_duration_seconds_sum{status_code="2xx",method="GET",path="/datasets/"} 0.038533292 +http_request_duration_seconds_count{status_code="2xx",method="GET",path="/datasets/"} 2 +``` + +Default metrics about node.js process are also collected. To view all metrics: + +```bash +api> curl locahost:9999/metrics > metrics.prom +``` + +### Usage + +The middleware is added to the Express application in `app.js`: + +```javascript +const { metricsMiddleware } = require('./core/metrics'); +app.use(metricsMiddleware); +``` + +This ensures that all incoming requests are automatically instrumented. + +## Custom Metrics + +`core/metrics.js` + +```javascript +const authFailures = new client.Counter({ + name: 'auth_failures_total', + help: 'Total number of failed authentication attempts', + labelNames: ['auth_method', 'reason', 'client_id'], +}); +``` + +Add your custom metric to the `metrics.js` file. This example creates a counter metric to track the total number of failed authentication attempts. You can define custom labels to provide additional context for the metric. This metric will be automatically registered and sent to the Prometheus server on every scrape. + +### Include the Metric in Your Code + +`services/authService.js` + +```javascript +const metrics = require('../core/metrics'); + +function authenticateUser(username, password) { + if (!isValidCredentials(username, password)) { + metrics.authFailures.inc({ auth_method: 'password', reason: 'invalid_credentials', client_id: 'web' }); + throw new Error('Invalid credentials'); + } +} +``` + +## Clustered Metrics Aggregation + +In a clustered environment, metrics from all worker processes are aggregated in the master process. This ensures that metrics are consistent and accessible from a single endpoint. + +### Implementation + +The aggregation is implemented in `cluster.js`: + +- **Master Process**: Exposes the `/metrics` endpoint on a separate port for Prometheus to scrape aggregated metrics. +- **Worker Processes**: Collect metrics locally and send them to the master process. + +Example configuration in `cluster.js`: + +```javascript +const promBundle = require('express-prom-bundle'); +metricsApp.use('/metrics', promBundle.clusterMetrics()); +``` + +Without this setup, metrics would be fragmented across worker processes, making it difficult to monitor the entire system. + +### Benefits + +- Centralized metrics collection in clustered environments. +- Simplifies monitoring and alerting. +- Ensures accurate and consistent metrics across all processes. \ No newline at end of file diff --git a/docs/api/05-performance/nodejs_metrics.md b/docs/api/05-performance/nodejs_metrics.md new file mode 100644 index 000000000..0fe2b7775 --- /dev/null +++ b/docs/api/05-performance/nodejs_metrics.md @@ -0,0 +1,186 @@ +# Node.js Metrics Documentation + +This document provides an overview of the performance metrics collected by the application. + +## `nodejs_gc_duration_seconds` +**Type**: Histogram +**Source**: Derived from `perf_hooks.PerformanceObserver` ([Node.js Documentation](https://nodejs.org/api/perf_hooks.html#garbage-collection-gc-details)) + +**Description**: +Measures the duration of garbage collection (GC) events, categorized by type: `major`, `minor`, `incremental`, or `weakcb`. + +--- + +## `nodejs_active_resources` +**Type**: Gauge +**Source**: Derived from `process.getActiveResourcesInfo()` + +**Description**: +Tracks the number of active resources currently keeping the event loop alive. +- `nodejs_active_resources`: Count of unique active resources. +- `nodejs_active_resources_total`: Total count of all active resources. + +**Usage**: +- Monitor the number of active resources to identify potential event loop bottlenecks. +- Set thresholds to alert when the number of active resources exceeds a defined limit. + +--- + +### Deprecated Metrics: `nodejs_active_requests` and `nodejs_active_handles` +**Status**: Deprecated in Node.js v23 ([Deprecation Notice](https://nodejs.org/api/deprecations.html#DEP0161)) +**Replacement**: Use `nodejs_active_resources` and `nodejs_active_resources_total`. + +--- + +## `process_start_time_seconds` +**Type**: Gauge +**Source**: Derived from `process.uptime()` + +**Description**: +Tracks the number of seconds since the Node.js process started. + +--- + +## `process_open_fds` +**Type**: Gauge +**Source**: Derived from `fs.readdirSync('/proc/self/fd').length - 1` + +**Description**: +Monitors the number of open file descriptors used by the Node.js process. + +**Usage**: +- Identify potential file descriptor leaks or excessive file usage. +- Compare with `process_max_fds` to ensure the process is not nearing the OS limit. + +**Caveats**: +- `readdirSync` is a synchronous operation and may block the event loop if there are many file descriptors. +- `/proc/self/fd` is in-memory (procfs), so it is faster than disk I/O but still incurs system call overhead. + +--- + +## `process_max_fds` +**Type**: Gauge +**Source**: Derived from `/proc/self/limits` + +**Description**: +Represents the maximum number of file descriptors the process can open, as configured by the OS. + +**Usage**: +- Compare `process_open_fds` with `process_max_fds` to detect if the process is nearing the limit. +- Helps identify potential file descriptor management issues. + +--- + +## `process_cpu_seconds_total` +**Type**: Counter +**Source**: Derived from `@opentelemetry/api` + +**Description**: +Tracks the total CPU time (user + system) consumed by the process. +**TODO**: Add implementation details. + +--- + +## `osMemoryHeapLinux` +**Type**: Gauge +**Source**: Derived from `/proc/self/status` + +**Metrics**: +- `process_resident_memory_bytes` (VmRSS): Physical memory in use. (real footprint in RAM). +- `process_virtual_memory_bytes` (VmSize): Total virtual memory allocated (includes swapped-out and unused portions). +- `nodejs_heap_size_total_bytes` (VmData): Heap memory size. + +**Usage**: +- Monitor memory usage to detect potential memory leaks or excessive memory consumption. + +--- + +## `heapSpacesSizeAndUsed` +**Type**: Gauge +**Source**: Derived from `v8.getHeapSpaceStatistics()` + +**Metrics**: +- `nodejs_heap_space_size_total_bytes`: Total size of each heap space. +- `nodejs_heap_space_size_used_bytes`: Used size of each heap space. +- `nodejs_heap_space_size_available_bytes`: Available size in each heap space. + +**Heap Spaces**: +- `new_space`: The new space is where new objects are allocated. It is a semi-space garbage collector. +- `old_space`: The old space is where long-lived objects are allocated. It is a mark-sweep garbage collector. +- `code_space`: The code space is where compiled code is stored. +- `map_space`: The map space is where object property maps are stored. +- `large_object_space`: The large object space is where large objects are allocated. + + +**Usage**: +- Monitor memory usage per heap space to identify memory bottlenecks or leaks. +- Focus on `new_space` for early detection of memory issues. + +--- + +## `heapSizeAndUsed` +**Type**: Gauge +**Source**: Derived from `process.memoryUsage()` + +**Metrics**: +- `nodejs_heap_size_total_bytes`: The total heap size allocated for the Node.js (V8) process. +- `nodejs_heap_size_used_bytes`: The amount of heap memory used by the Node.js (V8) process. +- `nodejs_external_memory_bytes`: refers to the memory usage of C++ objects bound to JavaScript objects managed by V8. + + +**Usage**: +- Monitor overall heap usage to detect memory leaks or excessive memory consumption. +- Compare with `heapSpacesSizeAndUsed` for detailed heap space analysis. + +**Caveats**: +- `process.memoryUsage()` iterates over memory pages, which may block the event loop for large heaps. + +--- + +Here's a refined and professional version of your documentation: + +--- + +## `eventLoopLag` + +**Type:** Gauge + +#### Metrics +- `nodejs_eventloop_lag_seconds` +- `nodejs_eventloop_lag_min_seconds` +- `nodejs_eventloop_lag_max_seconds` +- `nodejs_eventloop_lag_mean_seconds` +- `nodejs_eventloop_lag_stddev_seconds` +- `nodejs_eventloop_lag_p50_seconds` +- `nodejs_eventloop_lag_p90_seconds` +- `nodejs_eventloop_lag_p95_seconds` + +#### Description +`eventLoopLag` measures the delay between scheduling a timer (`setImmediate`) and its corresponding callback execution. This provides insight into how long the event loop is blocked by other operations, serving as an indicator of potential performance bottlenecks in a Node.js application. + +#### Computation of `nodejs_eventloop_lag_seconds` +The `nodejs_eventloop_lag_seconds` metric is computed as follows [(reference)](https://github.com/siimon/prom-client/issues/561): +1. A `Gauge` is created in `eventLoopLag.js` with a custom `collect` method. +2. When metrics are requested, the `collect` method is invoked, capturing a high-resolution timestamp. This occurs at the start of the metrics collection process. +3. The method then schedules another measurement using `setImmediate`. Since this executes in the next event loop iteration, it occurs after metrics collection and reporting have completed. +4. As the `collect` method does not return a promise, the computed value is only recorded in the subsequent metrics collection cycle. + +This means the metric measures the delay from the start of metrics collection until the next event loop iteration. However, it is not updated in real time and only reflects values from the previous collection cycle. + +#### Computation of Other Metrics +Other event loop lag metrics (e.g., min, max, mean, percentiles) are derived from `perf_hooks.monitorEventLoopDelay()`. +- The minimum measurable delay depends on the timer resolution, which defaults to **10ms** but can be adjusted using the `eventLoopMonitoringPrecision` configuration option. + +#### Use Cases +- **Performance Monitoring**: Helps detect high event loop lag, which may indicate that the application is being blocked by long-running operations. +- **Bottleneck Identification**: A consistently high event loop lag suggests potential issues such as synchronous operations blocking the event loop. + +#### Limitations +- **Delayed Updates**: + - `nodejs_eventloop_lag_seconds` is only updated during metrics collection. The reported value reflects the lag from the previous cycle, not the current one. + - In high-throughput applications (e.g., Express-based servers), event loop lag typically increases in short bursts (e.g., during sudden spikes in requests). If metrics are collected every **30 seconds**, the reported value may not accurately represent real-time lag. +- **Resolution Constraints**: + - Metrics derived from `perf_hooks.monitorEventLoopDelay()` have a **10ms resolution** by default. While this provides continuous monitoring, it may not capture sub-millisecond variations in event loop lag. + + + diff --git a/docs/api/06-integrations/api-clients.md b/docs/api/06-integrations/api-clients.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/06-integrations/index.md b/docs/api/06-integrations/index.md new file mode 100644 index 000000000..0884d2242 --- /dev/null +++ b/docs/api/06-integrations/index.md @@ -0,0 +1,3 @@ +--- +title: Integrations +--- \ No newline at end of file diff --git a/docs/api/06-integrations/swagger-openapi.md b/docs/api/06-integrations/swagger-openapi.md new file mode 100644 index 000000000..6a6d29162 --- /dev/null +++ b/docs/api/06-integrations/swagger-openapi.md @@ -0,0 +1,13 @@ +## OpenAPI Documentation + +Auto-generated OpenAPI documentation for the API routes. + +1. Add `// #swagger.tags = ['']` comment to the code of the route handler and replace `sub-router` with a valid name that describes the family of routes (ex: User, Dataset, etc). +2. Run `npm run swagger-autogen` to generate the documentation. +3. Visit `http://:/docs` + +Files: +- `swagger.js` - script that generates the documentation. Configures the output file, the router to generate routes and other common config. +- `swagger_output.json` - generated routes, not included in the version control. + +Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284 \ No newline at end of file diff --git a/docs/api/07-development/auto-reload-server.md b/docs/api/07-development/auto-reload-server.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/07-development/db-seed-data.md b/docs/api/07-development/db-seed-data.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/07-development/index.md b/docs/api/07-development/index.md new file mode 100644 index 000000000..2df401dfa --- /dev/null +++ b/docs/api/07-development/index.md @@ -0,0 +1,3 @@ +--- +title: Development +--- \ No newline at end of file diff --git a/docs/api/07-development/linting.md b/docs/api/07-development/linting.md new file mode 100644 index 000000000..09e98f07b --- /dev/null +++ b/docs/api/07-development/linting.md @@ -0,0 +1,6 @@ +# Linting + +Eslint rules inherited from `eslint-config-airbnb-base` and [Lodash-fp ruleset](https://www.npmjs.com/package/eslint-plugin-lodash-fp). + + +Consistent Coding styles with editorconfig \ No newline at end of file diff --git a/docs/api/index-draft.md b/docs/api/index-draft.md deleted file mode 100644 index a7b6c97b5..000000000 --- a/docs/api/index-draft.md +++ /dev/null @@ -1,469 +0,0 @@ -# API Documentation - -## Overview - -The Bioloop API is a RESTful service built with Express.js that provides endpoints for data management, user authentication, and task processing. This guide covers setup, authentication, endpoints, and best practices. - -## Table of Contents - -1. [Getting Started](#getting-started) -2. [Authentication](#authentication) -3. [Authorization](#authorization-role-based-access-control) -4. [Error Handling](#error-handling) -5. [Request Flow](#request-flow) -6. [Project Structure](#project-structure) -7. [Configuration](#config) -8. [Development](#development) - -## Getting Started - -1. Set up environment configuration: -```bash -cp .env.example .env -``` - -2. Configure required environment variables: -```env -NODE_ENV=development|docker|production -DATABASE_PASSWORD=your_password -DATABASE_URL=postgresql://user:pass@host:5432/db -``` - -## Running the API - -### Using Docker (Recommended) -```bash -# Start API and database -docker compose up postgres api -d - -# Environment settings -NODE_ENV=docker -DATABASE_URL="postgresql://appuser:example@postgres:5432/app?schema=public" -``` - -### Local Development -```bash -# Start local Postgres -# Create database and user -createdb app -createuser -P appuser # Set password to 'example' -psql -d app -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO appuser;" - -# Environment settings -NODE_ENV=default -DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public" - -# Install dependencies and start -pnpm install -pnpm start -``` - -## Core Features - -- **Authentication & Authorization** - - IU CAS authentication - - JWT-based session management - - Role-based access control (RBAC) - -- **Data Handling** - - Request validation - - Query/body parsing - - Response compression - - CORS support - -- **Database** - - Prisma ORM - - PostgreSQL support - - Migration management - -- **Development** - - Auto-generated Swagger docs - - Comprehensive logging - - Request validation - - Error handling - -## Development Features - -- Hot reload with Nodemon -- ESLint configuration -- EditorConfig for consistent styling -- PM2 for production process management -- Docker support - -Note: The API assumes a reverse proxy handles security headers (no Helmet module) - -## Request Flow - -Each API request goes through the following middleware pipeline: - -1. **Request Creation** - - Express creates request object with query string, parameters, body, headers - - [Express Request Docs](https://expressjs.com/en/4x/api.html#req) - -2. **Request Processing** ([app.js](src/app.js)) - - Parse body, query parameters, cookies - - Handle CORS - - Initial routing - -3. **Authentication** - - Validate JWT token - - Attach user profile to `req.user` - - Return 401 if unauthorized - -4. **Authorization** - - Check user permissions - - Attach permission object to `req.permission` - - Return 403 if forbidden - -5. **Validation** - - Validate request parameters - - Return 400 if invalid - -6. **Business Logic** - - Execute route handler - - Generate response - -7. **Response Processing** - - Apply gzip compression - - Set response headers - - Send response to client - -### Error Handling Flow - -When errors occur, the following handlers process them in order: - -1. **404 Handler** - - Catches routing failures - - Returns 404 response - -2. **Prisma Not Found Handler** - - Catches Prisma record not found errors - - Returns 404 response - -3. **Global Error Handler** - - Catches all other errors - - Returns appropriate error response (usually 500) - - Applies compression - - Sets response headers - -Note: Some routes (e.g., `/health`, `/auth`) bypass authentication - -## Project Structure - -``` -src/ -├── index.js # Application entry point -├── app.js # Express app configuration -├── routes/ # Route definitions -│ ├── index.js # Main router -│ └── *.js # Feature-specific routes -├── middleware/ # Express middleware -├── services/ # Business logic -│ └── logger.js # Winston logger -├── config/ # Configuration files -└── utils/ # Helper functions - -prisma/ -├── schema.prisma # Database schema -└── seed.js # Database seeding -``` - -## Error Handling - -The API implements a comprehensive error handling system based on [Express Error Handling](https://expressjs.com/en/guide/error-handling.html). - -### Key Concepts - -- Express auto-handles synchronous errors -- Async errors require explicit handling -- Calling `next(err)` triggers error middleware -- Error handlers receive `(err, req, res, next)` - -### Async Error Handling - -The `asyncHandler` middleware simplifies async error handling: - -```javascript -// Good - Using asyncHandler -router.get('/user', asyncHandler(async (req, res) => { - const user = await userService.findActiveUserBy('username', req.query.username); - res.json(user); -})); - -// Bad - Manual try/catch -router.get('/user', async (req, res, next) => { - try { - const user = await userService.findActiveUserBy('username', req.query.username); - res.json(user); - } catch(err) { - next(err); - } -}); -``` - - -### Error Handler Types - -1. **Default Handler** - - Sets status code from `err.status` - - Sets appropriate status message - - Returns stack trace in development - - Returns generic message in production - -2. **Custom Handler** - - Logs errors appropriately - - Exposes safe error messages - - Handles HTTP-specific errors - - Manages 4xx vs 5xx errors - -3. **Prisma Handler** - - Converts Prisma errors to HTTP responses - - Handles "not found" cases - - Maps database errors to appropriate status codes - -### Error Response Examples - -```javascript -// 400 Bad Request -throw createError(400, 'Invalid input'); -// Response: { "message": "Invalid input" } - -// 404 Not Found -throw createError.NotFound(); -// Response: { "message": "Not Found" } - -// 500 Internal Error -throw new Error('Database connection failed'); -// Response: { "message": "Internal Server Error" } -``` - -### Using HTTP Errors - -The `http-errors` module provides a clean API for error creation: - -```javascript -// Using status code -const err = createError(404, 'User not found'); -next(err); - -// Using named constructor -next(createError.NotFound('User not found')); - -// With custom options -next(createError(502, 'Gateway error', { expose: true })); -``` - -See [http-errors documentation](https://github.com/jshttp/http-errors) for more constructors. - - -### Prisma Error Handling - -The API includes middleware to handle Prisma-specific errors: - -```javascript -// Before - Manual Prisma error handling -router.delete('/:username', asyncHandler(async (req, res, next) => { - try { - const user = await userService.softDeleteUser(req.params.username); - res.json(user); - } catch(e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && - e?.meta?.cause?.includes('not found')) { - return next(createError.NotFound()); - } - return next(e); - } -})); - -// After - Using prismaNotFoundHandler middleware -router.delete('/:username', asyncHandler(async (req, res) => { - const user = await userService.softDeleteUser(req.params.username); - res.json(user); -})); -``` - -The `prismaNotFoundHandler` middleware automatically converts Prisma "not found" errors to HTTP 404 responses. - - -## Configuration - -The API uses [node-config](https://github.com/node-config/node-config) for configuration management: - -### Configuration Files -``` -config/ -├── default.json # Default settings -├── production.json # Production overrides -└── custom-environment-variables.json # ENV mapping -``` - -### Configuration Priority -1. Command line arguments -2. Environment variables -3. {NODE_ENV}.json -4. default.json - -### Environment Variables -```javascript -// Load .env file -require('dotenv-safe').config(); -``` - -See the [node-config documentation](https://github.com/node-config/node-config/wiki) for more details. - -## Authentication - -The API implements IU CAS authentication with JWT session management. - -### Authentication Flow -![Auth Flow](../public/api_auth.png) - -### Implementation - -1. **Protected Routes** - - All routes after `authenticate` middleware require authentication - - Public routes (e.g., `/health`, `/auth`) are registered first - -2. **JWT Verification** - - Extracts bearer token from Authorization header - - Verifies token cryptographically - - Attaches user data to `req.user` - -### Usage Example -```javascript -const { authenticate } = require('../middleware/auth'); - -// Protect single route -router.post('/refresh_token', - authenticate, - asyncHandler(async (req, res) => { - const user = await userService.findActiveUserBy('username', req.user.username); - // Handle token refresh - }) -); - -// Protect all routes in a router -router.use(authenticate); -router.get('/profile', ...); -router.put('/settings', ...); -``` - -## Request Validation - -The API uses [express-validator](https://express-validator.github.io/docs/) for request validation. - -### Benefits -- Declarative validation rules -- Reduces boilerplate validation code -- Consistent error handling -- Type coercion and sanitization - -### Example Usage - -```javascript -const validate = require('middleware/validators') - -// Route with validation -app.post('/user', - validate([ - body('username').isEmail(), - body('password').isLength({ min: 5 }), - body('role').isIn(['user', 'admin']), - ]), - asyncHandler(async (req, res) => { - const user = await User.create(req.body); - res.json(user); - }) -); - -// Validation error response -{ - "errors": [ - { - "msg": "Invalid email", - "param": "username", - "location": "body" - } - ] -} -``` - -## Role-Based Access Control - -The API implements RBAC using the [accesscontrol](https://www.npmjs.com/package/accesscontrol) library. - -### Roles -- user: Basic access -- operator: Enhanced access -- admin: Administrative access -- superadmin: Full system access - -### Permission Scopes -- own: Access to user's own resources -- any: Access to any user's resources - -### Example: User Profile Access - -```javascript -// Permission configuration -const permissions = { - admin: { - user: { - 'create:any': ['*'], - 'read:any': ['*'], - 'update:any': ['*'], - 'delete:any': ['*'], - }, - }, - user: { - user: { - 'read:own': ['*'], - 'update:own': ['*'], - }, - }, -}; - -// Route implementation -const isPermittedTo = accessControl('user'); - -router.get('/:username', - authenticate, - isPermittedTo('read', { checkOwnership: true }), - asyncHandler(async (req, res) => { - const user = await userService.findActiveUserBy('username', req.params.username); - res.json(user); - }) -); -``` - -This ensures: -- Admins can access any user profile -- Users can only access their own profile -- Unauthorized access returns 403 - -## API Documentation - -The API uses Swagger for automatic documentation generation. - -### Setup -1. Add swagger tags to routes: -```javascript -// #swagger.tags = ['Users'] -router.get('/users', ...); -``` - -2. Generate documentation: -```bash -npm run swagger-autogen -``` - -3. View documentation: -``` -http://:/docs -``` - -### Files -- `swagger.js`: Configuration and generation script -- `swagger_output.json`: Generated API documentation - -For more details, see [Swagger Auto-Gen Guide](https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284) diff --git a/docs/api/index.md b/docs/api/index.md index 0a7083f5e..ff9aec4ba 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,424 +1,4 @@ -# API - -## Getting Started - -Create a `.env` file from the template `.env.example`: -```bash -cp .env.example .env -``` -and populate the config values to all the keys. - -### Running using docker -In the developement environment with docker this is the content of `.env` file - -```bash -NODE_ENV=docker -DATABASE_PASSWORD='example' -DATABASE_URL="postgresql://appuser:example@postgres:5432/app?schema=public" -``` -From the project root run: `docker compose up postgres api -d` to start both the API server and the database - -### Running on host machine - -Start a postgres db server on localhost and create a database `app` and user `appuser` (password: `example`) with write premissions to public schema. In this local environment, the content of `.env` file is: - -```bash -NODE_ENV=default -DATABASE_PASSWORD='example' -DATABASE_URL="postgresql://appuser:example@localhost:5432/app?schema=public" -``` - -Run `pnpm install` and `pnpm start` to start the API server. - -## Features: -- Query and body parser, cookie parser, response compression, and CORS -- [Error Handling](#error-handling) - - [Async error handler](#asynchronous-error-handler) - - [Default error handler](#the-default-error-handler) - - [Prisma not found handler](#prisma-not-found-error-handler) - - [404 Not Found handler](#404-handler) -- Logger - - log incoming requests - - multi-level logger: ex: `logger.info()` - - log timestamps -- [Hierarchical layered configuration](#config) -- [IU CAS authentication](#authentication) -- JWT based stateless session management -- [Role Based Access Control](#authorization-role-based-access-control) -- [Request Validation](#request-validation) -- [Auto generated swagger documentation](#auto-generated-swagger-documentation) -- Prisma + Postgres ORM - -Developer Exprience -- Auto reload: `nodemon index.js` -- [Linting](#linting) - - auto highligt linting errors - - format on save -- Consistent Coding styles with editorconfig - -Production deployment: -- pm2 -- docker - -Assumptions: -- there is a reverse proxy which handles security headers as we are not using `helmet` module. - -## Typical request flow through the Express Server -1. Express creates a [`request`](https://expressjs.com/en/4x/api.html#req) object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on. -2. [app.js](src/app.js) - The body, query parameters, and cookies are parsed and converted to objects and `req` object is updated. -3. [app.js](src/app.js) - CORS? -4. [Index Router](src/routers/index.js) - Intial [routing](https://expressjs.com/en/guide/routing.html) is performed to select a sub-router to send the request to. -5. [Authentication](#authentication) - Validate JWT and attach user profile to `req.user` or send 401 error response*. -6. [AceessControl](#authorization-role-based-access-control) - Determine whether the requester has enough permissions to perform the desired operation on a particular resource and attach the permission object to `req.permission` or send 403 error response. -7. [Request Validation](#request-validation) - Validate if the request query, params, or the body is in expected format or send 400 error. -8. [Async Handler](#asynchronous-error-handler) - Envolpe the business logic route middleware to catch async error and propagate them to global error handler. -9. **Route Handler - Business Logic** - create and send the response. -10. [Compression](https://expressjs.com/en/resources/middleware/compression.html) - Apply gZip compression to the response body. -11. Express server sets some default headers and sends the [response](https://expressjs.com/en/4x/api.html#res) to the client. - - -### When something goes wrong - -10. [404 Handler](#404-handler) - Handle routing failures and send 404 error response. -11. [Prisma Not Found handler](#prisma-not-found-error-handler) - Handle not found prisma errors and send 404 error response. -12. [Global error handler](#custom-default-error-handler) - Handle all other errors and send 500 error response. -13. [Compression](https://expressjs.com/en/resources/middleware/compression.html) - Apply gZip compression to the response body. -14. Express server sets some default headers and sends the [response](https://expressjs.com/en/4x/api.html#res) to the client. - -\* For the routes that are registered before `authenticate` such as `/health` and `/auth`, this middleware not invoked. - -## Project Structure -files in `src/` -- `index.js` - import app and start -- `app.js` - create and configure express application -- `routes/index.js` - main router -- `routes/*.js` - modular routes -- `middleware/*.js` - express middleware functions -- `services/*.js` - common code specific to this project seperated by usage in router -- `services/logger.js` - winston logger -- `config/*.json` - hierarchical configuration -- `prisma/schema.prisma` - Data definitions -- `prisma/seed.js` - code to initialize tables with some data -- `utils/index.js` - non-specific common code - -## Error Handling -Source: [Express Error Handling](https://expressjs.com/en/guide/error-handling.html) - -- Express automatically handles errors thrown in the synchronous code, however it cannot catch errors thrown from asynchronous code (in versions below 5). These have to be caught and passed on to the `next` function. -- The error thrown from the sync code in the middleware are handled and passed on to the `next` automatically. -- When `next` is called with any argument except `'route'`, express assumes it is due to an error and skips any remaining non-error handling routing and middleware functions. - -### Asynchronous Error Handler -`asyncMiddleware` in [middleware/error.js](src/middleware/asyncHandler.js) - -Usage: Wrap the route handler middleware with `asyncHandler` to produce a middleware funtion that can catch the asynchronous error and pass on to the default error handler. - - -```javascript -const asyncHandler = require('../middleware/asyncHandler'); - -router.get('/user', asyncHandler(async (req, res, next) => { - const user = await userService.findActiveUserBy('username', req.query.username); - res.json(user) -})) -``` - -instead of - -```javascript -router.get('/user', async (req, res, next) => { - try { - const user = await userService.findActiveUserBy('username', req.query.username); - res.json(user) - } catch(err) { - next(err) - } -}) -``` - - -### The Default Error Handler -- The default error handler is added at the end of the middleware function stack -- The `res.statusCode` is set from `err.status` (or `err.statusCode`). If this value is outside the 4xx or 5xx range, it will be set to 500. -- The `res.statusMessage` is set according to the status code. -- The body will be the HTML of the status code message when in production environment, otherwise will be `err.stack`. (environment variable NODE_ENV=production) - -### Custom Default Error Handler -- `errorHandler` in [middleware/error.js](src/middleware/error.js) -- Logs error to console -- send actual message to client only if `err.expose` is true otherwise send a generic Internal server error. For http errors such as (`throw createError(400, 'foo bar')`), the client receives `{"message":"foo bar"}` with status code to 400. -- For non http errors such as `throw new Error('business logic error')`, only the `err.message` is set others are not. For such error, this handler will send a generic message. Client's will not see `business logic error` in thier response object. -- Does not log to console stack trace for 4xx errors - -### 404 handler -- `notFound` in [middleware/error.js](src/middleware/error.js) - -### http-errors module: -- Helps to create http specific error objects which can be thrown or passed to next -```javascript -err = createError(404, 'user not found') -return next(err) -``` -- Provides list of constructors to make the code readable - https://github.com/jshttp/http-errors - -```javascript -return next(createError.NotFound()) -``` -this will automatically set correct error message based on the constructor. - -- Create an error with expose being true: `createError(502, 'foo', { expose: true })` - - -### Prisma Not Found Error Handler -Prisma returns opaque error objects from the underlying query engine when DB queries fail. One such common error that must be handled everytime a DB query is made is the **Not Found** error. - -HTTP semantics require that if a resource cannot be found either while retrieving, updating or deleting the response should be sent 404 status code. In order to achieve this, the errors from the prisma code have to be caught and analysed for the **Not Found** errors. - -A typical example of handling a not found error and returning 404 response: - -```javascript -router.delete( - '/:username', - asyncHandler(async (req, res, next) => { - try{ - const deletedUser = await userService.softDeleteUser(req.params.username); - res.json(deletedUser); - } catch(e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - if (e?.meta?.cause?.includes('not found')) { - return next(createError.NotFound()); - } - } - return next(e); - } - }), -); -``` - -Here, the errors other than not found are propagated to the error handler by `next(e)`. - -The try-catch code can be refactored to a middleware that intercepts all errors before the Custom default error handler. The above code after refactoring looks like: - -`app.js` -```javascript -const { prismaNotFoundHandler } = require('./middleware/error'); -app.use(prismaNotFoundHandler); -``` - -`routes/*.js` -```javascript -router.delete( - '/:username', - asyncHandler(async (req, res, next) => { - const deletedUser = await userService.softDeleteUser(req.params.username); - res.json(deletedUser); - }), -); -``` -Now, the routes' business logic code is cleaner and 404s are automatically when prisma ORM throws not found errors. If you want to send other HTTP status codes, intercept the prisma error in your route handler without propagating it. - - -## Linting - -Eslint rules inherited from `eslint-config-airbnb-base` and [Lodash-fp ruleset](https://www.npmjs.com/package/eslint-plugin-lodash-fp). - -## Config - -Uses config module - https://github.com/node-config/node-config - -Configurations are stored in [configuration files](https://github.com/node-config/node-config/wiki/Configuration-Files) within your application, and can be overridden and extended by [environment variables](https://github.com/lorenwest/node-config/wiki/Environment-Variables), [command line parameters](https://github.com/node-config/node-config/wiki/Command-Line-Overrides), or [external sources](https://github.com/lorenwest/node-config/wiki/Configuring-from-an-External-Source). - -config files: `default.json`, `production.json`, `custom-environment-variables.json` in `./config/` directory. - -precdence of config: command line > environment > {NODE_ENV}.json > default.json - -The properties to read and override from environment is defined in `custom-environment-variables.json` - -### Loading environment variables - -```javascript -require('dotenv-safe').config(); -``` - -## Authentication - -The API uses IU CAS authnetication model. - - - -All the routes and sub-routers added after the [`authenticate`](src/middleware/auth.js) middleware in [index router](src/routes/index.js) require authentication. The routes that do not require authentication such as [auth routes](src/routes/auth.js) are added before this. - -The [`authenticate`](src/middleware/auth.js) middleware, parses the `Authorization` header for the bearer token and cryptographically verifies the JWT. If the JWT is deemed valid, the payload is decoded and added to the request as `req.user` - -To add authentication to a single route: -```javascript -const { authenticate } = require('../middleware/auth'); - -router.post('/refresh_token', authenticate, asyncHandler(async (req, res, next) => { - const user = await userService.findActiveUserBy('username', req.user.username); - // ... -})) -``` - -## Request Validation - -Uses [express-validator](https://express-validator.github.io/docs/) to validate if the request query, params, or the body is of the expected format and has acceptable values. This module helps to write declarative code that reduces repeatitive Spaghetti safety checking code inside the route handler. The route can now confidently presume that all of the required properties/keys of `req.params`, `req.query`, or `req.body` exist and have appropriate values and optional keys set to default values. - - -Using the [`validate`](src/middleware/validators.js) higher order function, the error checking code is factored out from the route specific middleware functions. - -```javascript -app.post( - '/user', - body('username').isEmail(), - body('password').isLength({ min: 5 }), - (req, res) => { - // Finds the validation errors in this request and wraps them in an object with handy functions - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - User.create({ - username: req.body.username, - password: req.body.password, - }).then(user => res.json(user)); - }, -); -``` - -becomes - -```javascript -const validate = require('middleware/validators') -app.post( - '/user', - validate([ - body('username').isEmail(), - body('password').isLength({ min: 5 }), - ]), - asyncHandler(async (req, res) => { - const user = await User.create({ - username: req.body.username, - password: req.body.password, - }); - res.json(user); - }, -)); -``` - -## Authorization: Role Based Access Control - -[accesscontrol](https://www.npmjs.com/package/accesscontrol) library is used to provide role based authorization to routes (resources). - -Roles in this application: -- user -- operator -- admin -- superadmin - -Each role defines CRUD permissions on resources with two scopes: "own" and "any". These are configured in [services/accesscontrols.js](src/services/accesscontrols.js). - -The goal of the [accessControl](src/middleware/auth.js) middleware is to determine from an incoming request whether the requester has enough permissions to perform the desired operation on a particular resource. - -### A simple use case: - -**Objective**: Users with `user` role are only permitted to read and update thier own profile. Whereas, users with `admin` role can create new users, read & update any user's profile, and delete any user. - -**Role design**: -- roles: `admin`, `user` -- actions: CRUD -- resource: `user` - -```javascript -{ - admin: { - user: { - 'create:any': ['*'], - 'read:any': ['*'], - 'update:any': ['*'], - 'delete:any': ['*'], - }, - }, - user: { - user: { - 'read:own': ['*'], - 'update:own': ['*'], - }, - }, -} -``` - -**Permission check**: - -Code to check if the requester to is authorized to `GET /users/dduck`. This route is protected by `authenticate` middleware which attaches the requester profile to `req.user` if the token is valid. - -```javascript -const { authenticate } = require('../middleware/auth'); - -router.get('/:username', - authenticate, - asyncHandler(async (req, res, next) => { - - const roles = req.user.roles; - const resourceOwner = req.params.username; - const requester = req.user?.username; - - const permission = (requester === resourceOwner) - ? ac.can(roles).readOwn('user') - : ac.can(roles).readAny('user'); - - if (!permission.granted) { - return next(createError(403)); // Forbidden - } - else { - const user = await userService.findActiveUserBy('username', req.params.username); - if (user) { return res.json(user); } - return next(createError.NotFound()); - } - }), -); -``` - -readOwn permission is verified against user roles if the requester and resource owner are the same, otherwise readAny permission is examined. If the requester has only `user` role and is requesting the profile of other users, the request will be denied. - -### AccessControl Middleware Usage - -[accessControl](src/middleware/auth.js) middleware is a generic function to handle authorization for any action or resource with optional ownership checking. - -The above code can be written consicely with the help of accessControl middleware. - -`routes/*.js` -```javascript -// import middleware -const { authenticate, accessControl } = require('../middleware/auth'); - -// configre the middleware to authorize requests to user resource -// resource ownership is checked by default -// throws 403 if not authorized -const isPermittedTo = accessControl('user'); - -// -router.get( - '/:username', - authenticate, - isPermittedTo('read', { checkOwnerShip: true }), - asyncHandler(async (req, res, next) => { - const user = await userService.findActiveUserBy('username', req.params.username); - if (user) { return res.json(user); } - return next(createError.NotFound()); - }), -); -``` - -## Auto-generated Swagger Documentation - -1. Add `// #swagger.tags = ['']` comment to the code of the route handler and replace `sub-router` with a valid name that describes the family of routes (ex: User, Dataset, etc). -2. Run `npm run swagger-autogen` to generate the documentation. -3. Visit `http://:/docs` - -Files: -- `swagger.js` - script that generates the documentation. Configures the output file, the router to generate routes and other common config. -- `swagger_output.json` - generated routes, not included in the version control. - -Source: https://medium.com/swlh/automatic-api-documentation-in-node-js-using-swagger-dd1ab3c78284 +--- +title: API +order: 3 +--- \ No newline at end of file diff --git a/docs/api/introduction.md b/docs/api/introduction.md new file mode 100644 index 000000000..ff2432993 --- /dev/null +++ b/docs/api/introduction.md @@ -0,0 +1,119 @@ +--- +title: Introduction +order: 0 +--- +# Introduction + +Developing robust and maintainable backend services requires a structured approach without unnecessary complexity. This framework extends Express.js by providing a set of utility functions that facilitate the development of production-ready applications. No additional abstractions have been introduced, ensuring that developers familiar with Express can use it without a learning curve. Essential features such as validation, authentication, logging, and metrics are integrated, offering a streamlined development experience. A fully configured development environment can be initialized with a single command, incorporating linting, hot reloading, and best-practice defaults. + +## Philosophy + +### **Minimal Abstractions** + +Rather than introducing new layers of abstraction, the framework enhances Express through structured utility functions. Full control over request handling is maintained while offering built-in tools for middleware management, configuration, and error handling. + +### **Comprehensive Feature Set** + +A variety of essential features required for modern web applications, including authentication, logging, and observability, have been integrated. This approach allows teams to focus on application logic rather than spending time configuring third-party packages. + +### **Adherence to Best Practices** + +The framework is structured to promote modular and maintainable code. The use of Prisma for database management, OpenAPI documentation, and structured logging ensures consistency and maintainability across projects. + +### **Optimized Developer Experience** + +A complete development environment can be set up using a single command through Docker Compose. Automated linting, nodemon-based reloads, and process clustering have been included to improve efficiency and reduce development overhead. + +By emphasizing simplicity, flexibility, and adherence to best practices, this framework enables the development of scalable and maintainable Express applications while minimizing complexity. + +## Example + +routes.index.js +![Router Explained](/api/router-explained.png) + + +routes/resources.js +![Middleware Explained](/api/middleware-explained.png) + +## Typical Request Flow Through the Express Server + +| Step | Component | Description | +|------|----------|-------------| +| 1 | Express Server | Receives an HTTP request | +| 2 | Middleware | Parses request body, query, and cookies | +| 3 | Middleware | Enforces CORS policies | +| 4 | Router | Routes request to appropriate sub-router | +| 5 | Authentication | Validates JWT, attaches user to request | +| 6 | Access Control | Checks permissions | +| 7 | Validation | Ensures request format is correct | +| 8 | Async Error Handler | Wraps route handlers for error management | +| 9 | Business Logic | Executes request-specific logic | +| 10 | Middleware | Applies gzip compression | +| 11 | Express Server | Sends response to client | + +Error Handling Steps: +| Step | Component | Description | +|------|----------|-------------| +| 10 | 404 | Handle routing failures | +| 11 | Custom Error Handlers | Handle specific errors | +| 12 | Global Error Handler | Handle all other errors | +| 13 | Middleware | Applies gzip compression | +| 14 | Express Server | Sends response to client | + +Detailed Steps: + +1. Express creates a [`request`](https://expressjs.com/en/4x/api.html#req) object that represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on. +2. `src/app.js` - The body, query parameters, and cookies are parsed and converted to objects, and the `req` object is updated. +3. `src/app.js` - Apply CORS policies to handle cross-origin requests. +4. Main Router (`src/routers/index.js`) - Initial [routing](https://expressjs.com/en/guide/routing.html) is performed to select a sub-router to send the request to. +5. [Authentication](03-security/authentication) - Validate JWT and attach the user profile to `req.user` or send a 401 error response*. +6. [Access Control](03-security/authorization) - Determine whether the requester has sufficient permissions to perform the desired operation on a particular resource. Attach the permission object to `req.permission` or send a 403 error response. +7. [Request Validation](01-core/validation) - Validate if the request query, parameters, or body is in the expected format, or send a 400 error response. +8. [Async Handler](01-core/error-handling.html#asynchronous-error-handler) - Wrap the business logic route middleware to catch asynchronous errors and propagate them to the global error handler. +9. **Route Handler - Business Logic** - Execute the business logic and create the response. +10. [Compression](https://expressjs.com/en/resources/middleware/compression.html) - Apply gzip compression to the response body. +11. Express server sets default headers and sends the [response](https://expressjs.com/en/4x/api.html#res) to the client. + +### When Something Goes Wrong + +10. [404 Handler](01-core/error-handling.html#_404-handler) - Handle routing failures and send a 404 error response. +11. [Custom Error Handlers](01-core/error-handling.html#custom-error-handlers) - Handle prisma, assertions, axios errors and send appropriate error responses. +12. [Global Error Handler](01-core/error-handling.html#the-default-error-handler) - Handle all other errors and send a 500 error response. +13. [Compression](https://expressjs.com/en/resources/middleware/compression.html) - Apply gzip compression to the response body. +14. Express server sets default headers and sends the [response](https://expressjs.com/en/4x/api.html#res) to the client. + +\* For routes registered before the `authenticate` middleware, such as `/health` and `/auth`, this middleware is not invoked. + +## Project Structure + +- `src/index.js` - Entry point of the application; imports and starts the application. +- `src/cluster.js` - Implements clustering for load balancing across multiple CPU cores. +- `src/app.js` - Configures and initializes the Express application. +- `src/routes/index.js` - Main router that consolidates all route modules. +- `src/routes/*.js` - Individual route modules implementing specific API endpoints. +- `src/middleware/*.js` - Express middleware functions. +- `src/services/*.js` - Houses core business logic separate from the routing layer. +- `src/core/*.js` - Essential to application logic but not business-specific. +- `src/cron/*.js` - Scheduled tasks run at specific intervals. +- `src/scripts/*.js` - Standalone scripts that need to be executed manually. +- `config/*.json` - Hierarchical configuration files. +- `prisma/schema.prisma` - Defines the database schema using Prisma ORM. +- `prisma/seed.js` - Script for seeding initial data into the database. +- `utils/index.js` - Reusable functions that are not tied to business logic. +- `keys/genKeys.sh` - Script for generating JWT keys. +- `.env` - Environment-specific configuration file used for managing secrets and runtime settings. + + + + + + + + + + + + + + + diff --git a/docs/architecture.md b/docs/architecture.md index d4d8a243e..a770c8de9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,5 +1,9 @@ +--- +title: Architecture +order: 0 +--- -## Architecture +# Architecture
diff --git a/docs/index.md b/docs/index.md index bc13e16e8..267ec087e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,13 +18,13 @@ hero: features: - title: Installation details: Get Started with Bioloop - link: /install-docker + link: /installation/install-docker - title: UI details: Explore the features of our UI. - link: /ui/ + link: /ui/overview - title: API details: The backend. - link: /api/ + link: /api/introduction --- diff --git a/docs/installation/index.md b/docs/installation/index.md new file mode 100644 index 000000000..ae0b246f6 --- /dev/null +++ b/docs/installation/index.md @@ -0,0 +1,4 @@ +--- +title: Installation +order: 1 +--- \ No newline at end of file diff --git a/docs/install-docker.md b/docs/installation/install-docker.md similarity index 99% rename from docs/install-docker.md rename to docs/installation/install-docker.md index 77b7998c3..ad8932fb4 100755 --- a/docs/install-docker.md +++ b/docs/installation/install-docker.md @@ -1,3 +1,8 @@ +--- +title: Docker +order: 1 +--- + # Installation Guide This guide will help you set up Bioloop using Docker for local development or production deployment. diff --git a/docs/install-local.md b/docs/installation/install-local.md similarity index 99% rename from docs/install-local.md rename to docs/installation/install-local.md index 1688693ca..fc656e448 100644 --- a/docs/install-local.md +++ b/docs/installation/install-local.md @@ -1,3 +1,8 @@ +--- +title: Local +order: 2 +--- + # Local Installation Guide This guide provides step-by-step instructions for setting up and running the Bioloop application on your local machine. diff --git a/docs/metrics.md b/docs/metrics.md new file mode 100644 index 000000000..bad328de9 --- /dev/null +++ b/docs/metrics.md @@ -0,0 +1,88 @@ +# Metrics and Monitoring + +This project uses **Prometheus** and **Grafana** for monitoring and visualizing application performance metrics. These tools are essential for understanding system behavior, identifying bottlenecks, and ensuring the application runs smoothly in production. + +## Prometheus + +Prometheus is a powerful monitoring system that collects and stores metrics from various sources. It is configured to scrape metrics from the following targets: + +- **Database (Postgres)**: Metrics are exposed via `postgres_exporter`, which is defined in `docker-compose.yml`. +- **Node.js and Express.js app**: Metrics are exposed via the `prom-client` library. + +### Configuration +- **Service Definition**: The Prometheus service is defined in `docker-compose.yml`. +- **Configuration File**: Located at `metrics/prometheus/config/prometheus.yml`. + +### Without Prometheus +Without Prometheus, there would be no centralized system to collect and store metrics, making it difficult to monitor application performance or detect issues in real-time. + +### Integration +Prometheus runs in the same Docker network as the Node.js app and Postgres database, allowing it to scrape metrics directly from their endpoints. + + + +## Grafana + +Grafana is used to visualize the metrics collected by Prometheus. It provides dashboards that make it easy to analyze system performance and identify trends. + +### Features +- **Pre-configured Dashboards**: + - Node.js app metrics + - Postgres metrics +- **Datasource**: Automatically configured to use Prometheus as the datasource. +- **Access**: Accessible at `https://localhost/grafana/`. Only users with the admin role can access it. + +### Configuration +- **Service Definition**: The Grafana service is defined in `docker-compose.yml`. +- **Configuration Files**: + - `metrics/grafana/config/grafana.ini`: Contains Grafana server and authentication settings. + - `metrics/grafana/provisioning/datasources`: Configures Prometheus as the datasource. + - `metrics/grafana/provisioning/dashboards`: Defines the dashboards to be imported. + +### Authentication and Authorization +- **JWT Authentication**: Grafana uses JWT tokens for authentication. + - Admin users receive a secure, HTTPS-only cookie containing the JWT token. + - The reverse proxy forwards this token to Grafana as a header (`X-JWT-Assertion`). +- **Reverse Proxy**: + - In development: Vite is used as the reverse proxy (`ui/vite.config.js`). + - In production: Nginx is used as the reverse proxy (`nginx/conf/app.conf`). + +### Integration +Grafana is integrated into the same Docker network as Prometheus, ensuring seamless access to metrics. + + + +## How This Setup Helps + +- **Centralized Monitoring**: Prometheus collects metrics from multiple sources, while Grafana visualizes them in a single interface. +- **Maintainability**: The configuration files are modular and well-organized, making it easy to update or extend the setup. +- **Real-time Insights**: Developers can monitor application performance in real-time, enabling faster debugging and optimization. + + + +## Usage Instructions + +1. **Start the Services**: + Ensure Docker is running and start the services using: + ```bash + docker-compose up -d + ``` + +2. **View Dashboards**: + - Log into [bioloop](https://localhost) with the admin credentials. + - In the sidebar, click on `Metrics` to access the Grafana dashboards screen. + - Navigate to the pre-configured dashboards for Node.js and Postgres metrics. + +3. **Add New Metrics**: + - For Node.js: See [Instrumentation](api/05-performance/instrumentation) to add custom metrics. + - For Postgres: Update the `queries.yml` file in `metrics/postgres_exporter/`. + +4. **Restart Services**: + After making changes to configurations, restart the affected services: + ```bash + docker-compose restart + ``` + +This setup ensures a robust monitoring system that is easy to maintain and extend as the application grows. + + diff --git a/docs/public/api/middleware-explained.png b/docs/public/api/middleware-explained.png new file mode 100644 index 000000000..cf24e7aa7 Binary files /dev/null and b/docs/public/api/middleware-explained.png differ diff --git a/docs/public/api/router-explained.png b/docs/public/api/router-explained.png new file mode 100644 index 000000000..d1800bac6 Binary files /dev/null and b/docs/public/api/router-explained.png differ diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 000000000..162675351 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md index 50be02dd7..410ec8f9a 100644 --- a/docs/pull_request_template.md +++ b/docs/pull_request_template.md @@ -1,3 +1,7 @@ +--- +title: Contributing +--- + **Description** Please provide a brief description of the changes made in this PR. diff --git a/docs/secure_download.md b/docs/secure_download.md index 4f902d8f2..3abf03a89 100644 --- a/docs/secure_download.md +++ b/docs/secure_download.md @@ -1,4 +1,4 @@ -# Secure Download Documentation +# Secure Download ## Table of Contents - Introduction diff --git a/docs/template.md b/docs/template.md index f144fd7fc..e9c911c5d 100644 --- a/docs/template.md +++ b/docs/template.md @@ -1,3 +1,7 @@ +--- +title: Project Template +--- + # Create a repository Fork this repo IUSCA/ (only the org owners can do this, ask Charles.) diff --git a/docs/ui/auth_explained.md b/docs/ui/auth_explained.md index dde62429e..b8be2422f 100644 --- a/docs/ui/auth_explained.md +++ b/docs/ui/auth_explained.md @@ -1,3 +1,7 @@ +--- +title: Auth Explained +--- + # Auth Explained ## Objectives for Auth module diff --git a/docs/ui/index.md b/docs/ui/index.md index b2d4a9504..a00b33330 100644 --- a/docs/ui/index.md +++ b/docs/ui/index.md @@ -1,352 +1,4 @@ -# UI - -## Getting Started -Create a `.env` file from the template `.env.example`: -```bash -cp .env.example .env -``` -and populate the config values to all the keys. - -In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on `VITE_API_REDIRECT_URL` (ex: http://localhost:3000) as GET http://localhost:3000/users. - -### Running using docker -1. Set `VITE_API_REDIRECT_URL` to http://api:3000 -2. From the project root run: `docker composer up ui -d` and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI) - -### Running on host machine -1. set `VITE_API_REDIRECT_URL` to the API base url (ex: http://localhost:3000) -2. Install modules: `pnpm install` -3. Start dev server: `pnpm dev` - -## Features -- Vue3 -- Vite -- [Vuestic](https://vuestic.dev/) -- [vueuse](https://github.com/antfu/vueuse) -- [vue-router](https://github.com/vuejs/router) -- [file based routing](https://github.com/hannoeru/vite-plugin-pages) -- [auto import](https://github.com/antfu/unplugin-auto-import) -- [auto component import](https://github.com/antfu/unplugin-vue-components) -- eslint - - [jsconfig.json / tsconfig.json](https://code.visualstudio.com/docs/languages/jsconfig) -- [tailwind](https://tailwindcss.com/docs/guides/vite) -- [Layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) -- Icons -- [Vuetify and tailwind](https://michaelzanggl.com/articles/add-tailwind-css-to-vuetify/) -- [pinia](https://pinia.vuejs.org/) -- [https dev](https://vitejs.dev/config/server-options.html#server-https) -- Docker -- Dark mode (TODO) -- [Rollup Dependencies Visualizer](https://www.npmjs.com/package/rollup-plugin-visualizer) - Visualize and analyze your Rollup bundle to see which modules are taking up space. Run `npm run build` and open `stats.html` -- Feature flags - -## Icons - -There are multiple ways to include icons: -- [Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons) are provided by Vuestic. Iconify icon components are auto imported. - - usage: `` -- [Iconify](https://icon-sets.iconify.design/?query=) has a lot of third party / community icons - - usage: `` - - usage: `` - -Iconify icons are installed using -- Installation: option-1: https://docs.iconify.design/icon-components/vue/ -- Installation: option-2: https://github.com/antfu/unplugin-icons and [autoimporting](https://github.com/antfu/unplugin-icons#auto-importing) - - need to install [specific packs](https://github.com/antfu/unplugin-icons#icons-data) ex: `pnpm i -D @iconify-json/mdi` - - mdi is installed in this repo and is a recommended icon library to use - -## Colors - -Vuestic colors: https://ui.vuestic.dev/en/styles/colors - -```css -:host { - --va-text-selected: #b3d4fc; - --va-text-highlighted: #ffc5274e; - --va-link-color: var(--va-primary); - --va-link-color-secondary: var(--va-secondary); - --va-link-color-hover: var(--va-primary-lighten, --va-primary); - --va-link-color-active: var(--va-primary); - --va-link-color-visited: var(--va-primary-darken, --va-primary); - --va-muted: #7f828b; - --va-primary: #154ec1; - --va-secondary: #767c88; - --va-success: #3d9209; - --va-info: #158de3; - --va-danger: #e42222; - --va-warning: #ffd43a; - --va-background-primary: #f6f6f6; - --va-background-secondary: #ffffff; - --va-background-element: #ebf1f4; - --va-background-border: #dee5f2; - --va-text-primary: #262824; - --va-text-inverted: #ffffff; - --va-shadow: rgba(0, 0, 0, .12); - --va-focus: #49a8ff; -} -``` - -Using - -```html - - - - - -``` -## Notable Vuestic Classes - -CSS: `bioloop/ui/node_modules/vuestic-ui/dist/styles/css-variables.css` - -## Configuration - -Configuration values are used to fine-tune the application's behavior. These values are likely to change between different environments or instances of the application. - -We have a layered and hierarchical config system: -- Config for multiple environment is managed through Env variables specified in `.env` files. -- Based on the [mode](https://vitejs.dev/guide/env-and-mode.html#modes), these environment variables are automatically imported into the code by vite. -- The values from the environment variables is merged with other static config centrally in [config.js](src/config.js). All environment variables are backed by sensible default values. - -To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable. - -```javascript -{ - ..., - foobar: import.meta.env.FOOBAR || 120, -} -``` - -Add the name and value of the environment variable to the `.env` file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called `.env.example` is maintained. This file contains all the variables defined in `.env` without the values. - - -## Constants - -Constants are values that remain unchanged across different environments. These can be values like the types of datasets recognized by the system, various dataset states, upload states, texts for alert messages, etc. These are stored in `constants.js`. - - --- - -> ### Configuration vs Constants -> -> 1. Mutability - Configuration values may change between environments or during runtime, while constants remain fixed. -> 2. Source: Configuration often comes from environment variables, while constants are hardcoded in the application. -> 3. Purpose: Configuration is used to adjust application behavior, while constants define fixed aspects of the application. - ---- - - -## Authentication - -Users are authenticated using IU CAS. [More on auth module](auth_explained.md). - -Authentication with google OpenID Connect is implemented following this guide https://developers.google.com/identity/openid-connect/openid-connect - -Authentication with CILogon OpenID Connect is implemented following this guide https://www.cilogon.org/oidc - -Enable / disable login with authentication providers: - -`ui/src/config.js` -- "auth_enabled.google": true | false -- "auth_enabled.cilogon": true | false - -`api/src/config/default.json` -- "auth.google.enabled": true | false -- "auth.cilogon.enabled": true | false - -Environment Variables: - -`ui/.env` -- VITE_GOOGLE_RETURN=https://localhost/auth/google -- VITE_CILOGON_RETURN=https://localhost/auth/cil - -`api/.env` -- GOOGLE_OAUTH_CLIENT_ID= -- GOOGLE_OAUTH_CLIENT_SECRET= -- CILOGON_OAUTH_CLIENT_ID= -- CILOGON_OAUTH_CLIENT_SECRET= - -### Authentication controls on router -By default any page will require user authentication - -```html - -meta: - title: Dashboard - -``` - -Page requires user authentication + role constrained. -```html - -meta: - title: Dashboard - requiresRoles: ['operator', 'admin'] - -``` -Only users with either operator or admin role can access this page - -No authentication, anonymous view -```html - -meta: - title: Dashboard - requiresAuth: false - -``` - -## Utility Components - -Vue Components developed in house to be reused in the app. [Documentation](util_components.md) - -## Coding Conventions -- Use custom component names as `` - -## Adding Additional Fonts -- Search for fonts on https://fontsource.org/ -- Install - `npm install @fontsource/audiowide` -- Add `import '@fontsource/audiowide';` in [main.js](src/main.js) -- Add 'Audiowide' to `font-family: ` in body styles in [base.css](src/styles/base.css) - -## Dates and Times -- All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone -- [datetime](src/services/datetime.js) module is used to consolidate the various date and time formats to use in the UI. -- Use browser's local time zone to show date and time whenever possible. - -Usage: - -```javascript -import * as datetime from '@/services/datetime.js' - -datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023" -datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00" - -datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago" -datetime.readableDuration(130*1000) // "2 minutes" -datetime.formatDuration(12000 * 1000) // "3h 20m" -``` - -If you have a usecase to display in formats other than above in more than one component, add a function to [datetime](src/services/datetime.js) service and use it. - -## Feature Flags - -Features can be enabled or disabled at the UI level. Components can determine whether a feature is enabled by reading it from `./config.js`, which in turn reads this config from `./.env`. - -``` -// ./config.js - - ... - enabledFeatures: { - genomeBrowser: import.meta.env.VITE_ENABLED_GENOME_BROWSER === "true", - }, - ... - -``` -``` -# ./.env - -VITE_ENABLED_GENOME_BROWSER=true -``` - -Reading the feature flag from `.env` allows for features to be toggled without changing the code. - -Once a feature's status has been changed in `.env`, the app will need to be redeployed for those changes to come into effect. - - -## Navigational Breadcrumbs - -To set static nav links for a page `/page1/page2`, add nav attr to route meta config block - -```html - -meta: - title: Users - requiresRoles: ["operator", "admin"] - nav: [{ label: "Users" }] - -``` - -Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled. - -```html - -``` - - -To set dynamic nav links for a page `/page-dyn-1/page-dyn-2` - -```html - -``` - -## HTTP API Error Handling and Notifications -API requests are to be made with axios. - -Catch the error - -```javascript -import toast from "@/services/toast"; - -getRecords() - .then((res) => {...}) - .catch((err) => { - if (err?.response?.status == 404) - toast.info("No datasets"); - else toast.error("Could not fetch datatset"); - }) -``` - -or let someone else handle the dirty work - -```javascript -getRecords() - .then((res) => {...}) -``` - -Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc. +title: UI +order: 2 +--- \ No newline at end of file diff --git a/docs/ui/overview.md b/docs/ui/overview.md new file mode 100644 index 000000000..fcf764417 --- /dev/null +++ b/docs/ui/overview.md @@ -0,0 +1,357 @@ +--- +title: Overview +order: 0 +--- + +# UI Overview + +## Getting Started +Create a `.env` file from the template `.env.example`: +```bash +cp .env.example .env +``` +and populate the config values to all the keys. + +In the developement environment, the API calls from the UI are proxied by the vite server. For example, the UI running on https://localhost:443 make an API call GET https://localhost:443/api/users which the vite server intercepts and proxies it to the API server running on `VITE_API_REDIRECT_URL` (ex: http://localhost:3000) as GET http://localhost:3000/users. + +### Running using docker +1. Set `VITE_API_REDIRECT_URL` to http://api:3000 +2. From the project root run: `docker composer up ui -d` and open https://localhost:443 in the browser. (start the API and its dependencies before starting UI) + +### Running on host machine +1. set `VITE_API_REDIRECT_URL` to the API base url (ex: http://localhost:3000) +2. Install modules: `pnpm install` +3. Start dev server: `pnpm dev` + +## Features +- Vue3 +- Vite +- [Vuestic](https://vuestic.dev/) +- [vueuse](https://github.com/antfu/vueuse) +- [vue-router](https://github.com/vuejs/router) +- [file based routing](https://github.com/hannoeru/vite-plugin-pages) +- [auto import](https://github.com/antfu/unplugin-auto-import) +- [auto component import](https://github.com/antfu/unplugin-vue-components) +- eslint + - [jsconfig.json / tsconfig.json](https://code.visualstudio.com/docs/languages/jsconfig) +- [tailwind](https://tailwindcss.com/docs/guides/vite) +- [Layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) +- Icons +- [Vuetify and tailwind](https://michaelzanggl.com/articles/add-tailwind-css-to-vuetify/) +- [pinia](https://pinia.vuejs.org/) +- [https dev](https://vitejs.dev/config/server-options.html#server-https) +- Docker +- Dark mode (TODO) +- [Rollup Dependencies Visualizer](https://www.npmjs.com/package/rollup-plugin-visualizer) - Visualize and analyze your Rollup bundle to see which modules are taking up space. Run `npm run build` and open `stats.html` +- Feature flags + +## Icons + +There are multiple ways to include icons: +- [Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons) are provided by Vuestic. Iconify icon components are auto imported. + - usage: `` +- [Iconify](https://icon-sets.iconify.design/?query=) has a lot of third party / community icons + - usage: `` + - usage: `` + +Iconify icons are installed using +- Installation: option-1: https://docs.iconify.design/icon-components/vue/ +- Installation: option-2: https://github.com/antfu/unplugin-icons and [autoimporting](https://github.com/antfu/unplugin-icons#auto-importing) + - need to install [specific packs](https://github.com/antfu/unplugin-icons#icons-data) ex: `pnpm i -D @iconify-json/mdi` + - mdi is installed in this repo and is a recommended icon library to use + +## Colors + +Vuestic colors: https://ui.vuestic.dev/en/styles/colors + +```css +:host { + --va-text-selected: #b3d4fc; + --va-text-highlighted: #ffc5274e; + --va-link-color: var(--va-primary); + --va-link-color-secondary: var(--va-secondary); + --va-link-color-hover: var(--va-primary-lighten, --va-primary); + --va-link-color-active: var(--va-primary); + --va-link-color-visited: var(--va-primary-darken, --va-primary); + --va-muted: #7f828b; + --va-primary: #154ec1; + --va-secondary: #767c88; + --va-success: #3d9209; + --va-info: #158de3; + --va-danger: #e42222; + --va-warning: #ffd43a; + --va-background-primary: #f6f6f6; + --va-background-secondary: #ffffff; + --va-background-element: #ebf1f4; + --va-background-border: #dee5f2; + --va-text-primary: #262824; + --va-text-inverted: #ffffff; + --va-shadow: rgba(0, 0, 0, .12); + --va-focus: #49a8ff; +} +``` + +Using + +```html + + + + + +``` +## Notable Vuestic Classes + +CSS: `bioloop/ui/node_modules/vuestic-ui/dist/styles/css-variables.css` + +## Configuration + +Configuration values are used to fine-tune the application's behavior. These values are likely to change between different environments or instances of the application. + +We have a layered and hierarchical config system: +- Config for multiple environment is managed through Env variables specified in `.env` files. +- Based on the [mode](https://vitejs.dev/guide/env-and-mode.html#modes), these environment variables are automatically imported into the code by vite. +- The values from the environment variables is merged with other static config centrally in [config.js](src/config.js). All environment variables are backed by sensible default values. + +To introduce new configuration to this project, determine if it is environment-specific or static. If it is static, add both the key and value directly to config.js. Otherwise, add the key to config.js and read the value from an environment variable. + +```javascript +{ + ..., + foobar: import.meta.env.FOOBAR || 120, +} +``` + +Add the name and value of the environment variable to the `.env` file. This file is not tracked by the version control system. To keep track of the environment variables required to initialize the project in a new machine, another file called `.env.example` is maintained. This file contains all the variables defined in `.env` without the values. + + +## Constants + +Constants are values that remain unchanged across different environments. These can be values like the types of datasets recognized by the system, various dataset states, upload states, texts for alert messages, etc. These are stored in `constants.js`. + + +--- + +> ### Configuration vs Constants +> +> 1. Mutability - Configuration values may change between environments or during runtime, while constants remain fixed. +> 2. Source: Configuration often comes from environment variables, while constants are hardcoded in the application. +> 3. Purpose: Configuration is used to adjust application behavior, while constants define fixed aspects of the application. + +--- + + +## Authentication + +Users are authenticated using IU CAS. [More on auth module](auth_explained.md). + +Authentication with google OpenID Connect is implemented following this guide https://developers.google.com/identity/openid-connect/openid-connect + +Authentication with CILogon OpenID Connect is implemented following this guide https://www.cilogon.org/oidc + +Enable / disable login with authentication providers: + +`ui/src/config.js` +- "auth_enabled.google": true | false +- "auth_enabled.cilogon": true | false + +`api/src/config/default.json` +- "auth.google.enabled": true | false +- "auth.cilogon.enabled": true | false + +Environment Variables: + +`ui/.env` +- VITE_GOOGLE_RETURN=https://localhost/auth/google +- VITE_CILOGON_RETURN=https://localhost/auth/cil + +`api/.env` +- GOOGLE_OAUTH_CLIENT_ID= +- GOOGLE_OAUTH_CLIENT_SECRET= +- CILOGON_OAUTH_CLIENT_ID= +- CILOGON_OAUTH_CLIENT_SECRET= + +### Authentication controls on router +By default any page will require user authentication + +```html + +meta: + title: Dashboard + +``` + +Page requires user authentication + role constrained. +```html + +meta: + title: Dashboard + requiresRoles: ['operator', 'admin'] + +``` +Only users with either operator or admin role can access this page + +No authentication, anonymous view +```html + +meta: + title: Dashboard + requiresAuth: false + +``` + +## Utility Components + +Vue Components developed in house to be reused in the app. [Documentation](util_components.md) + +## Coding Conventions +- Use custom component names as `` + +## Adding Additional Fonts +- Search for fonts on https://fontsource.org/ +- Install - `npm install @fontsource/audiowide` +- Add `import '@fontsource/audiowide';` in [main.js](src/main.js) +- Add 'Audiowide' to `font-family: ` in body styles in [base.css](src/styles/base.css) + +## Dates and Times +- All dates, timestamps are returned from API as ISO 8601 strings in UTC time zone +- [datetime](src/services/datetime.js) module is used to consolidate the various date and time formats to use in the UI. +- Use browser's local time zone to show date and time whenever possible. + +Usage: + +```javascript +import * as datetime from '@/services/datetime.js' + +datetime.date("2023-06-14T01:18:40.501Z") // "Jun 14 2023" +datetime.absolute("2023-06-14T01:18:40.501Z") // "2023-06-13 21:18:40 -04:00" + +datetime.fromNow("2023-06-14T01:18:40.501Z") // "2 months ago" +datetime.readableDuration(130*1000) // "2 minutes" +datetime.formatDuration(12000 * 1000) // "3h 20m" +``` + +If you have a usecase to display in formats other than above in more than one component, add a function to [datetime](src/services/datetime.js) service and use it. + +## Feature Flags + +Features can be enabled or disabled at the UI level. Components can determine whether a feature is enabled by reading it from `./config.js`, which in turn reads this config from `./.env`. + +``` +// ./config.js + + ... + enabledFeatures: { + genomeBrowser: import.meta.env.VITE_ENABLED_GENOME_BROWSER === "true", + }, + ... + +``` +``` +# ./.env + +VITE_ENABLED_GENOME_BROWSER=true +``` + +Reading the feature flag from `.env` allows for features to be toggled without changing the code. + +Once a feature's status has been changed in `.env`, the app will need to be redeployed for those changes to come into effect. + + +## Navigational Breadcrumbs + +To set static nav links for a page `/page1/page2`, add nav attr to route meta config block + +```html + +meta: + title: Users + requiresRoles: ["operator", "admin"] + nav: [{ label: "Users" }] + +``` + +Nav breadcrumb are not reset after leaving a page. So if a page should not show nav breadcrumbs they have to be explicitly disabled. + +```html + +``` + + +To set dynamic nav links for a page `/page-dyn-1/page-dyn-2` + +```html + +``` + +## HTTP API Error Handling and Notifications +API requests are to be made with axios. + +Catch the error + +```javascript +import toast from "@/services/toast"; + +getRecords() + .then((res) => {...}) + .catch((err) => { + if (err?.response?.status == 404) + toast.info("No datasets"); + else toast.error("Could not fetch datatset"); + }) +``` + +or let someone else handle the dirty work + +```javascript +getRecords() + .then((res) => {...}) +``` + +Global axios error handler will display a generic error toast based on error class ex: 4xx, 5xx, network errors, etc. diff --git a/docs/ui/util_components.md b/docs/ui/util_components.md index 04266f012..8055eac3f 100644 --- a/docs/ui/util_components.md +++ b/docs/ui/util_components.md @@ -1,3 +1,8 @@ +--- +title: Utility Components +--- + + # Utility Components ## AutoComplete diff --git a/docs/welcome-message.md b/docs/welcome-message.md index 74597fb15..0629f7a37 100644 --- a/docs/welcome-message.md +++ b/docs/welcome-message.md @@ -1,3 +1,7 @@ +--- +title: Welcome Message +--- + **Subject:** Your Research Data is Now Available Dear \[Researcher’s Name\], diff --git a/docs/worker/index.md b/docs/worker/index.md index d98e7784b..f7252733b 100644 --- a/docs/worker/index.md +++ b/docs/worker/index.md @@ -1,184 +1,4 @@ -# Workers - -## Coding Guidelines - -### Hierarchical Config - -- The default & dev config goes into `workers/config/common.py` -- The overrides for production goes into `workers/config/production.py` -- Based on the environment variable APP_ENV, config from that file is imported and merged with the common config. - - Add APP_ENV=production to `.env` file which load_dotenv reads or - - directly set it as `export APP_ENV=production`. -- In project files, import config as `from workers.config import config` -- Imported config is a [DotMap](https://pypi.org/project/dotmap/) object, which supports both `config[]` and `config.` - access. -- To add a new environment (for example "stage"), create a new file inside `workers/config` called `stage.py` and have - the overriding config as a dict assigned to a variable named `config`. - -### Celery config - -- config specific to Celery is in `workers/config/celeryconfig.py` -- Config is in python values, instead of a dict -- Env specific values and secrets are loaded from `.env` file - -### Code Organization - -- Celery Tasks: `workers/tasks/*.py` -- Scheduled job and other scripts: `workers/scripts/*.py` -- Helper code: `workers/*.py` -- Config / settings are in `workers/config/*.py` and `.env` -- Test code is in `tests/` - -### Parallel tasks limit - -The maximum number of active (i.e. not 'PENDING') tasks that can run at a time is determined by the number of Celery workers, which is currently set to 8. - -This config can be found in `ecosystem.config.js`, under app `celery_worker`: - -``` --m celery -A workers.celery_app worker ... --autoscale=8,2 -``` - -### Hot Module Replacement - -Worker automatically run with updated code except for the code in - -- workers.config.* -- workers.utils -- workers.celery_app -- workers.task.declaration - -## Deployment - -- Add `module load python/3.10.5` to ~/.modules -- Update `.env` (make a copy of `.env.example` and add values) -- Install dependencies - -```bash -poetry export --without-hashes --format=requirements.txt > requirements.txt -pip install -r requirements.txt -``` - -```bash -cd ~/app/workers -pm2 start ecosystem.config.js -# optional -pm2 save -``` - -## Testing with workers running on local machine - -Start mongo and queue - -```bash -cd -docker-compose up queue mongo -d -``` - -Start Workers - -```bash -python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q' -``` - -`--concurrency 1`: number of worker processed to pre-fork - -`-O fair`: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks -when they are actually available. - -Use `--hostname '-celery-@%h'` to distinguish multiple workers running on the same machine either -for the same app or different apps. - -- replace `` with app name (ex: bioloop) -- replace `` with worker name (ex: w1) - -Auto-scaling - max_concurrency,min_concurrency -`--autoscale=10,3` (always keep 3 processes, but grow to 10 if necessary). - -`--queues '-dev.sca.iu.edu'` comma separated queue names. worker will subscribe to these queues for accepting tasks. -Configured in `workers/config/celeryconfig.py` with `task_routes`, `task_default_queue` - -Run test - -```bash -python -m tests.test -``` - -## Testing with workers running on COLO node and Rhythm API - -There are no test instances of API, rhythm_api, mongo, postgres, queue running. -These need to be run in local and port forwarded through ssh. - -- start postgres locally using docker - -```bash -cd -docker-compose up postgres -d -``` - -- start rhythm_api locally - -```bash -cd -docker-compose up queue mongo -d -poetry run dev -``` - -- start UI and API locally - -```bash -cd /api -pnpm start -``` - -```bash -cd /ui -pnpm dev -``` - -- Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server - running on the local machine. - - API - local port - 3030, remote port - 3130 - - Mongo - local port - 27017, remote port - 28017 - - queue - local port - 5672, remote port - 5772 - -```bash -ssh \ - -A \ - -R 3130:localhost:3030 \ - -R 28017:localhost:27017 \ - -R 5772:localhost:5672 \ - bioloopuser@workers.iu.edu -``` - -- pull latest changes in dev branch to `` - -```bash -colo23> cd -colo23> git checkout dev -colo23> git pull -``` - -- create / update `/workers/.env` -- create an auth token to communicate with the express server (postgres db) - - `cd /api` - - `node src/scripts/issue_token.js ` - - ex: `node src/scripts/issue_token.js svc_tasks` - - docker ex: `sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks` - -- install dependencies using poetry and start celery workers - -```bash -colo23> cd workers -colo23> poetry install -colo23> poetry shell -colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1 -``` - - -Dataset Name: -- taken from the name of the directory ingested -- used in watch.py to filter out registered datasets -- used to compute the staging path `staging_dir / alias / dataset['name']` -- used to compute the qc path `Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'` -- used to compute the scratch tar path while downloading the tar file from SDA `Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')` +--- +title: Workers +order: 4 +--- \ No newline at end of file diff --git a/docs/worker/overview.md b/docs/worker/overview.md new file mode 100644 index 000000000..e5b02a45d --- /dev/null +++ b/docs/worker/overview.md @@ -0,0 +1,189 @@ +--- +title: Overview +order: 0 +--- + +# Worker Overview + +## Coding Guidelines + +### Hierarchical Config + +- The default & dev config goes into `workers/config/common.py` +- The overrides for production goes into `workers/config/production.py` +- Based on the environment variable APP_ENV, config from that file is imported and merged with the common config. + - Add APP_ENV=production to `.env` file which load_dotenv reads or + - directly set it as `export APP_ENV=production`. +- In project files, import config as `from workers.config import config` +- Imported config is a [DotMap](https://pypi.org/project/dotmap/) object, which supports both `config[]` and `config.` + access. +- To add a new environment (for example "stage"), create a new file inside `workers/config` called `stage.py` and have + the overriding config as a dict assigned to a variable named `config`. + +### Celery config + +- config specific to Celery is in `workers/config/celeryconfig.py` +- Config is in python values, instead of a dict +- Env specific values and secrets are loaded from `.env` file + +### Code Organization + +- Celery Tasks: `workers/tasks/*.py` +- Scheduled job and other scripts: `workers/scripts/*.py` +- Helper code: `workers/*.py` +- Config / settings are in `workers/config/*.py` and `.env` +- Test code is in `tests/` + +### Parallel tasks limit + +The maximum number of active (i.e. not 'PENDING') tasks that can run at a time is determined by the number of Celery workers, which is currently set to 8. + +This config can be found in `ecosystem.config.js`, under app `celery_worker`: + +``` +-m celery -A workers.celery_app worker ... --autoscale=8,2 +``` + +### Hot Module Replacement + +Worker automatically run with updated code except for the code in + +- workers.config.* +- workers.utils +- workers.celery_app +- workers.task.declaration + +## Deployment + +- Add `module load python/3.10.5` to ~/.modules +- Update `.env` (make a copy of `.env.example` and add values) +- Install dependencies + +```bash +poetry export --without-hashes --format=requirements.txt > requirements.txt +pip install -r requirements.txt +``` + +```bash +cd ~/app/workers +pm2 start ecosystem.config.js +# optional +pm2 save +``` + +## Testing with workers running on local machine + +Start mongo and queue + +```bash +cd +docker-compose up queue mongo -d +``` + +Start Workers + +```bash +python -m celery -A tests.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-celery-w1@%h' --autoscale=2,1 --queues 'bioloop-dev.sca.iu.edu.q' +``` + +`--concurrency 1`: number of worker processed to pre-fork + +`-O fair`: Optimization profile, disables prefetching of tasks. Guarantees child processes will only be allocated tasks +when they are actually available. + +Use `--hostname '-celery-@%h'` to distinguish multiple workers running on the same machine either +for the same app or different apps. + +- replace `` with app name (ex: bioloop) +- replace `` with worker name (ex: w1) + +Auto-scaling - max_concurrency,min_concurrency +`--autoscale=10,3` (always keep 3 processes, but grow to 10 if necessary). + +`--queues '-dev.sca.iu.edu'` comma separated queue names. worker will subscribe to these queues for accepting tasks. +Configured in `workers/config/celeryconfig.py` with `task_routes`, `task_default_queue` + +Run test + +```bash +python -m tests.test +``` + +## Testing with workers running on COLO node and Rhythm API + +There are no test instances of API, rhythm_api, mongo, postgres, queue running. +These need to be run in local and port forwarded through ssh. + +- start postgres locally using docker + +```bash +cd +docker-compose up postgres -d +``` + +- start rhythm_api locally + +```bash +cd +docker-compose up queue mongo -d +poetry run dev +``` + +- start UI and API locally + +```bash +cd /api +pnpm start +``` + +```bash +cd /ui +pnpm dev +``` + +- Reverse port forward API, mongo and queue. let the clients on remote machine talk to a server + running on the local machine. + - API - local port - 3030, remote port - 3130 + - Mongo - local port - 27017, remote port - 28017 + - queue - local port - 5672, remote port - 5772 + +```bash +ssh \ + -A \ + -R 3130:localhost:3030 \ + -R 28017:localhost:27017 \ + -R 5772:localhost:5672 \ + bioloopuser@workers.iu.edu +``` + +- pull latest changes in dev branch to `` + +```bash +colo23> cd +colo23> git checkout dev +colo23> git pull +``` + +- create / update `/workers/.env` +- create an auth token to communicate with the express server (postgres db) + - `cd /api` + - `node src/scripts/issue_token.js ` + - ex: `node src/scripts/issue_token.js svc_tasks` + - docker ex: `sudo docker compose -f "docker-compose-prod.yml" exec api node src/scripts/issue_token.js svc_tasks` + +- install dependencies using poetry and start celery workers + +```bash +colo23> cd workers +colo23> poetry install +colo23> poetry shell +colo23> python -m celery -A workers.celery_app worker --loglevel INFO -O fair --pidfile celery_worker.pid --hostname 'bioloop-dev-celery-w1@%h' --autoscale=2,1 +``` + + +Dataset Name: +- taken from the name of the directory ingested +- used in watch.py to filter out registered datasets +- used to compute the staging path `staging_dir / alias / dataset['name']` +- used to compute the qc path `Path(config['paths'][dataset_type]['qc']) / dataset['name'] / 'qc'` +- used to compute the scratch tar path while downloading the tar file from SDA `Path(f'{str(compute_staging_path(dataset)[0].parent)}/{dataset["name"]}.tar')` diff --git a/metrics/grafana/config/provisioning/dashboards/NodejsAndExpressMetrics.json b/metrics/grafana/config/provisioning/dashboards/NodejsAndExpressMetrics.json new file mode 100644 index 000000000..e8f80c6f5 --- /dev/null +++ b/metrics/grafana/config/provisioning/dashboards/NodejsAndExpressMetrics.json @@ -0,0 +1,3260 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Metrics for node.js and express router status", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 14565, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 98, + "panels": [], + "title": "General", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 104, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^v21\\.4\\.0$/", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "nodejs_version_info{instance=~\"$instance\"}", + "format": "time_series", + "instant": true, + "legendFormat": "{{version}}", + "range": false, + "refId": "A" + } + ], + "title": "NodeJS Version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "The Apdex score is the number of satisfied requests plus half of the tolerating requests plus none of the frustrated requests, divided by all the requests\n\nThe Apdex formula is equivalent to a weighted average, where a satisfied user is given a score of 1, a tolerating user is given a score of 0.5, and a frustrated user is given a score of 0.\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#C4162A", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 70 + }, + { + "color": "#299c46", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 6, + "x": 3, + "y": 1 + }, + "id": 51, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "(\n ( \n sum(http_request_duration_seconds_bucket{instance=~\"$instance\",le=\"$target\",status_code=~\"2..\"})\n + (\n sum(http_request_duration_seconds_bucket{instance=~\"$instance\",le=\"$tolerated\",status_code=~\"2..\"})\n - sum(http_request_duration_seconds_bucket{instance=~\"$instance\",le=\"$target\",status_code=~\"2..\"})\n ) / 2\n ) / sum(http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"2..\"})\n) * 100", + "interval": "", + "legendFormat": "score", + "range": true, + "refId": "A" + } + ], + "title": "Apdex Score: target $target s, tolerated $tolerated s", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Average Requests per Second in the selected time range", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#299c46", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 100 + }, + { + "color": "#C4162A", + "value": 200 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 9, + "y": 1 + }, + "id": 57, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range]))", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "RPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "95% of requests are completed in less time than this.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#299c46", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 0.1 + }, + { + "color": "#C4162A", + "value": 0.3 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 11, + "y": 1 + }, + "id": 109, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le))", + "interval": "", + "legendFormat": "error %", + "range": true, + "refId": "A" + } + ], + "title": "P95 Latency", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Number of requests in the selected time range", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "rgb(31, 120, 193)", + "mode": "fixed" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 14, + "y": 1 + }, + "id": 53, + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "round(sum(increase(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range])))", + "instant": false, + "legendFormat": "requests", + "refId": "A" + } + ], + "title": "Total Requests", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#299c46", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 5 + }, + { + "color": "#C4162A", + "value": 10 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 17, + "y": 1 + }, + "id": 105, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "(\n\tsum(increase(http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"[4]..\"}[$__range])OR on() vector(0))\n\t/ \n\tsum(increase(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range]))\n)*100", + "interval": "", + "legendFormat": "error %", + "range": true, + "refId": "A" + } + ], + "title": "4xx Errors", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#299c46", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 5 + }, + { + "color": "#C4162A", + "value": 10 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 2, + "x": 19, + "y": 1 + }, + "id": 106, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "(\n\tsum(increase(http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"[5]..\"}[$__range])OR on() vector(0))\n\t/ \n\tsum(increase(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range]))\n)*100", + "interval": "", + "legendFormat": "error %", + "range": true, + "refId": "A" + } + ], + "title": "5xx Errors", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#299c46", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 1 + }, + { + "color": "#C4162A", + "value": 2 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 21, + "y": 1 + }, + "id": 61, + "maxDataPoints": 100, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(changes(process_start_time_seconds{instance=~\"$instance\"}[$restarts_interval]))", + "range": true, + "refId": "A" + } + ], + "title": "Restarts in $restarts_interval", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Number of worker processes in cluster mode", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 3 + }, + "id": 110, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "up{instance=~\"$instance\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Instances", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 99, + "panels": [], + "title": "HTTP Traffic", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Rate of requests grouped by status (2xx, 3xx, 4xx, 5xx) over a $interval window.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 6 + }, + "id": 101, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(http_request_duration_seconds_count{instance=~\"$instance\"}[$interval])) by (status_code)", + "instant": false, + "legendFormat": "{{status_code}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "distribution of HTTP response status codes (e.g., 2XX, 4XX, 5XX)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 6 + }, + "id": 100, + "options": { + "displayLabels": [ + "name" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "round(\n sum(increase(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range])) by (status_code)\n)", + "instant": false, + "legendFormat": "{{status_code}}", + "range": true, + "refId": "A" + } + ], + "title": "Status Codes Breakdown", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 6 + }, + "id": 73, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(\n(\n sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"2..\",le=\"$target\"}[$interval]))\n +\n (\n sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"2..\",le=\"$tolerated\"}[$interval]))\n -\n sum(rate(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"2..\",le=\"$target\"}[$interval]))\n )/2\n)\n/\nsum(rate(http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"2..\"}[$interval]))\n)*100", + "instant": false, + "interval": "", + "legendFormat": "score", + "range": true, + "refId": "A" + } + ], + "title": "Apdex Score: target $target s, tolerated $tolerated s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Number of requests in the selected time range", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "displayName": "", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "displayName", + "value": "Time" + }, + { + "id": "unit", + "value": "time: YYYY-MM-DD HH:mm:ss" + }, + { + "id": "custom.align" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 12 + }, + "id": 91, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "topk(5, \n round(\n increase(\n http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"[23]..\"}[$__range]\n )\n )\n)", + "instant": true, + "interval": "", + "legendFormat": "{{status_code}} {{path}}", + "refId": "A" + } + ], + "title": "Top 5 Success Count", + "transformations": [ + { + "id": "reduce", + "options": { + "includeTimeField": false, + "reducers": [ + "lastNotNull" + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Number of requests in the selected time range", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "displayName": "", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "displayName", + "value": "Time" + }, + { + "id": "unit", + "value": "time: YYYY-MM-DD HH:mm:ss" + }, + { + "id": "custom.align" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 12 + }, + "id": 92, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "topk(5, \n round(\n increase(\n http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"[45]..\"}[$__range]\n )\n )\n)", + "instant": true, + "interval": "", + "legendFormat": "{{status_code}} {{path}}", + "refId": "A" + } + ], + "title": "Top 5 Error Count", + "transformations": [ + { + "id": "reduce", + "options": { + "includeTimeField": false, + "reducers": [ + "lastNotNull" + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Measured using mean latency for successful requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "displayName": "", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "displayName", + "value": "Time" + }, + { + "id": "unit", + "value": "time: YYYY-MM-DD HH:mm:ss" + }, + { + "id": "custom.align" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 12 + }, + "id": 89, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "exemplar": true, + "expr": "topk(5, \nsum(increase(http_request_duration_seconds_sum{instance=~\"$instance\",status_code=~\"[23]xx\"}[$__range]))\n by (path)\n/ \nsum(increase(http_request_duration_seconds_count{instance=~\"$instance\",status_code=~\"[23]xx\"}[$__range]))\n by (path)\n)", + "instant": true, + "interval": "", + "legendFormat": "{{path}}", + "refId": "A" + } + ], + "title": "Top 5 Slowest Endpoints", + "transformations": [ + { + "id": "reduce", + "options": { + "includeTimeField": false, + "reducers": [ + "lastNotNull" + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Average duration of HTTP requests across all endpoints", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 103, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", + "hide": false, + "instant": false, + "legendFormat": "p95", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.9, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", + "hide": false, + "instant": false, + "legendFormat": "p90", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.5, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", + "hide": false, + "instant": false, + "legendFormat": "median", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(increase(http_request_duration_seconds_sum{instance=~\"$instance\"}[$__range])) \n/ \nsum(increase(http_request_duration_seconds_count{instance=~\"$instance\"}[$__range]))", + "instant": false, + "legendFormat": "mean", + "range": true, + "refId": "D" + } + ], + "title": "Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Latency for all methods and path for success responses", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 108, + "options": { + "displayMode": "gradient", + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.03\"}[$__range])\n)", + "instant": false, + "legendFormat": "0-30ms", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.1\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.03\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "30-100ms", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.3\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.1\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "100-300ms", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"1\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.3\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "300ms-1s", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"3\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"1\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "1s-3s", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"10\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"3\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "3s-10s", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"30\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"10\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "10s-30s", + "range": true, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"+Inf\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"30\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "30s+", + "range": true, + "refId": "H" + } + ], + "title": "Success Latency Buckets", + "type": "bargauge" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 87, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "refId": "A" + } + ], + "title": "Node.js Process", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "CPU time used by application for every 1 second that has passed during the last $interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 42, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "rate(process_cpu_seconds_total{instance=~\"$instance\"}[$interval])", + "legendFormat": "cpu", + "refId": "A" + } + ], + "title": "Rate of CPU Time Spent", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "RAM usage excluding SWAP", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 82, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "process_resident_memory_bytes{instance=~\"$instance\"}", + "legendFormat": "virtual memory", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "process_heap_bytes{instance=~\"$instance\"}", + "legendFormat": "heap memory", + "refId": "B" + } + ], + "title": "Process Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Time between scheduling a timer and its callback being evaluated.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 21, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_eventloop_lag_p50_seconds{instance=~\"$instance\"}", + "legendFormat": "p50", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "nodejs_eventloop_lag_p90_seconds{instance=~\"$instance\"}", + "hide": false, + "instant": false, + "legendFormat": "p90", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_eventloop_lag_p99_seconds{instance=~\"$instance\"}", + "legendFormat": "p99", + "refId": "C" + } + ], + "title": "Event Loop Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Number of active resources that are currently keeping the event loop alive. TTYWrap, PipeWrap, TCPServerWrap, TCPSocketWrap, Immediate", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 66, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_active_handles_total{instance=~\"$instance\"}", + "legendFormat": "Active Handler", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "nodejs_active_requests_total{instance=~\"$instance\"}", + "legendFormat": "Active Request", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "nodejs_active_resources_total{instance=~\"$instance\"}", + "hide": false, + "legendFormat": "Active Resources", + "range": true, + "refId": "C" + } + ], + "title": "Active Handlers/Requests/Resources", + "type": "timeseries" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 40, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 43, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "rate(nodejs_gc_duration_seconds_sum{instance=~\"$instance\"}[$interval])", + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "Rate of Garbage Collection Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 38, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "rate(nodejs_gc_duration_seconds_count{instance=~\"$instance\"}[$interval])", + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "Rate of Garbage Collection", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 38 + }, + "id": 46, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_gc_duration_seconds_sum{instance=~\"$instance\"}", + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "Garbage Collection Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 38 + }, + "id": 45, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_gc_duration_seconds_count{instance=~\"$instance\"}", + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "Garbage Collection Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 34, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_heap_size_total_bytes{instance=~\"$instance\"}", + "legendFormat": "total", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_heap_size_used_bytes{instance=~\"$instance\"}", + "legendFormat": "used", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "process_resident_memory_bytes{instance=~\"$instance\"}", + "legendFormat": "resident", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_external_memory_bytes{instance=~\"$instance\"}", + "legendFormat": "external", + "refId": "D" + } + ], + "title": "Heap Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 36, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "nodejs_heap_space_size_used_bytes{instance=~\"$instance\"}", + "legendFormat": "{{space}}", + "refId": "A" + } + ], + "title": "Heap Space Used", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "refId": "A" + } + ], + "title": "Node.js Garbage Collection", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 111, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 13, + "x": 0, + "y": 39 + }, + "id": 113, + "options": { + "displayMode": "gradient", + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "10.4.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.03\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "instant": false, + "legendFormat": "0-30ms", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.1\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.03\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "30-100ms", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.3\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.1\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "100-300ms", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"1\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"0.3\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "300ms-1s", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"3\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"1\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "1s-3s", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"10\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"3\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "3s-10s", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"30\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"10\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "10s-30s", + "range": true, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"+Inf\",path=~\"$path\",method=~\"$method\"}[$__range])\n)\n-\nsum (\n increase(http_request_duration_seconds_bucket{instance=~\"$instance\",status_code=~\"[23]xx\",le=\"30\",path=~\"$path\",method=~\"$method\"}[$__range])\n)", + "hide": false, + "instant": false, + "legendFormat": "30s+", + "range": true, + "refId": "H" + } + ], + "title": "$method $path Latency Buckets", + "type": "bargauge" + } + ], + "title": "Latency Buckets", + "type": "row" + } + ], + "refresh": "10s", + "schemaVersion": 39, + "tags": [ + "node.js", + "express", + "prometheus" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "host.docker.internal:9999" + ], + "value": [ + "host.docker.internal:9999" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(nodejs_version_info, instance)", + "hide": 0, + "includeAll": false, + "label": "instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(nodejs_version_info, instance)", + "refId": "Prometheus-instance-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "5m", + "value": "5m" + }, + "hide": 0, + "name": "interval", + "options": [ + { + "selected": true, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "current": { + "selected": false, + "text": "0.1", + "value": "0.1" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(http_request_duration_seconds_bucket, le)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "target", + "options": [], + "query": { + "query": "label_values(http_request_duration_seconds_bucket, le)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "0.3", + "value": "0.3" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(http_request_duration_seconds_bucket, le)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "tolerated", + "options": [], + "query": { + "query": "label_values(http_request_duration_seconds_bucket, le)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "30d", + "value": "30d" + }, + "hide": 0, + "name": "restarts_interval", + "options": [ + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": true, + "text": "30d", + "value": "30d" + } + ], + "query": "1d,7d,14d,30d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "allValue": "All", + "current": { + "selected": false, + "text": "/datasets/", + "value": "/datasets/" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(http_request_duration_seconds_bucket,path)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "name": "path", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(http_request_duration_seconds_bucket,path)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": "", + "current": { + "selected": true, + "text": [ + "GET" + ], + "value": [ + "GET" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "label_values(http_request_duration_seconds_bucket,method)", + "description": "", + "hide": 0, + "includeAll": true, + "label": "", + "multi": true, + "name": "method", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(http_request_duration_seconds_bucket,method)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Node.js and Express Metrics", + "uid": "3J_78b6Bz", + "version": 11, + "weekStart": "" +} \ No newline at end of file diff --git a/metrics/grafana/config/provisioning/dashboards/PostgreSQL Database.json b/metrics/grafana/config/provisioning/dashboards/PostgreSQLDatabase.json similarity index 100% rename from metrics/grafana/config/provisioning/dashboards/PostgreSQL Database.json rename to metrics/grafana/config/provisioning/dashboards/PostgreSQLDatabase.json diff --git a/metrics/prometheus/config/prometheus.yml b/metrics/prometheus/config/prometheus.yml index 47eb4e20f..75b3330e7 100644 --- a/metrics/prometheus/config/prometheus.yml +++ b/metrics/prometheus/config/prometheus.yml @@ -5,7 +5,7 @@ scrape_configs: - job_name: 'postgres' static_configs: - targets: ['postgres_exporter:9187'] - # - job_name: "api" - # metrics_path: "/prom-metrics" - # static_configs: - # - targets: ["host.docker.internal:3030"] \ No newline at end of file + - job_name: "api" + metrics_path: "/metrics" + static_configs: + - targets: ["host.docker.internal:9999", "api:9999"] \ No newline at end of file diff --git a/nginx/conf/app.conf b/nginx/conf/app.conf index d08b7ef63..2b40e7e56 100644 --- a/nginx/conf/app.conf +++ b/nginx/conf/app.conf @@ -64,6 +64,13 @@ server { try_files $uri $uri/ /index.html =404; } + # Block access to /api/prom-metrics + # This is a special route that should only be accessed by Prometheus server + # that is on the same network as the server + location /api/metrics { + deny all; # Deny all access to this route + } + location /api/ { proxy_pass http://host.docker.internal:3030/; proxy_http_version 1.1; diff --git a/package-lock.json b/package-lock.json index c36c6aa7e..deaf2f74e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,42 +1,44 @@ { - "name": "bioloop", + "name": "docs", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "docs", "devDependencies": { - "vitepress": "^1.0.0-rc.42" + "vitepress": "~1.6.3", + "vitepress-sidebar": "^1.31.1" } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", - "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "dev": true, "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" } }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", - "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "dev": true, "dependencies": { - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", - "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "dev": true, "dependencies": { - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -44,149 +46,221 @@ } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", - "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", "dev": true, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, - "node_modules/@algolia/cache-browser-local-storage": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz", - "integrity": "sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==", + "node_modules/@algolia/client-abtesting": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.23.0.tgz", + "integrity": "sha512-AyZ+9CUgWXwaaJ2lSwOJSy+/w0MFBPFqLrjWYs/HEpYMzBuFfGNZ7gEM9a7h4j7jY8hSBARBl8qdvInmj5vOEQ==", "dev": true, "dependencies": { - "@algolia/cache-common": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/cache-common": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.22.1.tgz", - "integrity": "sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA==", - "dev": true - }, - "node_modules/@algolia/cache-in-memory": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz", - "integrity": "sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==", + "node_modules/@algolia/client-analytics": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.23.0.tgz", + "integrity": "sha512-oeKCPwLBnTEPF/RWr0aaJnrfRDfFRLT5O7KV0OF1NmpEXvmzLmN7RwnwDKsNtPUHNfpJ6esP9xzkPEtJabrZ2w==", "dev": true, "dependencies": { - "@algolia/cache-common": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-account": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.22.1.tgz", - "integrity": "sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==", + "node_modules/@algolia/client-common": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.23.0.tgz", + "integrity": "sha512-9jacdC44vXLSaYKNLkFpbU1J4BbBPi/N7uoPhcGO//8ubRuVzigH6+RfK5FbudmQlqFt0J5DGUCVeTlHtgyUeg==", "dev": true, - "dependencies": { - "@algolia/client-common": "4.22.1", - "@algolia/client-search": "4.22.1", - "@algolia/transporter": "4.22.1" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-analytics": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.22.1.tgz", - "integrity": "sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==", + "node_modules/@algolia/client-insights": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.23.0.tgz", + "integrity": "sha512-/Gw5UitweRsnyb24Td4XhjXmsx8PxFzCI0oW6FZZvyr4kjzB9ECP2IjO+PdDq1A2fzDl/LXQ+u8ROudoVnXnQg==", "dev": true, "dependencies": { - "@algolia/client-common": "4.22.1", - "@algolia/client-search": "4.22.1", - "@algolia/requester-common": "4.22.1", - "@algolia/transporter": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-common": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.22.1.tgz", - "integrity": "sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==", + "node_modules/@algolia/client-personalization": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.23.0.tgz", + "integrity": "sha512-ivrEZBoXfDatpqpifgHauydxHEe4udNqJ0gy7adR2KODeQ+39MQeaT10I24mu+eylIuiQKJRqORgEdLZycq2qQ==", "dev": true, "dependencies": { - "@algolia/requester-common": "4.22.1", - "@algolia/transporter": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-personalization": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.22.1.tgz", - "integrity": "sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==", + "node_modules/@algolia/client-query-suggestions": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.0.tgz", + "integrity": "sha512-DjSgJWqTcsnlXEKqDsU7Y2vB/W/VYLlr6UfkzJkMuKB554Ia7IJr4keP2AlHVjjbBG62IDpdh5OkEs/+fbWsOA==", "dev": true, "dependencies": { - "@algolia/client-common": "4.22.1", - "@algolia/requester-common": "4.22.1", - "@algolia/transporter": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.22.1.tgz", - "integrity": "sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.23.0.tgz", + "integrity": "sha512-XAYWUYUhEG4OIdo/N7H/OFFRD9fokfv3bBTky+4Y4/q07bxhnrGSUvcrU6JQ2jJTQyg6kv0ke1EIfiTO/Xxb+g==", "dev": true, "dependencies": { - "@algolia/client-common": "4.22.1", - "@algolia/requester-common": "4.22.1", - "@algolia/transporter": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/logger-common": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.22.1.tgz", - "integrity": "sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg==", - "dev": true + "node_modules/@algolia/ingestion": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.23.0.tgz", + "integrity": "sha512-ULbykzzhhLVofCDU1m/CqSzTyKmjaxA/z1d6o6hgUuR6X7/dll9/G0lu0e4vmWIOItklWWrhU2V8sXD0YGBIHg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.23.0.tgz", + "integrity": "sha512-oB3wG7CgQJQr+uoijV7bWBphiSHkvGX43At8RGgkDyc7Aeabcp9ik5HgLC1YDgbHVOlQI+tce5HIbDCifzQCIg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } }, - "node_modules/@algolia/logger-console": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.22.1.tgz", - "integrity": "sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==", + "node_modules/@algolia/recommend": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.23.0.tgz", + "integrity": "sha512-4PWvCV6VGhnCMAbv2zfQUAlc3ofMs6ovqKlC/xcp7tWaucYd//piHg9CcCM4S0p9OZznEGQMRYPt2uqbk6V9vg==", "dev": true, "dependencies": { - "@algolia/logger-common": "4.22.1" + "@algolia/client-common": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz", - "integrity": "sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.0.tgz", + "integrity": "sha512-bacOsX41pnsupNB0k0Ny+1JDchQxIsZIcp69GKDBT0NgTHG8OayEO141eFalNmGil+GXPY0NUPRpx+5s4RdhGA==", "dev": true, "dependencies": { - "@algolia/requester-common": "4.22.1" + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/requester-common": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.22.1.tgz", - "integrity": "sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg==", - "dev": true + "node_modules/@algolia/requester-fetch": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.23.0.tgz", + "integrity": "sha512-tVNFREexJWDrvc23evmRgAcb2KLZuVilOIB/rVnQCl0GDbqIWJuQ1lG22HKqvCEQFthHkgVFGLYE74wQ96768g==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" + } }, "node_modules/@algolia/requester-node-http": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz", - "integrity": "sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.23.0.tgz", + "integrity": "sha512-XXHbq2heOZc9EFCc4z+uyHS9YRBygZbYQVsWjWZWx8hdAz+tkBX/jLHM9Xg+3zO0/v8JN6pcZzqYEVsdrLeNLg==", "dev": true, "dependencies": { - "@algolia/requester-common": "4.22.1" + "@algolia/client-common": "5.23.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/transporter": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.22.1.tgz", - "integrity": "sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==", + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, - "dependencies": { - "@algolia/cache-common": "4.22.1", - "@algolia/logger-common": "4.22.1", - "@algolia/requester-common": "4.22.1" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -194,32 +268,45 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@docsearch/css": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.5.2.tgz", - "integrity": "sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", "dev": true }, "node_modules/@docsearch/js": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.5.2.tgz", - "integrity": "sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", "dev": true, "dependencies": { - "@docsearch/react": "3.5.2", + "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "node_modules/@docsearch/react": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.5.2.tgz", - "integrity": "sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", "dev": true, "dependencies": { - "@algolia/autocomplete-core": "1.9.3", - "@algolia/autocomplete-preset-algolia": "1.9.3", - "@docsearch/css": "3.5.2", - "algoliasearch": "^4.19.1" + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", @@ -610,16 +697,58 @@ "node": ">=12" } }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.29", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.29.tgz", + "integrity": "sha512-KYrxmxtRz6iOAulRiUsIBMUuXek+H+Evwf8UvYPIkbQ+KDoOqTegHx3q/w3GDDVC0qJYB+D3hXPMZcpm78qIuA==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", - "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", + "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", "cpu": [ "arm" ], @@ -630,9 +759,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", - "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", + "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", "cpu": [ "arm64" ], @@ -643,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", + "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", "cpu": [ "arm64" ], @@ -656,9 +785,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", - "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", + "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", "cpu": [ "x64" ], @@ -668,10 +797,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", + "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", + "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", - "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", + "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", "cpu": [ "arm" ], @@ -682,9 +837,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", - "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", + "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", "cpu": [ "arm" ], @@ -695,9 +850,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", - "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", + "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", "cpu": [ "arm64" ], @@ -708,9 +863,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", - "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", + "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", "cpu": [ "arm64" ], @@ -720,10 +875,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", + "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", - "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", + "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", "cpu": [ "ppc64" ], @@ -734,9 +902,22 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", - "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", + "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", + "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", "cpu": [ "riscv64" ], @@ -747,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", - "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", + "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", "cpu": [ "s390x" ], @@ -760,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", + "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", "cpu": [ "x64" ], @@ -773,9 +954,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", - "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", + "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", "cpu": [ "x64" ], @@ -786,9 +967,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", - "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", + "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", "cpu": [ "arm64" ], @@ -799,9 +980,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", - "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", + "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", "cpu": [ "ia32" ], @@ -812,9 +993,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", - "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", + "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", "cpu": [ "x64" ], @@ -825,271 +1006,335 @@ ] }, "node_modules/@shikijs/core": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.0.0-rc.0.tgz", - "integrity": "sha512-j/7te+hvEYlQTvk/wPoA+1rOklZTz8QuyqVvV81KcEN/g1WXKVnqp9WZ7jFuv0ZVLqBtDx/V8viRDROJniyMLA==", - "dev": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } }, "node_modules/@shikijs/transformers": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.0.0-rc.0.tgz", - "integrity": "sha512-1W4QpLKDM+hnlO6vqGre7orZxW4CrnO4F1zftj1KE6MdaEvy1awZKYUXPswvDIARvuetbzTvgc/ZE2yYVT/6GA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "dev": true, "dependencies": { - "shiki": "1.0.0-rc.0" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, "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==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/linkify-it": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true }, "node_modules/@types/markdown-it": { - "version": "13.0.7", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz", - "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" + "@types/unist": "*" } }, "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true }, "node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz", - "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^5.0.0", + "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "node_modules/@vue/compiler-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", - "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/shared": "3.4.15", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", - "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", - "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/compiler-core": "3.4.15", - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15", + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", - "postcss": "^8.4.33", - "source-map-js": "^1.0.2" + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", - "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/devtools-api": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.0.14.tgz", - "integrity": "sha512-TluWR9qZ6aO11bwtYK8+fzXxBqLfsE0mWZz1q/EQBmO9k82Cm6deieLwNNXjNFJz7xutazoia5Qa+zTYkPPOfw==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.2.tgz", + "integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==", "dev": true, "dependencies": { - "@vue/devtools-kit": "^7.0.14" + "@vue/devtools-kit": "^7.7.2" } }, "node_modules/@vue/devtools-kit": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.0.14.tgz", - "integrity": "sha512-wAAJazr4hI0aVRpgWOCVPw+NzMQdthhnprHHIg4njp1MkKrpCNGQ7MtQbZF1AltAA7xpMCGyyt+0kYH0FqTiPg==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz", + "integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==", "dev": true, "dependencies": { - "@vue/devtools-schema": "^7.0.14", - "@vue/devtools-shared": "^7.0.14", + "@vue/devtools-shared": "^7.7.2", + "birpc": "^0.2.19", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1" + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" } }, - "node_modules/@vue/devtools-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@vue/devtools-schema/-/devtools-schema-7.0.14.tgz", - "integrity": "sha512-tpUeCLVrdHX+KzWMLTAwx/vAPFbo6jAUi7sr6Q+0mBIqIVSSIxNr5wEhegiFvYva+OtDeM2OrT+f7/X/5bvZNg==", - "dev": true - }, "node_modules/@vue/devtools-shared": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.0.14.tgz", - "integrity": "sha512-79RP1NDakBVWou9rDpVnT1WMjTbL1lJKm6YEOodjQ0dq5ehf0wsRbeYDhgAlnjehWRzTq5GAYFBFUPYBs0/QpA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz", + "integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==", "dev": true, "dependencies": { - "rfdc": "^1.3.1" + "rfdc": "^1.4.1" } }, "node_modules/@vue/reactivity": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", - "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "dev": true, "dependencies": { - "@vue/shared": "3.4.15" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", - "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "dev": true, "dependencies": { - "@vue/reactivity": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", - "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "dev": true, "dependencies": { - "@vue/runtime-core": "3.4.15", - "@vue/shared": "3.4.15", + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", - "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.4.15" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", - "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "dev": true }, "node_modules/@vueuse/core": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz", - "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==", + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", "dev": true, "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.7.2", - "@vueuse/shared": "10.7.2", - "vue-demi": ">=0.14.6" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } } }, "node_modules/@vueuse/integrations": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.7.2.tgz", - "integrity": "sha512-+u3RLPFedjASs5EKPc69Ge49WNgqeMfSxFn+qrQTzblPXZg6+EFzhjarS5edj2qAf6xQ93f95TUxRwKStXj/sQ==", + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", "dev": true, "dependencies": { - "@vueuse/core": "10.7.2", - "@vueuse/shared": "10.7.2", - "vue-demi": ">=0.14.6" + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "async-validator": "*", - "axios": "*", - "change-case": "*", - "drauu": "*", - "focus-trap": "*", - "fuse.js": "*", - "idb-keyval": "*", - "jwt-decode": "*", - "nprogress": "*", - "qrcode": "*", - "sortablejs": "*", - "universal-cookie": "*" + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" }, "peerDependenciesMeta": { "async-validator": { @@ -1130,99 +1375,193 @@ } } }, - "node_modules/@vueuse/integrations/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.23.0.tgz", + "integrity": "sha512-7TCj+hLx6fZKppLL74lYGDEltSBNSu4vqRwgqeIKZ3VQ0q3aOrdEN0f1sDWcvU1b+psn2wnl7aHt9hWtYatUUA==", + "dev": true, + "dependencies": { + "@algolia/client-abtesting": "5.23.0", + "@algolia/client-analytics": "5.23.0", + "@algolia/client-common": "5.23.0", + "@algolia/client-insights": "5.23.0", + "@algolia/client-personalization": "5.23.0", + "@algolia/client-query-suggestions": "5.23.0", + "@algolia/client-search": "5.23.0", + "@algolia/ingestion": "1.23.0", + "@algolia/monitoring": "1.23.0", + "@algolia/recommend": "5.23.0", + "@algolia/requester-browser-xhr": "5.23.0", + "@algolia/requester-fetch": "5.23.0", + "@algolia/requester-node-http": "5.23.0" }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@vueuse/metadata": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz", - "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==", + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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 + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@vueuse/shared": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz", - "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==", + "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, "dependencies": { - "vue-demi": ">=0.14.6" - }, + "balanced-match": "^1.0.0" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, "funding": { - "url": "https://github.com/sponsors/antfu" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "dev": true, - "hasInstallScript": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "dependencies": { + "color-name": "~1.1.4" }, "engines": { - "node": ">=12" - }, + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, "funding": { - "url": "https://github.com/sponsors/antfu" + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" + "engines": { + "node": ">=12.13" }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/algoliasearch": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.22.1.tgz", - "integrity": "sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==", - "dev": true, - "dependencies": { - "@algolia/cache-browser-local-storage": "4.22.1", - "@algolia/cache-common": "4.22.1", - "@algolia/cache-in-memory": "4.22.1", - "@algolia/client-account": "4.22.1", - "@algolia/client-analytics": "4.22.1", - "@algolia/client-common": "4.22.1", - "@algolia/client-personalization": "4.22.1", - "@algolia/client-search": "4.22.1", - "@algolia/logger-common": "4.22.1", - "@algolia/logger-console": "4.22.1", - "@algolia/requester-browser-xhr": "4.22.1", - "@algolia/requester-common": "4.22.1", - "@algolia/requester-node-http": "4.22.1", - "@algolia/transporter": "4.22.1" + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, "node_modules/csstype": { @@ -1231,8 +1570,48 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, - "node_modules/entities": { - "version": "4.5.0", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, @@ -1281,21 +1660,62 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", "dev": true, "dependencies": { "tabbable": "^6.2.0" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1310,22 +1730,179 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "dev": true }, - "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, "engines": { - "node": ">=12" + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/mark.js": { @@ -1334,10 +1911,144 @@ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "dev": true }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/minisearch": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", - "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", + "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", "dev": true }, "node_modules/mitt": { @@ -1347,9 +2058,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1364,6 +2075,48 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1371,15 +2124,15 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -1396,8 +2149,8 @@ } ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1405,28 +2158,71 @@ } }, "node_modules/preact": { - "version": "10.19.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", - "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "version": "10.26.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz", + "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==", "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/qsu": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/qsu/-/qsu-1.10.0.tgz", + "integrity": "sha512-60UGE7IEYXX/xy/n1w7vDm+is43pmePajAdXAnFOczvVDJKbVqZ5/RvRy+yobjA4iQitr4H/4zojVFtAkNWW9g==", + "dev": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true + }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, "node_modules/rollup": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", + "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1436,39 +2232,96 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.4", - "@rollup/rollup-android-arm64": "4.22.4", - "@rollup/rollup-darwin-arm64": "4.22.4", - "@rollup/rollup-darwin-x64": "4.22.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", - "@rollup/rollup-linux-arm-musleabihf": "4.22.4", - "@rollup/rollup-linux-arm64-gnu": "4.22.4", - "@rollup/rollup-linux-arm64-musl": "4.22.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", - "@rollup/rollup-linux-riscv64-gnu": "4.22.4", - "@rollup/rollup-linux-s390x-gnu": "4.22.4", - "@rollup/rollup-linux-x64-gnu": "4.22.4", - "@rollup/rollup-linux-x64-musl": "4.22.4", - "@rollup/rollup-win32-arm64-msvc": "4.22.4", - "@rollup/rollup-win32-ia32-msvc": "4.22.4", - "@rollup/rollup-win32-x64-msvc": "4.22.4", + "@rollup/rollup-android-arm-eabi": "4.37.0", + "@rollup/rollup-android-arm64": "4.37.0", + "@rollup/rollup-darwin-arm64": "4.37.0", + "@rollup/rollup-darwin-x64": "4.37.0", + "@rollup/rollup-freebsd-arm64": "4.37.0", + "@rollup/rollup-freebsd-x64": "4.37.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", + "@rollup/rollup-linux-arm-musleabihf": "4.37.0", + "@rollup/rollup-linux-arm64-gnu": "4.37.0", + "@rollup/rollup-linux-arm64-musl": "4.37.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-musl": "4.37.0", + "@rollup/rollup-linux-s390x-gnu": "4.37.0", + "@rollup/rollup-linux-x64-gnu": "4.37.0", + "@rollup/rollup-linux-x64-musl": "4.37.0", + "@rollup/rollup-win32-arm64-msvc": "4.37.0", + "@rollup/rollup-win32-ia32-msvc": "4.37.0", + "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" } }, "node_modules/search-insights": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz", - "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", "dev": true, "peer": true }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.0.0-rc.0.tgz", - "integrity": "sha512-aeEjERF5qeK+YChgEv94LOjcEcjZBLd0acPaHginz0N8FvyTn2iSLhO0AtoqfvlZ8cWGCJRKLWtDApnQVQB6/Q==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", "dev": true, "dependencies": { - "@shikijs/core": "1.0.0-rc.0" + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/source-map-js": { @@ -1480,6 +2333,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -1489,18 +2352,260 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "dev": true }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "5.4.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", + "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1556,33 +2661,36 @@ } }, "node_modules/vitepress": { - "version": "1.0.0-rc.42", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-rc.42.tgz", - "integrity": "sha512-VeiVVXFblt/sjruFSJBNChMWwlztMrRMe8UXdNpf4e05mKtTYEY38MF5qoP90KxPTCfMQiKqwEGwXAGuOTK8HQ==", - "dev": true, - "dependencies": { - "@docsearch/css": "^3.5.2", - "@docsearch/js": "^3.5.2", - "@shikijs/core": "^1.0.0-rc.0", - "@shikijs/transformers": "^1.0.0-rc.0", - "@types/markdown-it": "^13.0.7", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/devtools-api": "^7.0.14", - "@vueuse/core": "^10.7.2", - "@vueuse/integrations": "^10.7.2", - "focus-trap": "^7.5.4", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.3.tgz", + "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", + "dev": true, + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", "mark.js": "8.11.1", - "minisearch": "^6.3.0", - "shiki": "^1.0.0-rc.0", - "vite": "^5.0.12", - "vue": "^3.4.15" + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" }, "bin": { "vitepress": "bin/vitepress.js" }, "peerDependencies": { - "markdown-it-mathjax3": "^4.3.2", - "postcss": "^8.4.34" + "markdown-it-mathjax3": "^4", + "postcss": "^8" }, "peerDependenciesMeta": { "markdown-it-mathjax3": { @@ -1593,17 +2701,31 @@ } } }, + "node_modules/vitepress-sidebar": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/vitepress-sidebar/-/vitepress-sidebar-1.31.1.tgz", + "integrity": "sha512-Hx10z5le87jIIXVfKq4AtRrVqVJJ/1cQsZhmwT+ghVR/j4Yor9FjNMszyigJ54ktrEtoxSLO6C9tvuLauT4lZA==", + "dev": true, + "dependencies": { + "glob": "10.4.5", + "gray-matter": "4.0.3", + "qsu": "^1.10.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/vue": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", - "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-sfc": "3.4.15", - "@vue/runtime-dom": "3.4.15", - "@vue/server-renderer": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" @@ -1613,6 +2735,122 @@ "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index c395edff3..2b0ffe33a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,16 @@ { + "name": "docs", "devDependencies": { - "vitepress": "^1.0.0-rc.42" + "vitepress": "~1.6.3", + "vitepress-sidebar": "^1.31.1" }, "scripts": { "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs" + "docs:preview": "vitepress preview docs", + "cloc": "git ls-files -z | grep -z -v -E '(^|/)(package-lock\\.json|poetry\\.lock|docs/\\.vitepress/dist/|metrics/grafana/config/provisioning/dashboards/)' | xargs -0 npx cloc" + }, + "dependencies": { + } -} \ No newline at end of file +} diff --git a/ui/.eslintrc-auto-import.json b/ui/.eslintrc-auto-import.json index 7a523d04f..2790769cd 100644 --- a/ui/.eslintrc-auto-import.json +++ b/ui/.eslintrc-auto-import.json @@ -299,6 +299,11 @@ "injectLocal": true, "provideLocal": true, "useQueryPersistence": true, - "useClipboardItems": true + "useClipboardItems": true, + "createRef": true, + "onElementRemoval": true, + "useCountdown": true, + "usePreferredReducedTransparency": true, + "useSSRWidth": true } } diff --git a/ui/auto-imports.d.ts b/ui/auto-imports.d.ts index 5e74a8505..8dc6c9f29 100644 --- a/ui/auto-imports.d.ts +++ b/ui/auto-imports.d.ts @@ -27,6 +27,7 @@ declare global { const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createRef: typeof import('@vueuse/core')['createRef'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] @@ -61,6 +62,7 @@ declare global { const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onDeactivated: typeof import('vue')['onDeactivated'] + const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval'] const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] const onLongPress: typeof import('@vueuse/core')['onLongPress'] @@ -143,6 +145,7 @@ declare global { const useCloned: typeof import('@vueuse/core')['useCloned'] const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] + const useCountdown: typeof import('@vueuse/core')['useCountdown'] const useCounter: typeof import('@vueuse/core')['useCounter'] const useCssModule: typeof import('vue')['useCssModule'] const useCssVar: typeof import('@vueuse/core')['useCssVar'] @@ -221,6 +224,7 @@ declare global { const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] + const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency'] const usePrevious: typeof import('@vueuse/core')['usePrevious'] const useQueryPersistence: typeof import('./src/composables/useQueryPersistence.js')['default'] const useRafFn: typeof import('@vueuse/core')['useRafFn'] @@ -228,6 +232,7 @@ declare global { const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] const useRoute: typeof import('vue-router')['useRoute'] const useRouter: typeof import('vue-router')['useRouter'] + const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] @@ -327,6 +332,7 @@ declare module 'vue' { readonly createGlobalState: UnwrapRef readonly createInjectionState: UnwrapRef readonly createReactiveFn: UnwrapRef + readonly createRef: UnwrapRef readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef readonly createTemplatePromise: UnwrapRef @@ -361,6 +367,7 @@ declare module 'vue' { readonly onBeforeUpdate: UnwrapRef readonly onClickOutside: UnwrapRef readonly onDeactivated: UnwrapRef + readonly onElementRemoval: UnwrapRef readonly onErrorCaptured: UnwrapRef readonly onKeyStroke: UnwrapRef readonly onLongPress: UnwrapRef @@ -442,6 +449,7 @@ declare module 'vue' { readonly useCloned: UnwrapRef readonly useColorMode: UnwrapRef readonly useConfirmDialog: UnwrapRef + readonly useCountdown: UnwrapRef readonly useCounter: UnwrapRef readonly useCssModule: UnwrapRef readonly useCssVar: UnwrapRef @@ -520,6 +528,7 @@ declare module 'vue' { readonly usePreferredDark: UnwrapRef readonly usePreferredLanguages: UnwrapRef readonly usePreferredReducedMotion: UnwrapRef + readonly usePreferredReducedTransparency: UnwrapRef readonly usePrevious: UnwrapRef readonly useQueryPersistence: UnwrapRef readonly useRafFn: UnwrapRef @@ -527,6 +536,7 @@ declare module 'vue' { readonly useResizeObserver: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef + readonly useSSRWidth: UnwrapRef readonly useScreenOrientation: UnwrapRef readonly useScreenSafeArea: UnwrapRef readonly useScriptTag: UnwrapRef @@ -619,6 +629,7 @@ declare module '@vue/runtime-core' { readonly createGlobalState: UnwrapRef readonly createInjectionState: UnwrapRef readonly createReactiveFn: UnwrapRef + readonly createRef: UnwrapRef readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef readonly createTemplatePromise: UnwrapRef @@ -653,6 +664,7 @@ declare module '@vue/runtime-core' { readonly onBeforeUpdate: UnwrapRef readonly onClickOutside: UnwrapRef readonly onDeactivated: UnwrapRef + readonly onElementRemoval: UnwrapRef readonly onErrorCaptured: UnwrapRef readonly onKeyStroke: UnwrapRef readonly onLongPress: UnwrapRef @@ -734,6 +746,7 @@ declare module '@vue/runtime-core' { readonly useCloned: UnwrapRef readonly useColorMode: UnwrapRef readonly useConfirmDialog: UnwrapRef + readonly useCountdown: UnwrapRef readonly useCounter: UnwrapRef readonly useCssModule: UnwrapRef readonly useCssVar: UnwrapRef @@ -812,6 +825,7 @@ declare module '@vue/runtime-core' { readonly usePreferredDark: UnwrapRef readonly usePreferredLanguages: UnwrapRef readonly usePreferredReducedMotion: UnwrapRef + readonly usePreferredReducedTransparency: UnwrapRef readonly usePrevious: UnwrapRef readonly useQueryPersistence: UnwrapRef readonly useRafFn: UnwrapRef @@ -819,6 +833,7 @@ declare module '@vue/runtime-core' { readonly useResizeObserver: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef + readonly useSSRWidth: UnwrapRef readonly useScreenOrientation: UnwrapRef readonly useScreenSafeArea: UnwrapRef readonly useScriptTag: UnwrapRef diff --git a/ui/src/services/metrics.js b/ui/src/services/metrics.js index af2d9ca1f..b35dd23c0 100644 --- a/ui/src/services/metrics.js +++ b/ui/src/services/metrics.js @@ -2,7 +2,7 @@ import api from "./api"; class MetricsService { getLatest() { - return api.get("/metrics/latest").then((res) => { + return api.get("/resource-metrics/latest").then((res) => { res.data.forEach((metric) => { // try to convert 'limit' and 'usage' to numbers if (!isNaN(metric?.limit)) metric.limit = Number(metric.limit); @@ -13,7 +13,7 @@ class MetricsService { } getSpaceUtilizationByTimeAndMeasurement(measurement) { - return api.get("/metrics/space-utilization-by-timestamp", { + return api.get("/resource-metrics/space-utilization-by-timestamp", { params: { measurement, },