diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5ab4315..5a252a7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -106,3 +106,44 @@ jobs:
CHANNEL=${{ steps.build.outputs.channel }}
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
+
+ build-adminpanel:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/falkendev/grindify-adminpanel
+ tags: |
+ type=raw,value=latest,enable=${{ github.ref_type == 'tag' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
+ type=ref,event=tag
+
+ - uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push Adminpanel
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: Dockerfile.adminpanel
+ target: production
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ VITE_API_URL=${{ vars.VITE_API_URL }}
+ cache-from: type=gha,scope=adminpanel
+ cache-to: type=gha,mode=max,scope=adminpanel
diff --git a/Dockerfile.adminpanel b/Dockerfile.adminpanel
new file mode 100644
index 0000000..d4e0b63
--- /dev/null
+++ b/Dockerfile.adminpanel
@@ -0,0 +1,50 @@
+# Development stage
+FROM node:22-bookworm-slim AS development
+
+WORKDIR /app
+
+ARG VITE_API_URL
+ENV VITE_API_URL=$VITE_API_URL
+
+COPY ./adminpanel/package*.json ./
+RUN npm ci
+
+COPY ./adminpanel .
+
+EXPOSE 3001
+CMD ["npm", "run", "dev", "--", "--port", "3001", "--host"]
+
+# Build stage
+FROM node:22-bookworm-slim AS builder
+
+WORKDIR /app
+
+ARG VITE_API_URL
+ENV VITE_API_URL=$VITE_API_URL
+
+COPY ./adminpanel/package*.json ./
+RUN npm ci
+
+COPY ./adminpanel .
+RUN npm run build
+
+# Serve stage
+FROM nginx:alpine AS production
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+RUN echo 'server { \
+ listen 3001; \
+ root /usr/share/nginx/html; \
+ index index.html; \
+ \
+ location /assets/ { \
+ add_header Cache-Control "public, max-age=31536000, immutable"; \
+ try_files $uri =404; \
+ } \
+ \
+ location / { try_files $uri $uri/ /index.html; } \
+}' > /etc/nginx/conf.d/default.conf
+
+EXPOSE 3001
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/adminpanel/.gitignore b/adminpanel/.gitignore
new file mode 100644
index 0000000..2ce5c6a
--- /dev/null
+++ b/adminpanel/.gitignore
@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+.env
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/adminpanel/components.json b/adminpanel/components.json
new file mode 100644
index 0000000..42c479e
--- /dev/null
+++ b/adminpanel/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://shadcn-vue.com/schema.json",
+ "style": "default",
+ "typescript": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/assets/main.css",
+ "baseColor": "zinc",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "framework": "vite",
+ "aliases": {
+ "components": "@/components",
+ "composables": "@/composables",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib"
+ }
+}
diff --git a/adminpanel/index.html b/adminpanel/index.html
new file mode 100644
index 0000000..49495dd
--- /dev/null
+++ b/adminpanel/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ Grindify Admin
+
+
+
+
+
+
diff --git a/adminpanel/package-lock.json b/adminpanel/package-lock.json
new file mode 100644
index 0000000..67e853f
--- /dev/null
+++ b/adminpanel/package-lock.json
@@ -0,0 +1,3863 @@
+{
+ "name": "grindify-admin",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "grindify-admin",
+ "version": "0.1.0",
+ "dependencies": {
+ "@vee-validate/zod": "^4.13.2",
+ "@vueuse/core": "^11.3.0",
+ "@vueuse/motion": "^2.2.6",
+ "chart.js": "^4.4.7",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-vue-next": "^0.469.0",
+ "pinia": "^3.0.4",
+ "pinia-plugin-persistedstate": "^4.7.1",
+ "radix-vue": "^1.9.9",
+ "tailwind-merge": "^2.6.0",
+ "vee-validate": "^4.14.7",
+ "vue": "^3.5.13",
+ "vue-router": "^4.5.0",
+ "vue-sonner": "^1.3.0",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@types/node": "^25.7.0",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/tsconfig": "^0.9.1",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.11",
+ "vue-tsc": "^2.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@floating-ui/vue": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.11.tgz",
+ "integrity": "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6",
+ "@floating-ui/utils": "^0.2.11",
+ "vue-demi": ">=0.13.0"
+ }
+ },
+ "node_modules/@floating-ui/vue/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@internationalized/date": {
+ "version": "3.12.1",
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz",
+ "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@internationalized/number": {
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz",
+ "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nuxt/kit": {
+ "version": "3.21.5",
+ "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.5.tgz",
+ "integrity": "sha512-eGo9DjJ9NzKMbJpFU/UTd4c5iOSYuivghKD8W/jVGHs7kew+hdSMvUy401IfQB7EObKPvt/WXEutAIaTg9OsyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "c12": "^3.3.4",
+ "consola": "^3.4.2",
+ "defu": "^6.1.7",
+ "destr": "^2.0.5",
+ "errx": "^0.1.0",
+ "exsolve": "^1.0.8",
+ "ignore": "^7.0.5",
+ "jiti": "^2.7.0",
+ "klona": "^2.0.6",
+ "knitwork": "^1.3.0",
+ "mlly": "^1.8.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.3.1",
+ "rc9": "^3.0.1",
+ "scule": "^1.3.0",
+ "semver": "^7.7.4",
+ "tinyglobby": "^0.2.16",
+ "ufo": "^1.6.4",
+ "unctx": "^2.5.0",
+ "untyped": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
+ "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
+ "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
+ "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
+ "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
+ "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
+ "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
+ "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
+ "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
+ "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
+ "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
+ "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
+ "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
+ "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
+ "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
+ "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
+ "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
+ "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
+ "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
+ "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
+ "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
+ "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
+ "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
+ "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
+ "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@swc/helpers/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
+ "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/vue-virtual": {
+ "version": "3.13.24",
+ "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.24.tgz",
+ "integrity": "sha512-A0k2qF0zFSUStXSZkGXABouXr2Tw2Ztl/cVIYG9qy84uR8W7UNjAcX3DvzBS3YnDcwvLxab8v7dbmYBZ39itDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.14.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.0.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/node": {
+ "version": "25.7.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
+ "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.21.0"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@vee-validate/zod": {
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@vee-validate/zod/-/zod-4.15.1.tgz",
+ "integrity": "sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.8.3",
+ "vee-validate": "4.15.1"
+ },
+ "peerDependencies": {
+ "zod": "^3.24.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
+ "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.3",
+ "@vue/shared": "3.5.34",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-core/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==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz",
+ "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.34",
+ "@vue/shared": "3.5.34"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz",
+ "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.3",
+ "@vue/compiler-core": "3.5.34",
+ "@vue/compiler-dom": "3.5.34",
+ "@vue/compiler-ssr": "3.5.34",
+ "@vue/shared": "3.5.34",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.14",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/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==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz",
+ "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.34",
+ "@vue/shared": "3.5.34"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+ "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.7.9"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+ "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.7.9",
+ "birpc": "^2.3.0",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-kit/node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+ "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz",
+ "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.34"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz",
+ "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.34",
+ "@vue/shared": "3.5.34"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz",
+ "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.34",
+ "@vue/runtime-core": "3.5.34",
+ "@vue/shared": "3.5.34",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz",
+ "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.34",
+ "@vue/shared": "3.5.34"
+ },
+ "peerDependencies": {
+ "vue": "3.5.34"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz",
+ "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/tsconfig": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+ "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">= 5.8",
+ "vue": "^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/core": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz",
+ "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "11.3.0",
+ "@vueuse/shared": "11.3.0",
+ "vue-demi": ">=0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz",
+ "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/motion": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@vueuse/motion/-/motion-2.2.6.tgz",
+ "integrity": "sha512-gKFktPtrdypSv44SaW1oBJKLBiP6kE5NcoQ6RsAU3InemESdiAutgQncfPe/rhLSLCtL4jTAhMmFfxoR6gm5LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "^10.10.0",
+ "@vueuse/shared": "^10.10.0",
+ "csstype": "^3.1.3",
+ "framesync": "^6.1.2",
+ "popmotion": "^11.0.5",
+ "style-value-types": "^5.1.2"
+ },
+ "optionalDependencies": {
+ "@nuxt/kit": "^3.13.0"
+ },
+ "peerDependencies": {
+ "vue": ">=3.0.0"
+ }
+ },
+ "node_modules/@vueuse/motion/node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/motion/node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/motion/node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/motion/node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/motion/node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz",
+ "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.29",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
+ "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/c12": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
+ "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "chokidar": "^5.0.0",
+ "confbox": "^0.2.4",
+ "defu": "^6.1.6",
+ "dotenv": "^17.3.1",
+ "exsolve": "^1.0.8",
+ "giget": "^3.2.0",
+ "jiti": "^2.6.1",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^2.1.0",
+ "pkg-types": "^2.3.0",
+ "rc9": "^3.0.1"
+ },
+ "peerDependencies": {
+ "magicast": "*"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001792",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
+ "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/copy-anything": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+ "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.353",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
+ "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/errx": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz",
+ "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/framesync": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
+ "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/giget": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
+ "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hey-listen": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
+ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+ "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/knitwork": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
+ "integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "0.469.0",
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.469.0.tgz",
+ "integrity": "sha512-EjOap+vY3xEzCMrnaccDHO4BH3k3Lr+sOyvzRQCaayYxkxKla0w6Jr4h3cHAzA4vMSp63Dcy7vDiGeCPcCY+Gg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
+ "node_modules/mlly": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.3"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+ "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+ "dependencies": {
+ "@vue/devtools-api": "^7.7.7"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.5.0",
+ "vue": "^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pinia-plugin-persistedstate": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
+ "integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
+ "dependencies": {
+ "defu": "^6.1.4"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": ">=3.0.0",
+ "@pinia/nuxt": ">=0.10.0",
+ "pinia": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ },
+ "@pinia/nuxt": {
+ "optional": true
+ },
+ "pinia": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+ "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "confbox": "^0.2.4",
+ "exsolve": "^1.0.8",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/popmotion": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
+ "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
+ "license": "MIT",
+ "dependencies": {
+ "framesync": "6.1.2",
+ "hey-listen": "^1.0.8",
+ "style-value-types": "5.1.2",
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/radix-vue": {
+ "version": "1.9.17",
+ "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.17.tgz",
+ "integrity": "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.7",
+ "@floating-ui/vue": "^1.1.0",
+ "@internationalized/date": "^3.5.4",
+ "@internationalized/number": "^3.5.3",
+ "@tanstack/vue-virtual": "^3.8.1",
+ "@vueuse/core": "^10.11.0",
+ "@vueuse/shared": "^10.11.0",
+ "aria-hidden": "^1.2.4",
+ "defu": "^6.1.4",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^5.0.7"
+ },
+ "peerDependencies": {
+ "vue": ">= 3.2.0"
+ }
+ },
+ "node_modules/radix-vue/node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/radix-vue/node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/radix-vue/node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/radix-vue/node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/radix-vue/node_modules/nanoid": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
+ "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/rc9": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
+ "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "defu": "^6.1.6",
+ "destr": "^2.0.5"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
+ "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.3",
+ "@rollup/rollup-android-arm64": "4.60.3",
+ "@rollup/rollup-darwin-arm64": "4.60.3",
+ "@rollup/rollup-darwin-x64": "4.60.3",
+ "@rollup/rollup-freebsd-arm64": "4.60.3",
+ "@rollup/rollup-freebsd-x64": "4.60.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.3",
+ "@rollup/rollup-linux-arm64-musl": "4.60.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.3",
+ "@rollup/rollup-linux-loong64-musl": "4.60.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.3",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-musl": "4.60.3",
+ "@rollup/rollup-openbsd-x64": "4.60.3",
+ "@rollup/rollup-openharmony-arm64": "4.60.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.3",
+ "@rollup/rollup-win32-x64-gnu": "4.60.3",
+ "@rollup/rollup-win32-x64-msvc": "4.60.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scule": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/style-value-types": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
+ "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "hey-listen": "^1.0.8",
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/superjson": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+ "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
+ "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/unctx": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz",
+ "integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21",
+ "unplugin": "^2.3.11"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
+ "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unplugin": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
+ "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "acorn": "^8.15.0",
+ "picomatch": "^4.0.3",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/unplugin/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/untyped": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz",
+ "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "citty": "^0.1.6",
+ "defu": "^6.1.4",
+ "jiti": "^2.4.2",
+ "knitwork": "^1.2.0",
+ "scule": "^1.3.0"
+ },
+ "bin": {
+ "untyped": "dist/cli.mjs"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vee-validate": {
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.15.1.tgz",
+ "integrity": "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^7.5.2",
+ "type-fest": "^4.8.3"
+ },
+ "peerDependencies": {
+ "vue": "^3.4.26"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.34",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz",
+ "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.34",
+ "@vue/compiler-sfc": "3.5.34",
+ "@vue/runtime-dom": "3.5.34",
+ "@vue/server-renderer": "3.5.34",
+ "@vue/shared": "3.5.34"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/vue-sonner": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-1.3.2.tgz",
+ "integrity": "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A==",
+ "license": "MIT"
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/adminpanel/package.json b/adminpanel/package.json
new file mode 100644
index 0000000..81557f3
--- /dev/null
+++ b/adminpanel/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "grindify-admin",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vee-validate/zod": "^4.13.2",
+ "@vueuse/core": "^11.3.0",
+ "@vueuse/motion": "^2.2.6",
+ "chart.js": "^4.4.7",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-vue-next": "^0.469.0",
+ "pinia": "^3.0.4",
+ "pinia-plugin-persistedstate": "^4.7.1",
+ "radix-vue": "^1.9.9",
+ "tailwind-merge": "^2.6.0",
+ "vee-validate": "^4.14.7",
+ "vue": "^3.5.13",
+ "vue-router": "^4.5.0",
+ "vue-sonner": "^1.3.0",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@types/node": "^25.7.0",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/tsconfig": "^0.9.1",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.11",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/adminpanel/postcss.config.js b/adminpanel/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/adminpanel/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/adminpanel/public/favicon.svg b/adminpanel/public/favicon.svg
new file mode 100644
index 0000000..602d065
--- /dev/null
+++ b/adminpanel/public/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/adminpanel/src/App.vue b/adminpanel/src/App.vue
new file mode 100644
index 0000000..b83c3f8
--- /dev/null
+++ b/adminpanel/src/App.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/adminpanel/src/assets/main.css b/adminpanel/src/assets/main.css
new file mode 100644
index 0000000..af03c23
--- /dev/null
+++ b/adminpanel/src/assets/main.css
@@ -0,0 +1,150 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ /* Backgrounds */
+ --bg: #09090b;
+ --bg-2: #0c0c0e;
+ --surface: #131316;
+ --surface-2: #18181b;
+ --surface-3: #1f1f23;
+ --surface-hi: #27272a;
+
+ /* Borders */
+ --border: #1f1f23;
+ --border-2: #2a2a2f;
+ --border-strong: #3f3f46;
+
+ /* Text */
+ --text: #fafafa;
+ --text-2: #d4d4d8;
+ --mute: #a1a1aa;
+ --dim: #71717a;
+ --faint: #52525b;
+ --ghost: #3f3f46;
+
+ /* Accent — lime (default) */
+ --lime: #c4f74a;
+ --lime-2: #a3e635;
+ --lime-deep: #84cc16;
+ --lime-ink: #1a2009;
+ --lime-soft: rgba(196, 247, 74, 0.12);
+ --lime-soft-2: rgba(196, 247, 74, 0.22);
+
+ /* Semantic colors */
+ --emerald: #34d399;
+ --emerald-soft: rgba(52, 211, 153, 0.14);
+ --amber: #f59e0b;
+ --amber-soft: rgba(245, 158, 11, 0.14);
+ --red: #f87171;
+ --red-soft: rgba(248, 113, 113, 0.14);
+ --blue: #60a5fa;
+ --blue-soft: rgba(96, 165, 250, 0.14);
+ --violet: #a78bfa;
+ --violet-soft: rgba(167, 139, 250, 0.14);
+
+ /* Radii */
+ --r-card: 16px;
+ --r-inner: 12px;
+ --r-pill: 999px;
+ --r-chip: 8px;
+
+ /* Shadows */
+ --shadow-card: 0 1px 0 rgba(255,255,255,0.03) inset, 0 0 0 1px var(--border);
+ --shadow-pop: 0 24px 60px -20px rgba(0,0,0,0.6), 0 8px 24px -12px rgba(0,0,0,0.45), 0 0 0 1px var(--border-2);
+
+ /* Layout */
+ --side-w: 248px;
+ --side-w-collapsed: 64px;
+ --top-h: 60px;
+}
+
+/* ── Base resets ──────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--bg);
+ color: var(--text);
+ font-family: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font-size: 14px;
+ line-height: 1.45;
+ letter-spacing: -0.005em;
+}
+
+button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
+input, select, textarea { font: inherit; color: inherit; }
+a { color: inherit; text-decoration: none; }
+h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.02em; }
+
+::selection { background: var(--lime-soft-2); color: var(--text); }
+
+/* ── Scrollbar ────────────────────────────────────────────────── */
+::-webkit-scrollbar { width: 10px; height: 10px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: #2a2a2f; border-radius: 8px; border: 2px solid var(--bg); }
+::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
+
+/* ── Page header ──────────────────────────────────────────────── */
+.page-head {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 22px;
+}
+
+.page-head .titles h1 {
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+}
+
+.page-head .titles p {
+ margin: 4px 0 0;
+ color: var(--mute);
+ font-size: 13.5px;
+}
+
+.page-head .actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* ── App shell ────────────────────────────────────────────────── */
+#app {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: var(--side-w) 1fr;
+ transition: grid-template-columns 0.2s ease;
+}
+
+#app.collapsed {
+ grid-template-columns: var(--side-w-collapsed) 1fr;
+}
+
+@media (max-width: 900px) {
+ #app { grid-template-columns: 1fr; }
+}
+
+/* ── Main content area ────────────────────────────────────────── */
+.admin-main {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 100vh;
+}
+
+.admin-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 28px 28px 40px;
+}
diff --git a/adminpanel/src/components/ComingSoon.vue b/adminpanel/src/components/ComingSoon.vue
new file mode 100644
index 0000000..622acab
--- /dev/null
+++ b/adminpanel/src/components/ComingSoon.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
{{ title }}
+
{{ description }}
+
+
+
+
+
+
Coming soon
+
This section is under construction.
+
+
diff --git a/adminpanel/src/components/layout/AppSidebar.vue b/adminpanel/src/components/layout/AppSidebar.vue
new file mode 100644
index 0000000..0e6c133
--- /dev/null
+++ b/adminpanel/src/components/layout/AppSidebar.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
diff --git a/adminpanel/src/components/layout/AppTopbar.vue b/adminpanel/src/components/layout/AppTopbar.vue
new file mode 100644
index 0000000..53c06a0
--- /dev/null
+++ b/adminpanel/src/components/layout/AppTopbar.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/adminpanel/src/composables/useSidebar.ts b/adminpanel/src/composables/useSidebar.ts
new file mode 100644
index 0000000..39a82fb
--- /dev/null
+++ b/adminpanel/src/composables/useSidebar.ts
@@ -0,0 +1,14 @@
+import { useStorage } from '@vueuse/core'
+import { computed } from 'vue'
+
+const collapsed = useStorage('grindify-admin-sidebar-collapsed', false)
+
+export function useSidebar() {
+ const toggle = () => {
+ collapsed.value = !collapsed.value
+ }
+
+ const sidebarClass = computed(() => collapsed.value ? 'collapsed' : '')
+
+ return { collapsed, toggle, sidebarClass }
+}
diff --git a/adminpanel/src/config/navigation.ts b/adminpanel/src/config/navigation.ts
new file mode 100644
index 0000000..1351e52
--- /dev/null
+++ b/adminpanel/src/config/navigation.ts
@@ -0,0 +1,122 @@
+export interface NavSection {
+ section: string
+}
+
+export interface NavItem {
+ id: string
+ label: string
+ icon: string
+ path: string
+ description: string
+ badge?: number
+}
+
+export type NavEntry = NavSection | NavItem
+
+export function isNavSection(entry: NavEntry): entry is NavSection {
+ return 'section' in entry
+}
+
+export function isNavItem(entry: NavEntry): entry is NavItem {
+ return 'id' in entry
+}
+
+export const ADMIN_NAV: NavEntry[] = [
+ { section: 'Workspace' },
+ {
+ id: 'a-overview',
+ label: 'Overview',
+ icon: 'LayoutDashboard',
+ path: '/',
+ description: 'Instance overview and aggregate stats',
+ },
+ {
+ id: 'a-users',
+ label: 'Users',
+ icon: 'Users',
+ path: '/users',
+ description: 'Manage user accounts, roles, and access',
+ },
+ { section: 'Content' },
+ {
+ id: 'a-exercises',
+ label: 'Global Exercises',
+ icon: 'Dumbbell',
+ path: '/exercises',
+ description: 'Manage the global exercise library',
+ },
+ {
+ id: 'a-images',
+ label: 'Exercise Images',
+ icon: 'Image',
+ path: '/exercise-images',
+ description: 'Upload and manage exercise media',
+ },
+ {
+ id: 'a-templates',
+ label: 'Workout Templates',
+ icon: 'Layers',
+ path: '/templates',
+ description: 'Curated workout templates for users',
+ },
+ {
+ id: 'a-activities',
+ label: 'Activity Types',
+ icon: 'Activity',
+ path: '/activities',
+ description: 'Configure activity types and metrics',
+ },
+ {
+ id: 'a-muscle-groups',
+ label: 'Muscle Groups',
+ icon: 'Layers',
+ path: '/muscle-groups',
+ description: 'Edit muscle group display names and translations',
+ },
+ { section: 'Data' },
+ {
+ id: 'a-analytics',
+ label: 'Analytics',
+ icon: 'BarChart2',
+ path: '/analytics',
+ description: 'Instance-wide usage and engagement analytics',
+ },
+ {
+ id: 'a-io',
+ label: 'Import / Export',
+ icon: 'Package',
+ path: '/import-export',
+ description: 'Bulk data import, export, and migrations',
+ },
+ {
+ id: 'a-audit',
+ label: 'Audit Logs',
+ icon: 'ScrollText',
+ path: '/audit-logs',
+ description: 'Full event log for all admin actions',
+ },
+ { section: 'Instance' },
+ {
+ id: 'a-health',
+ label: 'System Health',
+ icon: 'ShieldCheck',
+ path: '/system-health',
+ description: 'Service status, uptime, and diagnostics',
+ },
+ {
+ id: 'a-versions',
+ label: 'Versions',
+ icon: 'History',
+ path: '/versions',
+ description: 'Release history and update management',
+ },
+ {
+ id: 'a-settings',
+ label: 'Settings',
+ icon: 'Settings2',
+ path: '/settings',
+ description: 'Instance configuration and preferences',
+ },
+]
+
+export const NAV_ITEMS = ADMIN_NAV.filter(isNavItem) as NavItem[]
diff --git a/adminpanel/src/lib/utils.ts b/adminpanel/src/lib/utils.ts
new file mode 100644
index 0000000..d32b0fe
--- /dev/null
+++ b/adminpanel/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/adminpanel/src/main.ts b/adminpanel/src/main.ts
new file mode 100644
index 0000000..327d7b0
--- /dev/null
+++ b/adminpanel/src/main.ts
@@ -0,0 +1,16 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import { MotionPlugin } from '@vueuse/motion'
+import App from './App.vue'
+import router from './router'
+import './assets/main.css'
+
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+
+createApp(App)
+ .use(pinia)
+ .use(router)
+ .use(MotionPlugin)
+ .mount('#app')
diff --git a/adminpanel/src/router/index.ts b/adminpanel/src/router/index.ts
new file mode 100644
index 0000000..7338bff
--- /dev/null
+++ b/adminpanel/src/router/index.ts
@@ -0,0 +1,162 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { useAuthStore } from '@/stores/auth.store'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/login',
+ name: 'login',
+ component: () => import('@/views/LoginView.vue'),
+ meta: { layout: 'auth', title: 'Sign in' },
+ },
+ {
+ path: '/',
+ name: 'overview',
+ component: () => import('@/views/OverviewView.vue'),
+ meta: {
+ title: 'Overview',
+ description: 'Instance overview and aggregate stats',
+ navId: 'a-overview',
+ },
+ },
+ {
+ path: '/users',
+ name: 'users',
+ component: () => import('@/views/UsersView.vue'),
+ meta: {
+ title: 'Users',
+ description: 'Manage user accounts, roles, and access',
+ navId: 'a-users',
+ },
+ },
+ {
+ path: '/exercises',
+ name: 'global-exercises',
+ component: () => import('@/views/GlobalExercisesView.vue'),
+ meta: {
+ title: 'Global Exercises',
+ description: 'Manage the global exercise library',
+ navId: 'a-exercises',
+ },
+ },
+ {
+ path: '/exercise-images',
+ name: 'exercise-images',
+ component: () => import('@/views/ExerciseImagesView.vue'),
+ meta: {
+ title: 'Exercise Images',
+ description: 'Upload and manage exercise media',
+ navId: 'a-images',
+ },
+ },
+ {
+ path: '/templates',
+ name: 'workout-templates',
+ component: () => import('@/views/WorkoutTemplatesView.vue'),
+ meta: {
+ title: 'Workout Templates',
+ description: 'Curated workout templates for users',
+ navId: 'a-templates',
+ },
+ },
+ {
+ path: '/activities',
+ name: 'activity-types',
+ component: () => import('@/views/ActivityTypesView.vue'),
+ meta: {
+ title: 'Activity Types',
+ description: 'Configure activity types and metrics',
+ navId: 'a-activities',
+ },
+ },
+ {
+ path: '/muscle-groups',
+ name: 'muscle-groups',
+ component: () => import('@/views/MuscleGroupsView.vue'),
+ meta: {
+ title: 'Muscle Groups',
+ description: 'Edit muscle group display names and translations',
+ navId: 'a-muscle-groups',
+ },
+ },
+ {
+ path: '/analytics',
+ name: 'analytics',
+ component: () => import('@/views/AnalyticsView.vue'),
+ meta: {
+ title: 'Analytics',
+ description: 'Instance-wide usage and engagement analytics',
+ navId: 'a-analytics',
+ },
+ },
+ {
+ path: '/import-export',
+ name: 'import-export',
+ component: () => import('@/views/ImportExportView.vue'),
+ meta: {
+ title: 'Import / Export',
+ description: 'Bulk data import, export, and migrations',
+ navId: 'a-io',
+ },
+ },
+ {
+ path: '/audit-logs',
+ name: 'audit-logs',
+ component: () => import('@/views/AuditLogsView.vue'),
+ meta: {
+ title: 'Audit Logs',
+ description: 'Full event log for all admin actions',
+ navId: 'a-audit',
+ },
+ },
+ {
+ path: '/system-health',
+ name: 'system-health',
+ component: () => import('@/views/SystemHealthView.vue'),
+ meta: {
+ title: 'System Health',
+ description: 'Service status, uptime, and diagnostics',
+ navId: 'a-health',
+ },
+ },
+ {
+ path: '/versions',
+ name: 'versions',
+ component: () => import('@/views/VersionsView.vue'),
+ meta: {
+ title: 'Versions',
+ description: 'Release history and update management',
+ navId: 'a-versions',
+ },
+ },
+ {
+ path: '/settings',
+ name: 'settings',
+ component: () => import('@/views/SettingsView.vue'),
+ meta: {
+ title: 'Settings',
+ description: 'Instance configuration and preferences',
+ navId: 'a-settings',
+ },
+ },
+ ],
+})
+
+router.beforeEach(async (to) => {
+ if (to.name === 'login') return true
+
+ const authStore = useAuthStore()
+ if (!authStore.checked) {
+ await authStore.fetchMe()
+ }
+
+ if (!authStore.user) return { name: 'login' }
+ return true
+})
+
+router.afterEach((to) => {
+ document.title = to.meta.title ? `${to.meta.title} — Grindify Admin` : 'Grindify Admin'
+})
+
+export default router
diff --git a/adminpanel/src/services/adminApi.ts b/adminpanel/src/services/adminApi.ts
new file mode 100644
index 0000000..6986011
--- /dev/null
+++ b/adminpanel/src/services/adminApi.ts
@@ -0,0 +1,273 @@
+import { apiFetch, apiFetchForm, apiFetchBlob } from './api'
+
+export interface AdminUser {
+ id: number
+ email: string
+ firstName: string | null
+ lastName: string | null
+ avatar: string | null
+ role: 'user' | 'superadmin'
+ emailVerified: boolean
+ currentStreak: number
+ weeklyWorkoutGoal: number
+ onboardingCompleted: boolean
+ createdAt: string
+ updatedAt: string
+}
+
+export interface AdminUserDetail extends AdminUser {
+ currentWeekWorkouts: number
+ streakFreezes: number
+ completedGoalWeeksCount: number
+ unitScale: string | null
+ weight: number | null
+ height: number | null
+ dateOfBirth: string | null
+ gender: string | null
+ primaryGoal: string | null
+ showRpe: boolean
+ showWeightTracking: boolean
+ termsAcceptedAt: string | null
+ termsVersion: string | null
+}
+
+export interface UsersResponse {
+ users: AdminUser[]
+ total: number
+ page: number
+ limit: number
+}
+
+export interface StatsResponse {
+ totalUsers: number
+ totalWorkouts: number
+ totalSessions: number
+ newUsersLast30Days: number
+ recentUsers: Pick[]
+}
+
+export interface ReleaseEntry {
+ tagName: string
+ name: string
+ publishedAt: string | null
+ body: string
+ prerelease: boolean
+ htmlUrl: string | null
+}
+
+export interface ReleasesResponse {
+ status: 'ok' | 'unconfigured' | 'unavailable'
+ source: 'github'
+ repo: string | null
+ fetchedAt: string
+ latestReleaseVersion: string | null
+ releases: ReleaseEntry[]
+ message: string | null
+}
+
+export interface HealthResponse {
+ ok: boolean
+ at: string
+}
+
+// --- i18n types ---
+
+export interface I18nString {
+ default: string | null
+ eng?: string
+ swe?: string
+}
+
+export interface I18nStringArray {
+ default?: string[]
+ eng?: string[]
+ swe?: string[]
+}
+
+// --- Exercise Image Library ---
+
+export interface ExerciseImage {
+ id: number
+ url: string
+ fileSize: number | null
+ createdAt: string
+ usedBy: { id: number; title: Record } | null
+}
+
+// --- Global Exercise ---
+
+export interface ExerciseMediaItem {
+ id: number
+ type: 'image' | 'video'
+ url: string
+ order: number
+}
+
+export interface GlobalExercise {
+ id: number
+ title: I18nString
+ description?: I18nString | null
+ exerciseType?: string | null
+ muscleGroups: { id: number; name: string }[]
+ primaryMuscleGroups?: { id: number; name: string }[]
+ equipment: string[]
+ equipmentI18n?: I18nStringArray
+ image?: string | null
+ media?: ExerciseMediaItem[]
+ instructionsI18n?: I18nStringArray
+ proTipsI18n?: I18nStringArray
+ mistakesI18n?: I18nStringArray
+ isGlobal: boolean
+ createdAt: string
+}
+
+export interface CreateGlobalExercisePayload {
+ title: I18nString
+ description?: I18nString
+ exerciseType?: string
+ muscleGroupIds?: number[]
+ primaryMuscleGroupIds?: number[]
+ equipmentI18n?: I18nStringArray
+ imageUrl?: string | null
+ instructionsI18n?: I18nStringArray
+ proTipsI18n?: I18nStringArray
+ mistakesI18n?: I18nStringArray
+}
+
+// --- Global Activity ---
+
+export interface GlobalActivity {
+ id: number
+ title: I18nString
+ description?: I18nString | null
+ icon: string
+ equipment?: string[]
+ trackDistance: boolean
+ trackPace: boolean
+ trackElevation: boolean
+ trackCalories: boolean
+ isGlobal: boolean
+ createdAt: string
+ updatedAt: string
+}
+
+export interface CreateGlobalActivityPayload {
+ title: I18nString
+ description?: I18nString
+ icon: string
+ equipment?: string[]
+ trackDistance: boolean
+ trackPace: boolean
+ trackElevation: boolean
+ trackCalories: boolean
+}
+
+// --- Muscle Group ---
+
+export interface AdminMuscleGroup {
+ id: number
+ name: string
+ nameI18n?: I18nString
+ descriptionI18n?: I18nString
+ createdAt: string
+ updatedAt: string
+}
+
+export interface UpdateMuscleGroupPayload {
+ nameI18n?: I18nString
+ descriptionI18n?: I18nString
+}
+
+export interface CreateMuscleGroupPayload {
+ name: string
+ nameI18n?: I18nString
+ descriptionI18n?: I18nString
+}
+
+export interface ImportResult {
+ created: number
+ skipped: number
+ errors: string[]
+}
+
+export const adminApi = {
+ getMe: () => apiFetch('/admin/me'),
+ getStats: () => apiFetch('/admin/stats'),
+ getUsers: (page = 1, limit = 20, search?: string) => {
+ const params = new URLSearchParams({ page: String(page), limit: String(limit) })
+ if (search) params.set('search', search)
+ return apiFetch(`/admin/users?${params}`)
+ },
+ getUser: (id: number) => apiFetch(`/admin/users/${id}`),
+ getReleases: () => apiFetch('/releases'),
+ getHealth: () => apiFetch('/auth/health'),
+
+ // Global Exercises
+ getGlobalExercises: () => apiFetch('/admin/exercises'),
+ createGlobalExercise: (data: CreateGlobalExercisePayload) =>
+ apiFetch('/admin/exercises', { method: 'POST', body: JSON.stringify(data) }),
+ updateGlobalExercise: (id: number, data: Partial) =>
+ apiFetch(`/admin/exercises/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+ deleteGlobalExercise: (id: number) =>
+ apiFetch(`/admin/exercises/${id}`, { method: 'DELETE' }),
+
+ // Global Exercise Media
+ uploadGlobalExerciseMedia: (exerciseId: number, file: File) => {
+ const fd = new FormData()
+ fd.append('file', file)
+ return apiFetchForm(`/admin/exercises/${exerciseId}/media`, fd)
+ },
+ deleteGlobalExerciseMedia: (exerciseId: number, mediaId: number) =>
+ apiFetch(`/admin/exercises/${exerciseId}/media/${mediaId}`, { method: 'DELETE' }),
+ reorderGlobalExerciseMedia: (exerciseId: number, mediaIds: number[]) =>
+ apiFetch(`/admin/exercises/${exerciseId}/media/reorder`, {
+ method: 'PUT',
+ body: JSON.stringify({ mediaIds }),
+ }),
+
+ // Exercise Image Library
+ getExerciseImages: () => apiFetch('/admin/exercise-images'),
+ uploadExerciseImage: (file: File) => {
+ const fd = new FormData()
+ fd.append('file', file)
+ return apiFetchForm('/admin/exercise-images', fd)
+ },
+ deleteExerciseImage: (id: number) =>
+ apiFetch(`/admin/exercise-images/${id}`, { method: 'DELETE' }),
+
+ // Global Activities
+ getGlobalActivities: () => apiFetch('/admin/activities'),
+ createGlobalActivity: (data: CreateGlobalActivityPayload) =>
+ apiFetch('/admin/activities', { method: 'POST', body: JSON.stringify(data) }),
+ updateGlobalActivity: (id: number, data: Partial) =>
+ apiFetch(`/admin/activities/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+ deleteGlobalActivity: (id: number) =>
+ apiFetch(`/admin/activities/${id}`, { method: 'DELETE' }),
+
+ // Muscle Groups
+ getMuscleGroups: () => apiFetch('/admin/muscle-groups'),
+ createMuscleGroup: (data: CreateMuscleGroupPayload) =>
+ apiFetch('/admin/muscle-groups', { method: 'POST', body: JSON.stringify(data) }),
+ updateMuscleGroup: (id: number, data: UpdateMuscleGroupPayload) =>
+ apiFetch(`/admin/muscle-groups/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
+ getMuscleGroupExercises: (id: number) =>
+ apiFetch<{ id: number; title: I18nString }[]>(`/admin/muscle-groups/${id}/exercises`),
+ deleteMuscleGroup: (id: number) =>
+ apiFetch(`/admin/muscle-groups/${id}`, { method: 'DELETE' }),
+
+ // Export
+ exportExercises: () => apiFetchBlob('/admin/export/exercises'),
+ exportActivities: () => apiFetchBlob('/admin/export/activities'),
+
+ // Import
+ importExercises: (file: File) => {
+ const fd = new FormData()
+ fd.append('file', file)
+ return apiFetchForm('/admin/import/exercises', fd)
+ },
+ importActivities: (file: File) => {
+ const fd = new FormData()
+ fd.append('file', file)
+ return apiFetchForm('/admin/import/activities', fd)
+ },
+}
diff --git a/adminpanel/src/services/api.ts b/adminpanel/src/services/api.ts
new file mode 100644
index 0000000..b495a00
--- /dev/null
+++ b/adminpanel/src/services/api.ts
@@ -0,0 +1,82 @@
+const BASE_URL = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:1337/v1'
+
+export class ApiError extends Error {
+ constructor(
+ public status: number,
+ message: string,
+ ) {
+ super(message)
+ this.name = 'ApiError'
+ }
+}
+
+export async function apiFetch(path: string, init?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, {
+ ...init,
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(init?.headers ?? {}),
+ },
+ })
+
+ if (!res.ok) {
+ let message = res.statusText
+ try {
+ const body = await res.json()
+ message = body?.message ?? message
+ } catch {
+ // ignore parse error
+ }
+ throw new ApiError(res.status, message)
+ }
+
+ if (res.status === 204) return undefined as T
+ return res.json() as Promise
+}
+
+export async function apiFetchBlob(path: string): Promise<{ blob: Blob; filename: string }> {
+ const res = await fetch(`${BASE_URL}${path}`, {
+ credentials: 'include',
+ })
+
+ if (!res.ok) {
+ let message = res.statusText
+ try {
+ const body = await res.json()
+ message = body?.message ?? message
+ } catch {
+ // ignore parse error
+ }
+ throw new ApiError(res.status, message)
+ }
+
+ const disposition = res.headers.get('Content-Disposition') ?? ''
+ const match = disposition.match(/filename="([^"]+)"/)
+ const filename = match?.[1] ?? 'export.json'
+ const blob = await res.blob()
+ return { blob, filename }
+}
+
+export async function apiFetchForm(path: string, formData: FormData, method = 'POST'): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, {
+ method,
+ credentials: 'include',
+ body: formData,
+ // Do NOT set Content-Type – browser sets it with boundary for multipart/form-data
+ })
+
+ if (!res.ok) {
+ let message = res.statusText
+ try {
+ const body = await res.json()
+ message = body?.message ?? message
+ } catch {
+ // ignore parse error
+ }
+ throw new ApiError(res.status, message)
+ }
+
+ if (res.status === 204) return undefined as T
+ return res.json() as Promise
+}
diff --git a/adminpanel/src/stores/auth.store.ts b/adminpanel/src/stores/auth.store.ts
new file mode 100644
index 0000000..3475b5c
--- /dev/null
+++ b/adminpanel/src/stores/auth.store.ts
@@ -0,0 +1,72 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+import { adminApi, type AdminUserDetail } from '@/services/adminApi'
+import { ApiError } from '@/services/api'
+
+const BASE_URL = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:1337/v1'
+
+export const useAuthStore = defineStore(
+ 'adminAuth',
+ () => {
+ const user = ref(null)
+ const checked = ref(false)
+ const loading = ref(false)
+
+ async function fetchMe(): Promise {
+ try {
+ user.value = await adminApi.getMe()
+ checked.value = true
+ return true
+ } catch {
+ user.value = null
+ checked.value = true
+ return false
+ }
+ }
+
+ async function login(email: string, password: string): Promise {
+ loading.value = true
+ try {
+ const res = await fetch(`${BASE_URL}/auth/login`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ })
+
+ if (!res.ok) {
+ let msg = 'Invalid credentials'
+ try { msg = (await res.json()).message ?? msg } catch { /* ignore */ }
+ throw new ApiError(res.status, msg)
+ }
+
+ const ok = await fetchMe()
+ if (!ok || user.value?.role !== 'superadmin') {
+ user.value = null
+ throw new ApiError(403, 'This account does not have superadmin access.')
+ }
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function logout(): Promise {
+ await fetch(`${BASE_URL}/auth/logout`, { method: 'POST', credentials: 'include' }).catch(() => {})
+ user.value = null
+ checked.value = false
+ }
+
+ function resetStore() {
+ user.value = null
+ checked.value = false
+ loading.value = false
+ }
+
+ return { user, checked, loading, fetchMe, login, logout, resetStore }
+ },
+ {
+ persist: {
+ pick: ['user'],
+ },
+ },
+)
diff --git a/adminpanel/src/stores/stats.store.ts b/adminpanel/src/stores/stats.store.ts
new file mode 100644
index 0000000..b13f766
--- /dev/null
+++ b/adminpanel/src/stores/stats.store.ts
@@ -0,0 +1,30 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+import { adminApi, type StatsResponse } from '@/services/adminApi'
+
+export const useStatsStore = defineStore('adminStats', () => {
+ const stats = ref(null)
+ const loading = ref(false)
+ const error = ref(null)
+
+ async function fetchStats(): Promise {
+ if (stats.value || loading.value) return
+ loading.value = true
+ error.value = null
+ try {
+ stats.value = await adminApi.getStats()
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : 'Failed to load stats'
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function resetStore() {
+ stats.value = null
+ loading.value = false
+ error.value = null
+ }
+
+ return { stats, loading, error, fetchStats, resetStore }
+})
diff --git a/adminpanel/src/views/ActivityTypesView.vue b/adminpanel/src/views/ActivityTypesView.vue
new file mode 100644
index 0000000..f1f0791
--- /dev/null
+++ b/adminpanel/src/views/ActivityTypesView.vue
@@ -0,0 +1,315 @@
+
+
+
+
+
+
Global Activities
+
Manage activity types visible to all users
+
+
+
+
+
+
+
+
+
+
+
+
+ | Title (default) |
+ Icon |
+ Tracking |
+ Translations |
+ Actions |
+
+
+
+
+ |
+
+
+ Loading activities…
+
+ |
+
+
+ | No activities found |
+
+
+ | {{ a.title?.default ?? '—' }} |
+ {{ a.icon }} |
+ {{ trackingFlags(a) }} |
+
+ {{ [a.title?.eng ? 'eng' : '', a.title?.swe ? 'swe' : ''].filter(Boolean).join(', ') || 'default only' }}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
{{ dialogMode === 'create' ? 'New Global Activity' : 'Edit Activity' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Delete Activity?
+
Users with activity logs will automatically receive a personal copy. This action cannot be undone.
+
+
+
+
+
+
+
diff --git a/adminpanel/src/views/AnalyticsView.vue b/adminpanel/src/views/AnalyticsView.vue
new file mode 100644
index 0000000..470a67c
--- /dev/null
+++ b/adminpanel/src/views/AnalyticsView.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/adminpanel/src/views/AuditLogsView.vue b/adminpanel/src/views/AuditLogsView.vue
new file mode 100644
index 0000000..470a67c
--- /dev/null
+++ b/adminpanel/src/views/AuditLogsView.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/adminpanel/src/views/ExerciseImagesView.vue b/adminpanel/src/views/ExerciseImagesView.vue
new file mode 100644
index 0000000..009e0a1
--- /dev/null
+++ b/adminpanel/src/views/ExerciseImagesView.vue
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+
+
Exercise Images
+
Image library for exercise thumbnails — upload here, then assign to exercises
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total images
+
{{ totalImages }}
+
+
+
+
+
+
+
+
+
Storage used
+
{{ totalStorage }}
+
+
+
+
+
+
+
+
+
Avg. size
+
{{ avgSize }}
+
+
+
+
+
+
+
+
{{ filteredImages.length }} shown
+
+
+
+
+
+
+
+
+
{{ uploadError }}
+
+
+
+
+
+
+
+
+
+ Loading images…
+
+
+
+
+
+
+
+
+
No images yet
+
Upload an image above to get started
+
+
+
+
+
+
+
+
+
+ Gallery
+
+
{{ filteredImages.length }} images
+
+
+
+
+
+
+
+
![]()
+
+
+
+ thumbnail
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ img.usedBy
+ ? (img.usedBy.title?.default ?? img.usedBy.title?.eng ?? 'Untitled')
+ : fileName(img.url) }}
+
+
+ {{ formatBytes(img.fileSize) }}
+ ·
+ {{ formatExt(img.url) }}
+
+ ·
+ in use
+
+
+
+
+
+
+
+
+
+
+
Delete Image?
+
The image file will be permanently removed. If it is assigned to an exercise, remove it there first.
+
+
+
+
+
+
+
+
+
diff --git a/adminpanel/src/views/GlobalExercisesView.vue b/adminpanel/src/views/GlobalExercisesView.vue
new file mode 100644
index 0000000..dc5eba0
--- /dev/null
+++ b/adminpanel/src/views/GlobalExercisesView.vue
@@ -0,0 +1,646 @@
+
+
+
+
+
+
Global Exercises
+
Manage the shared exercise library visible to all users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Image |
+ Title (default) |
+ Type |
+ Translations |
+ Muscles |
+ Actions |
+
+
+
+
+ |
+
+
+ Loading exercises…
+
+ |
+
+
+ | No exercises found |
+
+
+
+
+ ![]()
+
+
+ |
+ {{ ex.title?.default ?? '—' }} |
+ {{ ex.exerciseType ?? '—' }} |
+ {{ coverageFor(ex) }} |
+
+ {{ ex.muscleGroups?.map(m => m.name).join(', ') || '—' }}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
{{ dialogMode === 'create' ? 'New Global Exercise' : 'Edit Exercise' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Thumbnail]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
JPEG, PNG, WebP or MP4 · max 50 MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select Exercise Image
+
+
+
+
+
+
+
+
+
+
+
+
Delete Exercise?
+
Users with training data will automatically receive a personal copy. This action cannot be undone.
+
+
+
+
+
+
+
diff --git a/adminpanel/src/views/ImportExportView.vue b/adminpanel/src/views/ImportExportView.vue
new file mode 100644
index 0000000..70c1cef
--- /dev/null
+++ b/adminpanel/src/views/ImportExportView.vue
@@ -0,0 +1,303 @@
+
+
+
+
+
+
Import / Export
+
Bulk data operations for content migration between instances.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Global Exercises
+
Exports a ZIP archive — one folder per exercise with exercise.json and an images/ subfolder containing cover and media files.
+
+
+
+
+
+
+
+
+
+
+
+
+
Global Activity Types
+
Exports all activity types with their tracking configuration and translations.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Import Exercises
+
Accepts a JSON file exported from this admin panel. Exercises with matching titles are skipped.
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ exerciseImportError }}
+
+
+
+
+ {{ exerciseResult.created }} created, {{ exerciseResult.skipped }} skipped
+
+
+
{{ exerciseResult.errors.length }} error(s):
+
{{ err }}
+
+
+
+
+
+
+
+
+
+
Import Activity Types
+
Accepts a JSON file exported from this admin panel. Activities with matching titles are skipped.
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activityImportError }}
+
+
+
+
+ {{ activityResult.created }} created, {{ activityResult.skipped }} skipped
+
+
+
{{ activityResult.errors.length }} error(s):
+
{{ err }}
+
+
+
+
+
+
+
+
diff --git a/adminpanel/src/views/LoginView.vue b/adminpanel/src/views/LoginView.vue
new file mode 100644
index 0000000..1be594f
--- /dev/null
+++ b/adminpanel/src/views/LoginView.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
Sign in
+
Superadmin access only
+
+
+
+
+
diff --git a/adminpanel/src/views/MuscleGroupsView.vue b/adminpanel/src/views/MuscleGroupsView.vue
new file mode 100644
index 0000000..2f0b863
--- /dev/null
+++ b/adminpanel/src/views/MuscleGroupsView.vue
@@ -0,0 +1,394 @@
+
+
+
+
+
+
Muscle Groups
+
Manage muscle groups — create, edit translations, or remove
+
+
+
+
+
+
+
+
+
+
+
+
+ | Key |
+ Default name |
+ English |
+ Swedish |
+ Actions |
+
+
+
+
+ |
+
+
+ Loading muscle groups…
+
+ |
+
+
+ | No muscle groups found |
+
+
+ | {{ mg.name }} |
+ {{ mg.nameI18n?.default ?? mg.name }} |
+
+ {{ mg.nameI18n?.eng ?? '—' }}
+ |
+
+ {{ mg.nameI18n?.swe ?? '—' }}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ {{ dialogMode === 'create' ? 'New Muscle Group' : 'Edit Muscle Group' }}
+ {{ form.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Delete Muscle Group?
+
+
+
+
+
+ You are about to delete
+ {{ pendingDeleteMg.nameI18n?.default ?? pendingDeleteMg.name }}
+ ({{ pendingDeleteMg.name }}).
+
+
+
+
+
+ Checking exercise references…
+
+
+
+
+
+
+
+ {{ deleteExercises.length }} exercise{{ deleteExercises.length === 1 ? '' : 's' }} reference this muscle group
+
+
+ -
+ {{ ex.title?.default ?? ex.title?.eng ?? `Exercise #${ex.id}` }}
+
+
+
+ The muscle group will be removed from these exercises. The exercises themselves will not be deleted.
+
+
+
+
+
+
+
+
+ No exercises reference this muscle group. This action cannot be undone.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/adminpanel/src/views/OverviewView.vue b/adminpanel/src/views/OverviewView.vue
new file mode 100644
index 0000000..3c2f219
--- /dev/null
+++ b/adminpanel/src/views/OverviewView.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
Overview
+
Instance overview and aggregate stats
+
+
+
+
+
+ Loading…
+
+
+
+
+
+
+
+
+
+
+
+
{{ stats.totalUsers.toLocaleString() }}
+
Total users
+
+
+
+
+
+
+
+
+
{{ stats.newUsersLast30Days.toLocaleString() }}
+
New users (30 days)
+
+
+
+
+
+
+
+
+
{{ stats.totalWorkouts.toLocaleString() }}
+
Total workouts
+
+
+
+
+
+
+
+
+
{{ stats.totalSessions.toLocaleString() }}
+
Completed sessions
+
+
+
+
+ Recent signups
+
+
+
+ {{ initials(user.firstName, user.lastName, user.email) }}
+
+
+ {{ displayName(user.firstName, user.lastName, user.email) }}
+ {{ user.email }}
+
+
{{ formatDate(user.createdAt) }}
+
+
No users yet
+
+
+
diff --git a/adminpanel/src/views/SettingsView.vue b/adminpanel/src/views/SettingsView.vue
new file mode 100644
index 0000000..470a67c
--- /dev/null
+++ b/adminpanel/src/views/SettingsView.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/adminpanel/src/views/SystemHealthView.vue b/adminpanel/src/views/SystemHealthView.vue
new file mode 100644
index 0000000..cd3afcb
--- /dev/null
+++ b/adminpanel/src/views/SystemHealthView.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
System Health
+
Service status, uptime, and diagnostics
+
+
+
+
+
+
+
+ {{ overallOk() ? 'All systems operational' : 'Service degraded' }}
+
+
+
+
+
+
+ {{ service.name }}
+
+
+
+
+ {{ service.status === 'checking' ? 'Checking…' : service.status === 'ok' ? 'Operational' : 'Error' }}
+
+
+
+
{{ service.error }}
+
+
+ {{ service.latency !== null ? `${service.latency} ms` : '—' }}
+ ·
+ Last checked {{ formatTime(service.checkedAt) }}
+
+
+
+
diff --git a/adminpanel/src/views/UsersView.vue b/adminpanel/src/views/UsersView.vue
new file mode 100644
index 0000000..f4dbd9b
--- /dev/null
+++ b/adminpanel/src/views/UsersView.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
Users
+
Manage user accounts, roles, and access
+
+
+ {{ total.toLocaleString() }} users
+
+
+
+
+
+
+
+
+
+
+
+ | User |
+ Role |
+ Streak |
+ Verified |
+ Onboarded |
+ Joined |
+
+
+
+
+ |
+
+
+ Loading users…
+
+ |
+
+
+ | No users found |
+
+
+
+
+
+ {{ initials(user) }}
+
+
+ {{ displayName(user) }}
+ {{ user.email }}
+
+
+ |
+
+
+ {{ user.role }}
+
+ |
+
+
+ {{ user.currentStreak }}
+
+ —
+ |
+
+
+
+ |
+
+
+
+ |
+ {{ formatDate(user.createdAt) }} |
+
+
+
+
+
+
+ Page {{ page }} of {{ totalPages() }}
+
+
+
+
diff --git a/adminpanel/src/views/VersionsView.vue b/adminpanel/src/views/VersionsView.vue
new file mode 100644
index 0000000..80d95dd
--- /dev/null
+++ b/adminpanel/src/views/VersionsView.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
Versions
+
Release history and update management
+
+
+
+ Latest: {{ data.latestReleaseVersion }}
+
+
+
+
+
+ Loading releases…
+
+
+
+
+
+
+ {{ data?.message ?? 'No releases found.' }}
+
+
+
+
+
+
+
+ {{ release.tagName }}
+ pre-release
+ {{ release.name }}
+
+
+
{{ formatDate(release.publishedAt) }}
+
+
+
+
+
+
+
+
+
+ {{ section.section }}
+
+
+
+
+ No release notes
+
+
+
diff --git a/adminpanel/src/views/WorkoutTemplatesView.vue b/adminpanel/src/views/WorkoutTemplatesView.vue
new file mode 100644
index 0000000..470a67c
--- /dev/null
+++ b/adminpanel/src/views/WorkoutTemplatesView.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/adminpanel/src/vite-env.d.ts b/adminpanel/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/adminpanel/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/adminpanel/tailwind.config.ts b/adminpanel/tailwind.config.ts
new file mode 100644
index 0000000..21a5d68
--- /dev/null
+++ b/adminpanel/tailwind.config.ts
@@ -0,0 +1,61 @@
+import type { Config } from 'tailwindcss'
+
+export default {
+ darkMode: ['class'],
+ content: ['./index.html', './src/**/*.{vue,ts,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ bg: 'var(--bg)',
+ 'bg-2': 'var(--bg-2)',
+ surface: 'var(--surface)',
+ 'surface-2': 'var(--surface-2)',
+ 'surface-3': 'var(--surface-3)',
+ 'surface-hi': 'var(--surface-hi)',
+ border: 'var(--border)',
+ 'border-2': 'var(--border-2)',
+ 'border-strong': 'var(--border-strong)',
+ text: 'var(--text)',
+ 'text-2': 'var(--text-2)',
+ mute: 'var(--mute)',
+ dim: 'var(--dim)',
+ faint: 'var(--faint)',
+ ghost: 'var(--ghost)',
+ lime: 'var(--lime)',
+ 'lime-2': 'var(--lime-2)',
+ 'lime-deep': 'var(--lime-deep)',
+ 'lime-ink': 'var(--lime-ink)',
+ emerald: 'var(--emerald)',
+ amber: 'var(--amber)',
+ red: 'var(--red)',
+ blue: 'var(--blue)',
+ violet: 'var(--violet)',
+ 'lime-soft': 'var(--lime-soft)',
+ 'lime-soft-2': 'var(--lime-soft-2)',
+ 'emerald-soft': 'var(--emerald-soft)',
+ 'amber-soft': 'var(--amber-soft)',
+ 'red-soft': 'var(--red-soft)',
+ 'blue-soft': 'var(--blue-soft)',
+ 'violet-soft': 'var(--violet-soft)',
+ },
+ fontFamily: {
+ sans: ['Geist', 'ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', 'sans-serif'],
+ mono: ['Geist Mono', 'ui-monospace', 'JetBrains Mono', 'Menlo', 'monospace'],
+ },
+ borderRadius: {
+ card: 'var(--r-card)',
+ inner: 'var(--r-inner)',
+ chip: 'var(--r-chip)',
+ pill: 'var(--r-pill)',
+ },
+ height: {
+ topbar: 'var(--top-h)',
+ },
+ width: {
+ sidebar: 'var(--side-w)',
+ 'sidebar-collapsed': 'var(--side-w-collapsed)',
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config
diff --git a/adminpanel/tsconfig.app.json b/adminpanel/tsconfig.app.json
new file mode 100644
index 0000000..e50f927
--- /dev/null
+++ b/adminpanel/tsconfig.app.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"]
+}
diff --git a/adminpanel/tsconfig.json b/adminpanel/tsconfig.json
new file mode 100644
index 0000000..ba5ccc4
--- /dev/null
+++ b/adminpanel/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.app.json" }
+ ]
+}
diff --git a/adminpanel/tsconfig.node.json b/adminpanel/tsconfig.node.json
new file mode 100644
index 0000000..28fd07b
--- /dev/null
+++ b/adminpanel/tsconfig.node.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"],
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["vite.config.ts", "tailwind.config.ts", "postcss.config.js"]
+}
diff --git a/adminpanel/vite.config.ts b/adminpanel/vite.config.ts
new file mode 100644
index 0000000..a52797c
--- /dev/null
+++ b/adminpanel/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+})
diff --git a/backend/package-lock.json b/backend/package-lock.json
index db655bc..50c957d 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -9,12 +9,12 @@
"version": "1.1.0",
"license": "AGPL-3.0",
"dependencies": {
- "@nestjs/common": "^11.1.3",
+ "@nestjs/common": "^11.1.21",
"@nestjs/config": "^4.0.3",
- "@nestjs/core": "^11.1.3",
+ "@nestjs/core": "^11.1.21",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
- "@nestjs/platform-express": "^11.1.17",
+ "@nestjs/platform-express": "^11.1.21",
"@nestjs/swagger": "^11.2.6",
"@nestjs/typeorm": "^11.0.0",
"@types/multer": "^2.0.0",
@@ -26,8 +26,9 @@
"cookie-parser": "^1.4.7",
"express-basic-auth": "^1.2.1",
"install": "^0.13.0",
+ "jszip": "^3.10.1",
"multer": "^2.1.1",
- "npm": "^11.11.1",
+ "npm": "^11.14.1",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
@@ -42,9 +43,9 @@
"typeorm": "^0.3.25"
},
"devDependencies": {
- "@nestjs/cli": "^11.0.16",
- "@nestjs/schematics": "^11.0.5",
- "@nestjs/testing": "^11.1.3",
+ "@nestjs/cli": "^11.0.21",
+ "@nestjs/schematics": "^11.1.0",
+ "@nestjs/testing": "^11.1.21",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.0",
@@ -70,16 +71,15 @@
}
},
"node_modules/@angular-devkit/core": {
- "version": "19.2.15",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.15.tgz",
- "integrity": "sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz",
+ "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "ajv": "8.17.1",
+ "ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
- "picomatch": "4.0.2",
+ "picomatch": "4.0.4",
"rxjs": "7.8.1",
"source-map": "0.7.4"
},
@@ -98,11 +98,10 @@
}
},
"node_modules/@angular-devkit/core/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -118,27 +117,24 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@angular-devkit/core/node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@angular-devkit/schematics": {
- "version": "19.2.15",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.15.tgz",
- "integrity": "sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz",
+ "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "19.2.15",
+ "@angular-devkit/core": "19.2.24",
"jsonc-parser": "3.3.1",
"magic-string": "0.30.17",
"ora": "5.4.1",
@@ -151,14 +147,13 @@
}
},
"node_modules/@angular-devkit/schematics-cli": {
- "version": "19.2.19",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz",
- "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.24.tgz",
+ "integrity": "sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "19.2.19",
- "@angular-devkit/schematics": "19.2.19",
+ "@angular-devkit/core": "19.2.24",
+ "@angular-devkit/schematics": "19.2.24",
"@inquirer/prompts": "7.3.2",
"ansi-colors": "4.1.3",
"symbol-observable": "4.0.0",
@@ -173,59 +168,11 @@
"yarn": ">= 1.13.0"
}
},
- "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": {
- "version": "19.2.19",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz",
- "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "8.17.1",
- "ajv-formats": "3.0.1",
- "jsonc-parser": "3.3.1",
- "picomatch": "4.0.2",
- "rxjs": "7.8.1",
- "source-map": "0.7.4"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "peerDependencies": {
- "chokidar": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "chokidar": {
- "optional": true
- }
- }
- },
- "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": {
- "version": "19.2.19",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz",
- "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "19.2.19",
- "jsonc-parser": "3.3.1",
- "magic-string": "0.30.17",
- "ora": "5.4.1",
- "rxjs": "7.8.1"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
"node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz",
"integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@inquirer/checkbox": "^4.1.2",
"@inquirer/confirm": "^5.1.6",
@@ -250,46 +197,11 @@
}
}
},
- "node_modules/@angular-devkit/schematics-cli/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@angular-devkit/schematics-cli/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
"node_modules/@angular-devkit/schematics/node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -811,10 +723,9 @@
"license": "MIT"
},
"node_modules/@borewit/text-codec": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz",
- "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==",
- "license": "MIT",
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
+ "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
@@ -918,17 +829,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -978,17 +878,6 @@
"node": ">=10.10.0"
}
},
- "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -2201,17 +2090,6 @@
}
}
},
- "node_modules/@jest/reporters/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@jest/reporters/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -2429,15 +2307,14 @@
"license": "MIT"
},
"node_modules/@nestjs/cli": {
- "version": "11.0.16",
- "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz",
- "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==",
+ "version": "11.0.21",
+ "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.21.tgz",
+ "integrity": "sha512-F8mV0Sj/zVEouzR3NxBuJy08YHTUOmC5Xdcx3qIIaJWzrm8Vw86CHkhkaPBJ5ewRMHPDCShPmhsfwhpCcjts3A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "19.2.19",
- "@angular-devkit/schematics": "19.2.19",
- "@angular-devkit/schematics-cli": "19.2.19",
+ "@angular-devkit/core": "19.2.24",
+ "@angular-devkit/schematics": "19.2.24",
+ "@angular-devkit/schematics-cli": "19.2.24",
"@inquirer/prompts": "7.10.1",
"@nestjs/schematics": "^11.0.1",
"ansis": "4.2.0",
@@ -2445,13 +2322,13 @@
"cli-table3": "0.6.5",
"commander": "4.1.1",
"fork-ts-checker-webpack-plugin": "9.1.0",
- "glob": "13.0.0",
+ "glob": "13.0.6",
"node-emoji": "1.11.0",
"ora": "5.4.1",
"tsconfig-paths": "4.2.0",
"tsconfig-paths-webpack-plugin": "4.2.0",
"typescript": "5.9.3",
- "webpack": "5.104.1",
+ "webpack": "5.106.0",
"webpack-node-externals": "3.0.0"
},
"bin": {
@@ -2461,7 +2338,7 @@
"node": ">= 20.11"
},
"peerDependencies": {
- "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0",
+ "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0",
"@swc/core": "^1.3.62"
},
"peerDependenciesMeta": {
@@ -2473,96 +2350,14 @@
}
}
},
- "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": {
- "version": "19.2.19",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz",
- "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "8.17.1",
- "ajv-formats": "3.0.1",
- "jsonc-parser": "3.3.1",
- "picomatch": "4.0.2",
- "rxjs": "7.8.1",
- "source-map": "0.7.4"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "peerDependencies": {
- "chokidar": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "chokidar": {
- "optional": true
- }
- }
- },
- "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": {
- "version": "19.2.19",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz",
- "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "19.2.19",
- "jsonc-parser": "3.3.1",
- "magic-string": "0.30.17",
- "ora": "5.4.1",
- "rxjs": "7.8.1"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@nestjs/cli/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@nestjs/cli/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@nestjs/cli/node_modules/rxjs": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
"node_modules/@nestjs/common": {
- "version": "11.1.6",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz",
- "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==",
- "license": "MIT",
+ "version": "11.1.21",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz",
+ "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==",
"dependencies": {
- "file-type": "21.0.0",
+ "file-type": "21.3.4",
"iterare": "1.2.1",
- "load-esm": "1.0.2",
+ "load-esm": "1.0.3",
"tslib": "2.8.1",
"uid": "2.0.2"
},
@@ -2586,14 +2381,13 @@
}
},
"node_modules/@nestjs/config": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz",
- "integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==",
- "license": "MIT",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz",
+ "integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==",
"dependencies": {
- "dotenv": "17.2.3",
+ "dotenv": "17.4.1",
"dotenv-expand": "12.0.3",
- "lodash": "4.17.23"
+ "lodash": "4.18.1"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
@@ -2601,10 +2395,9 @@
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
- "version": "17.2.3",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
- "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
- "license": "BSD-2-Clause",
+ "version": "17.4.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
+ "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
"engines": {
"node": ">=12"
},
@@ -2613,16 +2406,15 @@
}
},
"node_modules/@nestjs/core": {
- "version": "11.1.6",
- "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz",
- "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==",
+ "version": "11.1.21",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.21.tgz",
+ "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==",
"hasInstallScript": true,
- "license": "MIT",
"dependencies": {
"@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
- "path-to-regexp": "8.2.0",
+ "path-to-regexp": "8.4.2",
"tslib": "2.8.1",
"uid": "2.0.2"
},
@@ -2670,7 +2462,6 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz",
"integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==",
- "license": "MIT",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
@@ -2697,15 +2488,14 @@
}
},
"node_modules/@nestjs/platform-express": {
- "version": "11.1.17",
- "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz",
- "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==",
- "license": "MIT",
+ "version": "11.1.21",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.21.tgz",
+ "integrity": "sha512-lA3ViycOnz4Df3EstIKpuAVFhqxQixTnjAVk0M+LRyNBlGM6VSCaNJaAIrb9Pcry39T4hTHpNVbRqGLSvhL8gA==",
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.1.1",
- "path-to-regexp": "8.3.0",
+ "path-to-regexp": "8.4.2",
"tslib": "2.8.1"
},
"funding": {
@@ -2717,45 +2507,39 @@
"@nestjs/core": "^11.0.0"
}
},
- "node_modules/@nestjs/platform-express/node_modules/path-to-regexp": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/@nestjs/schematics": {
- "version": "11.0.7",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.7.tgz",
- "integrity": "sha512-t8dNYYMwEeEsrlwc2jbkfwCfXczq4AeNEgx1KVQuJ6wYibXk0ZbXbPdfp8scnEAaQv1grpncNV5gWgzi7ZwbvQ==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
+ "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "19.2.15",
- "@angular-devkit/schematics": "19.2.15",
- "comment-json": "4.2.5",
+ "@angular-devkit/core": "19.2.24",
+ "@angular-devkit/schematics": "19.2.24",
+ "comment-json": "5.0.0",
"jsonc-parser": "3.3.1",
"pluralize": "8.0.0"
},
"peerDependencies": {
+ "prettier": "^3.0.0",
"typescript": ">=4.8.2"
+ },
+ "peerDependenciesMeta": {
+ "prettier": {
+ "optional": true
+ }
}
},
"node_modules/@nestjs/swagger": {
- "version": "11.2.6",
- "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz",
- "integrity": "sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==",
- "license": "MIT",
+ "version": "11.4.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.3.tgz",
+ "integrity": "sha512-LR4BuOj+iBFzhGRnNP0OHjmrPXliDEjrmniXtLsfLDIELjkuUXYCTGjZMqgDdOY+QSabeF59LndaDzOOe+vMmw==",
"dependencies": {
"@microsoft/tsdoc": "0.16.0",
- "@nestjs/mapped-types": "2.1.0",
+ "@nestjs/mapped-types": "2.1.1",
"js-yaml": "4.1.1",
- "lodash": "4.17.23",
- "path-to-regexp": "8.3.0",
- "swagger-ui-dist": "5.31.0"
+ "lodash": "4.18.1",
+ "path-to-regexp": "8.4.2",
+ "swagger-ui-dist": "5.32.6"
},
"peerDependencies": {
"@fastify/static": "^8.0.0 || ^9.0.0",
@@ -2777,22 +2561,11 @@
}
}
},
- "node_modules/@nestjs/swagger/node_modules/path-to-regexp": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/@nestjs/testing": {
- "version": "11.1.6",
- "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz",
- "integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==",
+ "version": "11.1.21",
+ "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.21.tgz",
+ "integrity": "sha512-RhzaUFxr6/bpXWjKIzr7p2eHKMFMLwPgsxJNFcCf2CkkT3UEjW+KRGb7E2JY+fh+ck3zAdvQJrzATDnSsVlFZw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
@@ -2975,14 +2748,12 @@
"license": "MIT"
},
"node_modules/@tokenizer/inflate": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
- "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==",
- "license": "MIT",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
+ "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"dependencies": {
- "debug": "^4.4.0",
- "fflate": "^0.8.2",
- "token-types": "^6.0.0"
+ "debug": "^4.4.3",
+ "token-types": "^6.1.1"
},
"engines": {
"node": ">=18"
@@ -2995,8 +2766,7 @@
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
- "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
- "license": "MIT"
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
@@ -3873,11 +3643,10 @@
}
},
"node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"devOptional": true,
- "license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -3922,11 +3691,10 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -3943,7 +3711,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
@@ -3957,11 +3724,10 @@
}
},
"node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3977,8 +3743,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
@@ -3995,7 +3760,6 @@
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -4109,8 +3873,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
"integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/asap": {
"version": "2.0.6",
@@ -4381,10 +4144,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
- "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
- "license": "MIT",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -4857,17 +4619,13 @@
}
},
"node_modules/comment-json": {
- "version": "4.2.5",
- "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
- "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz",
+ "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"array-timsort": "^1.0.3",
- "core-util-is": "^1.0.3",
- "esprima": "^4.0.1",
- "has-own-prop": "^2.0.0",
- "repeat-string": "^1.6.1"
+ "esprima": "^4.0.1"
},
"engines": {
"node": ">= 6"
@@ -4883,13 +4641,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -4981,7 +4732,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/cors": {
@@ -5335,14 +5085,13 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.18.3",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
- "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "version": "5.21.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz",
+ "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
+ "tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
@@ -5570,17 +5319,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/eslint/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5890,9 +5628,9 @@
"license": "Unlicense"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
@@ -5903,8 +5641,7 @@
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
- ],
- "license": "BSD-3-Clause"
+ ]
},
"node_modules/fastq": {
"version": "1.19.1",
@@ -5926,12 +5663,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fflate": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
- "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
- "license": "MIT"
- },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5946,14 +5677,13 @@
}
},
"node_modules/file-type": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
- "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
- "license": "MIT",
+ "version": "21.3.4",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
+ "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
"dependencies": {
- "@tokenizer/inflate": "^0.2.7",
- "strtok3": "^10.2.2",
- "token-types": "^6.0.0",
+ "@tokenizer/inflate": "^0.4.1",
+ "strtok3": "^10.3.4",
+ "token-types": "^6.1.1",
"uint8array-extras": "^1.4.0"
},
"engines": {
@@ -6091,17 +5821,6 @@
"webpack": "^5.11.0"
}
},
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -6324,18 +6043,17 @@
}
},
"node_modules/glob": {
- "version": "13.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
- "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
- "license": "BlueOak-1.0.0",
"dependencies": {
- "minimatch": "^10.1.1",
- "minipass": "^7.1.2",
- "path-scurry": "^2.0.0"
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
},
"engines": {
- "node": "20 || >=22"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -6358,40 +6076,15 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
- "dev": true,
- "license": "BSD-2-Clause"
- },
- "node_modules/glob/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- }
+ "dev": true
},
"node_modules/glob/node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
- "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^5.0.2"
+ "brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -6497,16 +6190,6 @@
"node": ">=8"
}
},
- "node_modules/has-own-prop": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
- "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -6646,6 +6329,12 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -7139,17 +6828,6 @@
}
}
},
- "node_modules/jest-config/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/jest-config/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -7502,17 +7180,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-runtime/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/jest-runtime/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -7598,19 +7265,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/jest-util/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/jest-validate": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
@@ -7771,8 +7425,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/jsonfile": {
"version": "6.2.0",
@@ -7809,6 +7462,54 @@
"npm": ">=6"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
@@ -7880,6 +7581,15 @@
"integrity": "sha512-bsxi8FoceAYR/bjHcLYc2ShJ/aVAzo5jaxAYiMHF0BD+NTp47405CGuPNKYpw+lHadN9k/ClFGc9X5vaZswIrA==",
"license": "MIT"
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -7888,9 +7598,9 @@
"license": "MIT"
},
"node_modules/load-esm": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz",
- "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz",
+ "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==",
"funding": [
{
"type": "github",
@@ -7901,7 +7611,6 @@
"url": "https://buymeacoffee.com/borewit"
}
],
- "license": "MIT",
"engines": {
"node": ">=13.2.0"
}
@@ -7937,10 +7646,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "license": "MIT"
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
@@ -8030,7 +7738,6 @@
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
@@ -8235,10 +7942,9 @@
}
},
"node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -8406,9 +8112,9 @@
}
},
"node_modules/npm": {
- "version": "11.11.1",
- "resolved": "https://registry.npmjs.org/npm/-/npm-11.11.1.tgz",
- "integrity": "sha512-asazCodkFdz1ReQzukyzS/DD77uGCIqUFeRG3gtaT8b9UR0ne1m9QOBuMgT72ij1rt7TRrOox4A1WzntMWIuEg==",
+ "version": "11.14.1",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-11.14.1.tgz",
+ "integrity": "sha512-aopNZ0eEl6LbxoFcrXLmTEPzNBNxfiQnVgR9RmJBqzm+5h5pFoOmRljpRJbsXxocBeSl7GLcx3MoDf2UlEOjZw==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -8476,18 +8182,10 @@
"validate-npm-package-name",
"which"
],
- "license": "Artistic-2.0",
- "workspaces": [
- "docs",
- "smoke-tests",
- "mock-globals",
- "mock-registry",
- "workspaces/*"
- ],
"dependencies": {
"@isaacs/string-locale-compare": "^1.1.0",
- "@npmcli/arborist": "^9.4.1",
- "@npmcli/config": "^10.7.1",
+ "@npmcli/arborist": "^9.5.0",
+ "@npmcli/config": "^10.9.0",
"@npmcli/fs": "^5.0.0",
"@npmcli/map-workspaces": "^5.0.3",
"@npmcli/metavuln-calculator": "^9.0.3",
@@ -8495,10 +8193,10 @@
"@npmcli/promise-spawn": "^9.0.1",
"@npmcli/redact": "^4.0.0",
"@npmcli/run-script": "^10.0.4",
- "@sigstore/tuf": "^4.0.1",
+ "@sigstore/tuf": "^4.0.2",
"abbrev": "^4.0.0",
"archy": "~1.0.0",
- "cacache": "^20.0.3",
+ "cacache": "^20.0.4",
"chalk": "^5.6.2",
"ci-info": "^4.4.0",
"fastest-levenshtein": "^1.0.16",
@@ -8508,24 +8206,24 @@
"hosted-git-info": "^9.0.2",
"ini": "^6.0.0",
"init-package-json": "^8.2.5",
- "is-cidr": "^6.0.3",
+ "is-cidr": "^6.0.4",
"json-parse-even-better-errors": "^5.0.0",
"libnpmaccess": "^10.0.3",
- "libnpmdiff": "^8.1.4",
- "libnpmexec": "^10.2.4",
- "libnpmfund": "^7.0.18",
+ "libnpmdiff": "^8.1.7",
+ "libnpmexec": "^10.2.7",
+ "libnpmfund": "^7.0.21",
"libnpmorg": "^8.0.1",
- "libnpmpack": "^9.1.4",
+ "libnpmpack": "^9.1.7",
"libnpmpublish": "^11.1.3",
"libnpmsearch": "^9.0.1",
"libnpmteam": "^8.0.2",
"libnpmversion": "^8.0.3",
- "make-fetch-happen": "^15.0.4",
- "minimatch": "^10.2.4",
+ "make-fetch-happen": "^15.0.5",
+ "minimatch": "^10.2.5",
"minipass": "^7.1.3",
"minipass-pipeline": "^1.2.4",
"ms": "^2.1.2",
- "node-gyp": "^12.2.0",
+ "node-gyp": "^12.3.0",
"nopt": "^9.0.0",
"npm-audit-report": "^7.0.0",
"npm-install-checks": "^8.0.0",
@@ -8544,7 +8242,7 @@
"spdx-expression-parse": "^4.0.0",
"ssri": "^13.0.1",
"supports-color": "^10.2.2",
- "tar": "^7.5.11",
+ "tar": "^7.5.13",
"text-table": "~0.2.0",
"tiny-relative-date": "^2.0.2",
"treeverse": "^3.0.0",
@@ -8573,24 +8271,13 @@
}
},
"node_modules/npm/node_modules/@gar/promise-retry": {
- "version": "1.0.2",
+ "version": "1.0.3",
"inBundle": true,
"license": "MIT",
- "dependencies": {
- "retry": "^0.13.1"
- },
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/npm/node_modules/@gar/promise-retry/node_modules/retry": {
- "version": "0.13.1",
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
"node_modules/npm/node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"inBundle": true,
@@ -8623,7 +8310,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/arborist": {
- "version": "9.4.1",
+ "version": "9.5.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -8670,7 +8357,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/config": {
- "version": "10.7.1",
+ "version": "10.9.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -8850,7 +8537,7 @@
}
},
"node_modules/npm/node_modules/@sigstore/core": {
- "version": "3.1.0",
+ "version": "3.2.0",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
@@ -8858,7 +8545,7 @@
}
},
"node_modules/npm/node_modules/@sigstore/protobuf-specs": {
- "version": "0.5.0",
+ "version": "0.5.1",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
@@ -8866,23 +8553,23 @@
}
},
"node_modules/npm/node_modules/@sigstore/sign": {
- "version": "4.1.0",
+ "version": "4.1.1",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
+ "@gar/promise-retry": "^1.0.2",
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.1.0",
+ "@sigstore/core": "^3.2.0",
"@sigstore/protobuf-specs": "^0.5.0",
- "make-fetch-happen": "^15.0.3",
- "proc-log": "^6.1.0",
- "promise-retry": "^2.0.1"
+ "make-fetch-happen": "^15.0.4",
+ "proc-log": "^6.1.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/npm/node_modules/@sigstore/tuf": {
- "version": "4.0.1",
+ "version": "4.0.2",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
@@ -8987,7 +8674,7 @@
}
},
"node_modules/npm/node_modules/brace-expansion": {
- "version": "5.0.4",
+ "version": "5.0.5",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -8998,7 +8685,7 @@
}
},
"node_modules/npm/node_modules/cacache": {
- "version": "20.0.3",
+ "version": "20.0.4",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -9011,8 +8698,7 @@
"minipass-flush": "^1.0.5",
"minipass-pipeline": "^1.2.4",
"p-map": "^7.0.2",
- "ssri": "^13.0.0",
- "unique-filename": "^5.0.0"
+ "ssri": "^13.0.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -9052,7 +8738,7 @@
}
},
"node_modules/npm/node_modules/cidr-regex": {
- "version": "5.0.3",
+ "version": "5.0.5",
"inBundle": true,
"license": "BSD-2-Clause",
"engines": {
@@ -9103,7 +8789,7 @@
}
},
"node_modules/npm/node_modules/diff": {
- "version": "8.0.3",
+ "version": "8.0.4",
"inBundle": true,
"license": "BSD-3-Clause",
"engines": {
@@ -9120,7 +8806,6 @@
},
"node_modules/npm/node_modules/err-code": {
"version": "2.0.3",
- "inBundle": true,
"license": "MIT"
},
"node_modules/npm/node_modules/exponential-backoff": {
@@ -9237,7 +8922,6 @@
},
"node_modules/npm/node_modules/imurmurhash": {
"version": "0.1.4",
- "inBundle": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
@@ -9268,7 +8952,7 @@
}
},
"node_modules/npm/node_modules/ip-address": {
- "version": "10.1.0",
+ "version": "10.1.1",
"inBundle": true,
"license": "MIT",
"engines": {
@@ -9276,11 +8960,11 @@
}
},
"node_modules/npm/node_modules/is-cidr": {
- "version": "6.0.3",
+ "version": "6.0.4",
"inBundle": true,
"license": "BSD-2-Clause",
"dependencies": {
- "cidr-regex": "^5.0.1"
+ "cidr-regex": "^5.0.4"
},
"engines": {
"node": ">=20"
@@ -9341,11 +9025,11 @@
}
},
"node_modules/npm/node_modules/libnpmdiff": {
- "version": "8.1.4",
+ "version": "8.1.7",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.4.1",
+ "@npmcli/arborist": "^9.5.0",
"@npmcli/installed-package-contents": "^4.0.0",
"binary-extensions": "^3.0.0",
"diff": "^8.0.2",
@@ -9359,12 +9043,12 @@
}
},
"node_modules/npm/node_modules/libnpmexec": {
- "version": "10.2.4",
+ "version": "10.2.7",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@gar/promise-retry": "^1.0.0",
- "@npmcli/arborist": "^9.4.1",
+ "@npmcli/arborist": "^9.5.0",
"@npmcli/package-json": "^7.0.0",
"@npmcli/run-script": "^10.0.0",
"ci-info": "^4.0.0",
@@ -9381,11 +9065,11 @@
}
},
"node_modules/npm/node_modules/libnpmfund": {
- "version": "7.0.18",
+ "version": "7.0.21",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.4.1"
+ "@npmcli/arborist": "^9.5.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -9404,11 +9088,11 @@
}
},
"node_modules/npm/node_modules/libnpmpack": {
- "version": "9.1.4",
+ "version": "9.1.7",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.4.1",
+ "@npmcli/arborist": "^9.5.0",
"@npmcli/run-script": "^10.0.0",
"npm-package-arg": "^13.0.0",
"pacote": "^21.0.2"
@@ -9474,7 +9158,7 @@
}
},
"node_modules/npm/node_modules/lru-cache": {
- "version": "11.2.6",
+ "version": "11.3.5",
"inBundle": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -9482,12 +9166,13 @@
}
},
"node_modules/npm/node_modules/make-fetch-happen": {
- "version": "15.0.4",
+ "version": "15.0.5",
"inBundle": true,
"license": "ISC",
"dependencies": {
"@gar/promise-retry": "^1.0.0",
"@npmcli/agent": "^4.0.0",
+ "@npmcli/redact": "^4.0.0",
"cacache": "^20.0.1",
"http-cache-semantics": "^4.1.1",
"minipass": "^7.0.2",
@@ -9503,11 +9188,11 @@
}
},
"node_modules/npm/node_modules/minimatch": {
- "version": "10.2.4",
+ "version": "10.2.5",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^5.0.2"
+ "brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -9552,32 +9237,16 @@
}
},
"node_modules/npm/node_modules/minipass-flush": {
- "version": "1.0.5",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": {
- "version": "3.3.6",
+ "version": "1.0.6",
"inBundle": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "yallist": "^4.0.0"
+ "minipass": "^7.1.3"
},
"engines": {
- "node": ">=8"
+ "node": ">=16 || 14 >=14.17"
}
},
- "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": {
- "version": "4.0.0",
- "inBundle": true,
- "license": "ISC"
- },
"node_modules/npm/node_modules/minipass-pipeline": {
"version": "1.2.4",
"inBundle": true,
@@ -9649,19 +9318,19 @@
}
},
"node_modules/npm/node_modules/node-gyp": {
- "version": "12.2.0",
+ "version": "12.3.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"env-paths": "^2.2.0",
"exponential-backoff": "^3.1.1",
"graceful-fs": "^4.2.6",
- "make-fetch-happen": "^15.0.0",
"nopt": "^9.0.0",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"tar": "^7.5.4",
"tinyglobby": "^0.2.12",
+ "undici": "^6.25.0",
"which": "^6.0.0"
},
"bin": {
@@ -9916,7 +9585,6 @@
},
"node_modules/npm/node_modules/promise-retry": {
"version": "2.0.1",
- "inBundle": true,
"license": "MIT",
"dependencies": {
"err-code": "^2.0.2",
@@ -9965,7 +9633,6 @@
},
"node_modules/npm/node_modules/retry": {
"version": "0.12.0",
- "inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 4"
@@ -10025,11 +9692,11 @@
}
},
"node_modules/npm/node_modules/socks": {
- "version": "2.8.7",
+ "version": "2.8.8",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "ip-address": "^10.0.1",
+ "ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
@@ -10092,7 +9759,7 @@
}
},
"node_modules/npm/node_modules/tar": {
- "version": "7.5.11",
+ "version": "7.5.13",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -10117,12 +9784,12 @@
"license": "MIT"
},
"node_modules/npm/node_modules/tinyglobby": {
- "version": "0.2.15",
+ "version": "0.2.16",
"inBundle": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -10148,7 +9815,7 @@
}
},
"node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
+ "version": "4.0.4",
"inBundle": true,
"license": "MIT",
"engines": {
@@ -10179,9 +9846,16 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/npm/node_modules/undici": {
+ "version": "6.25.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/npm/node_modules/unique-filename": {
"version": "5.0.0",
- "inBundle": true,
"license": "ISC",
"dependencies": {
"unique-slug": "^6.0.0"
@@ -10192,7 +9866,6 @@
},
"node_modules/npm/node_modules/unique-slug": {
"version": "6.0.0",
- "inBundle": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4"
@@ -10409,6 +10082,12 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -10577,39 +10256,37 @@
"license": "MIT"
},
"node_modules/path-scurry": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
- "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
- "license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
- "node": "20 || >=22"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
- "version": "11.2.4",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
- "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
"dev": true,
- "license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/path-to-regexp": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
- "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
- "license": "MIT",
- "engines": {
- "node": ">=16"
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/path-type": {
@@ -10724,11 +10401,10 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -10946,6 +10622,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -11117,16 +10799,6 @@
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
- "node_modules/repeat-string": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
- "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -11280,17 +10952,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -11486,6 +11147,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11900,10 +11567,9 @@
}
},
"node_modules/strtok3": {
- "version": "10.3.4",
- "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
- "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==",
- "license": "MIT",
+ "version": "10.3.5",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
+ "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
"dependencies": {
"@tokenizer/token": "^0.3.0"
},
@@ -11986,24 +11652,10 @@
"uuid": "^10.0.0"
}
},
- "node_modules/svix/node_modules/uuid": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
- "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/swagger-ui-dist": {
- "version": "5.31.0",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
- "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==",
- "license": "Apache-2.0",
+ "version": "5.32.6",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz",
+ "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
@@ -12028,7 +11680,6 @@
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
"integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.10"
}
@@ -12050,11 +11701,10 @@
}
},
"node_modules/tapable": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
- "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
},
@@ -12244,17 +11894,6 @@
"node": ">=8"
}
},
- "node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/test-exclude/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -12341,12 +11980,11 @@
}
},
"node_modules/token-types": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz",
- "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==",
- "license": "MIT",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
+ "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
"dependencies": {
- "@borewit/text-codec": "^0.1.0",
+ "@borewit/text-codec": "^0.2.1",
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
@@ -12861,7 +12499,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
- "license": "MIT",
"engines": {
"node": ">=18"
},
@@ -12951,14 +12588,13 @@
}
},
"node_modules/uuid": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
- "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
+ "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
- "license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
@@ -13014,11 +12650,10 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
- "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -13038,11 +12673,10 @@
}
},
"node_modules/webpack": {
- "version": "5.104.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
- "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
+ "version": "5.106.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz",
+ "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -13050,11 +12684,11 @@
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.15.0",
+ "acorn": "^8.16.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.4",
+ "enhanced-resolve": "^5.20.0",
"es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -13066,9 +12700,9 @@
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
- "terser-webpack-plugin": "^5.3.16",
- "watchpack": "^2.4.4",
- "webpack-sources": "^3.3.3"
+ "terser-webpack-plugin": "^5.3.17",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.3.4"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -13097,21 +12731,19 @@
}
},
"node_modules/webpack-sources": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
- "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz",
+ "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
diff --git a/backend/package.json b/backend/package.json
index 13e4f01..1357706 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -19,15 +19,18 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"seed": "ts-node -r tsconfig-paths/register src/v1/seed/seed.ts",
- "seed:build": "npm run build && node dist/v1/seed/seed.js"
+ "seed:build": "npm run build && node dist/v1/seed/seed.js",
+ "migration:run": "ts-node -r tsconfig-paths/register node_modules/.bin/typeorm migration:run -d src/v1/dataSource.ts",
+ "migration:revert": "ts-node -r tsconfig-paths/register node_modules/.bin/typeorm migration:revert -d src/v1/dataSource.ts",
+ "migration:generate": "ts-node -r tsconfig-paths/register node_modules/.bin/typeorm migration:generate -d src/v1/dataSource.ts"
},
"dependencies": {
- "@nestjs/common": "^11.1.3",
+ "@nestjs/common": "^11.1.21",
"@nestjs/config": "^4.0.3",
- "@nestjs/core": "^11.1.3",
+ "@nestjs/core": "^11.1.21",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
- "@nestjs/platform-express": "^11.1.17",
+ "@nestjs/platform-express": "^11.1.21",
"@nestjs/swagger": "^11.2.6",
"@nestjs/typeorm": "^11.0.0",
"@types/multer": "^2.0.0",
@@ -39,8 +42,9 @@
"cookie-parser": "^1.4.7",
"express-basic-auth": "^1.2.1",
"install": "^0.13.0",
+ "jszip": "^3.10.1",
"multer": "^2.1.1",
- "npm": "^11.11.1",
+ "npm": "^11.14.1",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
@@ -55,9 +59,9 @@
"typeorm": "^0.3.25"
},
"devDependencies": {
- "@nestjs/cli": "^11.0.16",
- "@nestjs/schematics": "^11.0.5",
- "@nestjs/testing": "^11.1.3",
+ "@nestjs/cli": "^11.0.21",
+ "@nestjs/schematics": "^11.1.0",
+ "@nestjs/testing": "^11.1.21",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.0",
@@ -99,6 +103,17 @@
"testEnvironment": "node"
},
"overrides": {
- "@nestjs/mapped-types": "2.0.6"
+ "@nestjs/mapped-types": "2.0.6",
+ "fast-uri": "^3.1.2",
+ "path-to-regexp": "^8.4.0",
+ "picomatch": "^4.0.4",
+ "anymatch": { "picomatch": "^4.0.4" },
+ "micromatch": { "picomatch": "^4.0.4" },
+ "webpack": { "ajv": "^8.18.0" },
+ "file-type": "^21.3.4",
+ "uuid": "^11.1.0",
+ "lodash": "4.17.21",
+ "brace-expansion": "^2.0.4",
+ "ip-address": "^9.0.5"
}
}
diff --git a/backend/src/v1/activity/activity.controller.ts b/backend/src/v1/activity/activity.controller.ts
index b08e4b6..4805109 100644
--- a/backend/src/v1/activity/activity.controller.ts
+++ b/backend/src/v1/activity/activity.controller.ts
@@ -21,12 +21,13 @@ import {
Delete,
Param,
Body,
+ Query,
Req,
UseGuards,
UnauthorizedException,
ParseIntPipe,
} from '@nestjs/common';
-import { ActivityService } from './activity.service';
+import { ActivityService, ActivityFilter } from './activity.service';
import { CreateActivityDto } from './dto/createActivity.dto';
import { UpdateActivityDto } from './dto/updateActivity.dto';
import {
@@ -48,15 +49,16 @@ export class ActivityController {
constructor(private readonly activityService: ActivityService) {}
@Get()
- @ApiOperation({ summary: 'Get all activities for the logged-in user' })
+ @ApiOperation({ summary: 'Get activities (filter: all | global | mine)' })
@ApiOkResponse({ type: [ActivityResponseDto] })
getAllActivities(
@Req() req: RequestWithUser,
+ @Query('filter') filter?: ActivityFilter,
): Promise {
if (!req.user?.id) {
throw new UnauthorizedException('User not authenticated');
}
- return this.activityService.findAll(+req.user.id);
+ return this.activityService.findAll(+req.user.id, filter ?? 'all');
}
@Get(':id')
@@ -111,4 +113,18 @@ export class ActivityController {
}
return this.activityService.delete(id, +req.user.id);
}
+
+ @Post(':id/duplicate')
+ @ApiOperation({ summary: 'Duplicate a global activity for personal use' })
+ @ApiCreatedResponse({ type: ActivityResponseDto })
+ duplicateActivity(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() body: { transferStats?: boolean },
+ @Req() req: RequestWithUser,
+ ): Promise {
+ if (!req.user?.id) {
+ throw new UnauthorizedException('User not authenticated');
+ }
+ return this.activityService.duplicateGlobalActivity(id, +req.user.id, body.transferStats ?? false);
+ }
}
diff --git a/backend/src/v1/activity/activity.entity.ts b/backend/src/v1/activity/activity.entity.ts
index bb58d6d..e11266a 100644
--- a/backend/src/v1/activity/activity.entity.ts
+++ b/backend/src/v1/activity/activity.entity.ts
@@ -21,9 +21,9 @@ import {
UpdateDateColumn,
DeleteDateColumn,
ManyToOne,
- Unique,
} from 'typeorm';
import { User } from '../user/user.entity';
+import { I18nString } from '../common/types/i18n.types';
export enum ActivityIcon {
RUNNING = 'run',
@@ -49,17 +49,28 @@ export enum ActivityIcon {
OTHER = 'dots-horizontal',
}
+// Partial unique indexes (not decorator) handle uniqueness:
+// - Global activities: UNIQUE (name) WHERE createdById IS NULL
+// - User activities: UNIQUE (name, createdById) WHERE createdById IS NOT NULL
@Entity()
-@Unique(['name', 'createdBy'])
export class Activity {
@PrimaryGeneratedColumn()
id: number;
- @Column()
- name: string;
+ @Column({ type: 'jsonb' })
+ title: I18nString;
+
+ @Column({ name: 'descriptionI18n', type: 'jsonb', nullable: true })
+ descriptionI18n?: I18nString;
+
+ @Column({ default: false })
+ isGlobal: boolean;
@Column({ nullable: true })
- description?: string;
+ personalizedFromGlobalId?: number;
+
+ @Column({ type: 'timestamptz', nullable: true })
+ personalizedAt?: Date;
@Column({
type: 'enum',
@@ -68,13 +79,12 @@ export class Activity {
})
icon: ActivityIcon;
- @ManyToOne(() => User, { onDelete: 'CASCADE' })
- createdBy: User;
+ @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
+ createdBy: User | null;
@Column({ type: 'simple-array', nullable: true })
equipment?: string[];
- // Configuration flags for which fields to track
@Column({ default: false })
trackDistance: boolean;
diff --git a/backend/src/v1/activity/activity.module.ts b/backend/src/v1/activity/activity.module.ts
index 49b9c2c..92f558e 100644
--- a/backend/src/v1/activity/activity.module.ts
+++ b/backend/src/v1/activity/activity.module.ts
@@ -17,12 +17,13 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Activity } from './activity.entity';
import { ActivityService } from './activity.service';
+import { ActivitySeedService } from './activitySeed.service';
import { ActivityController } from './activity.controller';
@Module({
imports: [TypeOrmModule.forFeature([Activity])],
controllers: [ActivityController],
- providers: [ActivityService],
- exports: [ActivityService],
+ providers: [ActivityService, ActivitySeedService],
+ exports: [ActivityService, ActivitySeedService],
})
export class ActivityModule {}
diff --git a/backend/src/v1/activity/activity.service.ts b/backend/src/v1/activity/activity.service.ts
index 7c0b663..f6a8ef4 100644
--- a/backend/src/v1/activity/activity.service.ts
+++ b/backend/src/v1/activity/activity.service.ts
@@ -16,27 +16,33 @@
import {
Injectable,
NotFoundException,
- ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { DataSource, Repository } from 'typeorm';
import { Activity } from './activity.entity';
import { CreateActivityDto } from './dto/createActivity.dto';
import { UpdateActivityDto } from './dto/updateActivity.dto';
import { ActivityResponseDto } from './dto/activityResponse.dto';
+import { CreateGlobalActivityDto, UpdateGlobalActivityDto } from './dto/createGlobalActivity.dto';
+
+export type ActivityFilter = 'all' | 'global' | 'mine';
@Injectable()
export class ActivityService {
constructor(
@InjectRepository(Activity)
private readonly activityRepo: Repository,
+ private readonly dataSource: DataSource,
) {}
private toResponseDto(activity: Activity): ActivityResponseDto {
return {
id: activity.id,
- name: activity.name,
- description: activity.description,
+ title: activity.title,
+ description: activity.descriptionI18n,
+ isGlobal: activity.isGlobal,
+ personalizedFromGlobalId: activity.personalizedFromGlobalId,
+ personalizedAt: activity.personalizedAt,
icon: activity.icon,
equipment: activity.equipment,
trackDistance: activity.trackDistance,
@@ -48,87 +54,177 @@ export class ActivityService {
};
}
- async findAll(userId: number): Promise {
- const activities = await this.activityRepo.find({
- where: { createdBy: { id: userId } },
- order: { name: 'ASC' },
- });
+ async findAll(userId: number, filter: ActivityFilter = 'all'): Promise {
+ if (filter === 'mine') {
+ const activities = await this.activityRepo.find({
+ where: { createdBy: { id: userId }, isGlobal: false },
+ });
+ return activities.map((a) => this.toResponseDto(a));
+ }
+
+ if (filter === 'global') {
+ const activities = await this.activityRepo.find({
+ where: { isGlobal: true },
+ order: { id: 'ASC' },
+ });
+ return activities.map((a) => this.toResponseDto(a));
+ }
- return activities.map((a) => this.toResponseDto(a));
+ // 'all': user's own first, then globals
+ const [userActivities, globalActivities] = await Promise.all([
+ this.activityRepo.find({ where: { createdBy: { id: userId }, isGlobal: false } }),
+ this.activityRepo.find({ where: { isGlobal: true }, order: { id: 'ASC' } }),
+ ]);
+ return [...userActivities, ...globalActivities].map((a) => this.toResponseDto(a));
}
async findOne(id: number, userId: number): Promise {
const activity = await this.activityRepo.findOne({
- where: { id, createdBy: { id: userId } },
+ where: [
+ { id, createdBy: { id: userId } },
+ { id, isGlobal: true },
+ ],
});
-
- if (!activity) {
- throw new NotFoundException('Activity not found');
- }
-
+ if (!activity) throw new NotFoundException('Activity not found');
return this.toResponseDto(activity);
}
- async create(
- dto: CreateActivityDto,
- userId: number,
- ): Promise {
- // Check for duplicate name
- const existing = await this.activityRepo.findOne({
- where: { name: dto.name, createdBy: { id: userId } },
+ async create(dto: CreateActivityDto, userId: number): Promise {
+ const { name, description, ...rest } = dto;
+
+ const activity = this.activityRepo.create({
+ ...rest,
+ title: { default: name },
+ descriptionI18n: description ? { default: description } : undefined,
+ isGlobal: false,
+ createdBy: { id: userId } as any,
});
- if (existing) {
- throw new ConflictException('Activity with this name already exists');
- }
+ const saved = await this.activityRepo.save(activity);
+ return this.toResponseDto(saved);
+ }
+ async createGlobal(dto: CreateGlobalActivityDto): Promise {
const activity = this.activityRepo.create({
...dto,
- createdBy: { id: userId } as any,
+ isGlobal: true,
+ createdBy: null,
});
+ const saved = await this.activityRepo.save(activity);
+ return this.toResponseDto(saved);
+ }
+ async updateGlobal(id: number, dto: UpdateGlobalActivityDto): Promise {
+ const activity = await this.activityRepo.findOne({ where: { id, isGlobal: true } });
+ if (!activity) throw new NotFoundException('Global activity not found');
+ Object.assign(activity, dto);
const saved = await this.activityRepo.save(activity);
return this.toResponseDto(saved);
}
- async update(
- id: number,
- dto: UpdateActivityDto,
- userId: number,
- ): Promise {
- const activity = await this.activityRepo.findOne({
- where: { id, createdBy: { id: userId } },
+ async deleteGlobal(id: number): Promise<{ message: string }> {
+ const activity = await this.activityRepo.findOne({ where: { id, isGlobal: true } });
+ if (!activity) throw new NotFoundException('Global activity not found');
+
+ await this.dataSource.transaction(async (manager) => {
+ // Find all users with activity logs referencing this activity
+ const affectedUsers: { userId: number }[] = await manager.query(`
+ SELECT DISTINCT user_id AS "userId"
+ FROM activity_log
+ WHERE "activityId" = $1
+ `, [id]);
+
+ for (const { userId } of affectedUsers) {
+ const copy = manager.create(Activity, {
+ title: activity.title,
+ descriptionI18n: activity.descriptionI18n,
+ isGlobal: false,
+ createdBy: { id: userId } as any,
+ personalizedFromGlobalId: activity.id,
+ personalizedAt: new Date(),
+ icon: activity.icon,
+ equipment: activity.equipment,
+ trackDistance: activity.trackDistance,
+ trackPace: activity.trackPace,
+ trackElevation: activity.trackElevation,
+ trackCalories: activity.trackCalories,
+ });
+ const saved = await manager.save(Activity, copy);
+
+ await manager.query(
+ `UPDATE activity_log SET "activityId" = $1 WHERE "activityId" = $2 AND user_id = $3`,
+ [saved.id, id, userId],
+ );
+ }
+
+ await manager.softRemove(Activity, activity);
});
- if (!activity) {
- throw new NotFoundException('Activity not found');
- }
+ return { message: 'Global activity deleted and user data preserved' };
+ }
- // Check for duplicate name if name is being updated
- if (dto.name && dto.name !== activity.name) {
- const existing = await this.activityRepo.findOne({
- where: { name: dto.name, createdBy: { id: userId } },
- });
+ async update(id: number, dto: UpdateActivityDto, userId: number): Promise {
+ const activity = await this.activityRepo.findOne({
+ where: { id, createdBy: { id: userId }, isGlobal: false },
+ });
+ if (!activity) throw new NotFoundException('Activity not found');
- if (existing) {
- throw new ConflictException('Activity with this name already exists');
- }
+ const { name, description, ...rest } = dto as CreateActivityDto;
+ Object.assign(activity, rest);
+
+ if (name !== undefined) {
+ activity.title = { ...activity.title, default: name };
+ }
+ if (description !== undefined) {
+ activity.descriptionI18n = { ...(activity.descriptionI18n ?? {}), default: description };
}
- Object.assign(activity, dto);
const saved = await this.activityRepo.save(activity);
return this.toResponseDto(saved);
}
async delete(id: number, userId: number): Promise {
const activity = await this.activityRepo.findOne({
- where: { id, createdBy: { id: userId } },
+ where: { id, createdBy: { id: userId }, isGlobal: false },
});
+ if (!activity) throw new NotFoundException('Activity not found');
+ await this.activityRepo.softRemove(activity);
+ }
- if (!activity) {
- throw new NotFoundException('Activity not found');
- }
+ async duplicateGlobalActivity(
+ activityId: number,
+ userId: number,
+ transferStats: boolean = false,
+ ): Promise {
+ const source = await this.activityRepo.findOne({ where: { id: activityId, isGlobal: true } });
+ if (!source) throw new NotFoundException('Global activity not found');
+
+ return this.dataSource.transaction(async (manager) => {
+ const copy = manager.create(Activity, {
+ title: source.title,
+ descriptionI18n: source.descriptionI18n,
+ isGlobal: false,
+ createdBy: { id: userId } as any,
+ personalizedFromGlobalId: source.id,
+ personalizedAt: new Date(),
+ icon: source.icon,
+ equipment: source.equipment,
+ trackDistance: source.trackDistance,
+ trackPace: source.trackPace,
+ trackElevation: source.trackElevation,
+ trackCalories: source.trackCalories,
+ });
+ const saved = await manager.save(Activity, copy);
- await this.activityRepo.softRemove(activity);
+ if (transferStats) {
+ await manager.query(
+ `UPDATE activity_log SET "activityId" = $1 WHERE "activityId" = $2 AND user_id = $3`,
+ [saved.id, activityId, userId],
+ );
+ }
+
+ const full = await manager.findOne(Activity, { where: { id: saved.id } });
+ return this.toResponseDto(full!);
+ });
}
}
diff --git a/backend/src/v1/activity/activitySeed.service.ts b/backend/src/v1/activity/activitySeed.service.ts
new file mode 100644
index 0000000..b04273d
--- /dev/null
+++ b/backend/src/v1/activity/activitySeed.service.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { Activity } from './activity.entity';
+import { activitiesToSeed } from '../seed/data/activities.data';
+
+@Injectable()
+export class ActivitySeedService implements OnModuleInit {
+ private readonly logger = new Logger(ActivitySeedService.name);
+
+ constructor(
+ @InjectRepository(Activity)
+ private readonly activityRepo: Repository,
+ ) {}
+
+ /**
+ * Seeds the global activity catalog on application start if none exist yet.
+ * Global activities are shared across all users and managed via the admin panel.
+ */
+ async onModuleInit(): Promise {
+ await this.seedGlobalActivities();
+ }
+
+ async seedGlobalActivities(): Promise {
+ const existing = await this.activityRepo.count({ where: { isGlobal: true } });
+ if (existing > 0) return;
+
+ this.logger.log('No global activities found – seeding defaults…');
+
+ const activities: Activity[] = [];
+
+ for (const def of activitiesToSeed) {
+ const activity = this.activityRepo.create({
+ title: {
+ default: def.name,
+ eng: def.name,
+ swe: def.swedenName ?? def.name,
+ },
+ descriptionI18n: def.description
+ ? {
+ default: def.description,
+ eng: def.description,
+ swe: def.swedenDescription ?? def.description,
+ }
+ : undefined,
+ isGlobal: true,
+ createdBy: null,
+ icon: def.icon,
+ trackDistance: def.trackDistance,
+ trackPace: def.trackPace,
+ trackElevation: def.trackElevation,
+ trackCalories: def.trackCalories,
+ });
+
+ activities.push(activity);
+ }
+
+ await this.activityRepo.save(activities);
+ this.logger.log(`Seeded ${activities.length} global activity(s)`);
+ }
+}
diff --git a/backend/src/v1/activity/dto/activityResponse.dto.ts b/backend/src/v1/activity/dto/activityResponse.dto.ts
index ed8f48a..f62f4b1 100644
--- a/backend/src/v1/activity/dto/activityResponse.dto.ts
+++ b/backend/src/v1/activity/dto/activityResponse.dto.ts
@@ -15,16 +15,26 @@
import { ApiProperty } from '@nestjs/swagger';
import { ActivityIcon } from '../activity.entity';
+import { I18nString } from '../../common/types/i18n.types';
export class ActivityResponseDto {
@ApiProperty()
id: number;
@ApiProperty()
- name: string;
+ title: I18nString;
@ApiProperty({ required: false })
- description?: string;
+ description?: I18nString;
+
+ @ApiProperty()
+ isGlobal: boolean;
+
+ @ApiProperty({ required: false })
+ personalizedFromGlobalId?: number;
+
+ @ApiProperty({ required: false })
+ personalizedAt?: Date;
@ApiProperty({ enum: ActivityIcon })
icon: ActivityIcon;
diff --git a/backend/src/v1/activity/dto/createGlobalActivity.dto.ts b/backend/src/v1/activity/dto/createGlobalActivity.dto.ts
new file mode 100644
index 0000000..2ac54d8
--- /dev/null
+++ b/backend/src/v1/activity/dto/createGlobalActivity.dto.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import {
+ IsOptional,
+ IsArray,
+ IsEnum,
+ IsBoolean,
+ IsObject,
+ ValidateNested,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+import { ApiProperty } from '@nestjs/swagger';
+import { ActivityIcon } from '../activity.entity';
+import { I18nStringDto } from '../../exercise/dto/createGlobalExercise.dto';
+
+export class CreateGlobalActivityDto {
+ @ApiProperty({ type: I18nStringDto })
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ title: I18nStringDto;
+
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ description?: I18nStringDto;
+
+ @ApiProperty({ enum: ActivityIcon })
+ @IsEnum(ActivityIcon)
+ icon: ActivityIcon;
+
+ @ApiProperty({ type: [String], required: false })
+ @IsOptional()
+ @IsArray()
+ equipment?: string[];
+
+ @ApiProperty()
+ @IsBoolean()
+ trackDistance: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ trackPace: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ trackElevation: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ trackCalories: boolean;
+}
+
+export class UpdateGlobalActivityDto extends CreateGlobalActivityDto {}
diff --git a/backend/src/v1/activityLog/activityLog.service.ts b/backend/src/v1/activityLog/activityLog.service.ts
index c824dcc..3f74f90 100644
--- a/backend/src/v1/activityLog/activityLog.service.ts
+++ b/backend/src/v1/activityLog/activityLog.service.ts
@@ -63,9 +63,13 @@ export class ActivityLogService {
id: log.id,
activity: {
id: activity.id,
- name: activity.name,
- description: activity.description,
+ title: activity.title,
+ description: activity.descriptionI18n ?? undefined,
+ isGlobal: activity.isGlobal,
+ personalizedFromGlobalId: activity.personalizedFromGlobalId ?? undefined,
+ personalizedAt: activity.personalizedAt ?? undefined,
icon: activity.icon,
+ equipment: activity.equipment ?? undefined,
trackDistance: activity.trackDistance,
trackPace: activity.trackPace,
trackElevation: activity.trackElevation,
diff --git a/backend/src/v1/admin/admin.controller.ts b/backend/src/v1/admin/admin.controller.ts
new file mode 100644
index 0000000..5147e8d
--- /dev/null
+++ b/backend/src/v1/admin/admin.controller.ts
@@ -0,0 +1,665 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import {
+ Controller,
+ Get,
+ Post,
+ Put,
+ Delete,
+ Param,
+ Body,
+ Query,
+ Req,
+ ParseIntPipe,
+ UseGuards,
+ HttpCode,
+ HttpStatus,
+ UseInterceptors,
+ UploadedFile,
+ BadRequestException,
+ ConflictException,
+ StreamableFile,
+ Header,
+} from '@nestjs/common';
+import { FileInterceptor } from '@nestjs/platform-express';
+import {
+ ApiTags,
+ ApiOperation,
+ ApiBearerAuth,
+ ApiOkResponse,
+ ApiCreatedResponse,
+ ApiConsumes,
+} from '@nestjs/swagger';
+import { Request } from 'express';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { AdminService } from './admin.service';
+import { JwtAuthGuard } from '../guards/jwtAuth.guard';
+import { SuperAdminGuard } from '../guards/superAdmin.guard';
+import { ExerciseService } from '../exercise/exercise.service';
+import { ExerciseResponseDto } from '../exercise/dto/exerciseResponse.dto';
+import {
+ CreateGlobalExerciseDto,
+ UpdateGlobalExerciseDto,
+} from '../exercise/dto/createGlobalExercise.dto';
+import { ActivityService } from '../activity/activity.service';
+import { ActivityResponseDto } from '../activity/dto/activityResponse.dto';
+import {
+ CreateGlobalActivityDto,
+ UpdateGlobalActivityDto,
+} from '../activity/dto/createGlobalActivity.dto';
+import { MuscleGroupService } from '../muscleGroup/muscleGroup.service';
+import { MuscleGroup } from '../muscleGroup/muscleGroup.entity';
+import { UpdateMuscleGroupAdminDto } from '../muscleGroup/dto/updateMuscleGroupAdmin.dto';
+import { CreateMuscleGroupAdminDto } from '../muscleGroup/dto/createMuscleGroupAdmin.dto';
+import { ExerciseImage } from '../exercise/exerciseImage.entity';
+import { ExerciseImageResponseDto } from '../exercise/dto/exerciseImageResponse.dto';
+import { UploadService } from '../upload/upload.service';
+
+interface RequestWithUser extends Request {
+ user: { id: number; email: string; role: string };
+}
+
+@ApiTags('admin')
+@ApiBearerAuth()
+@UseGuards(JwtAuthGuard, SuperAdminGuard)
+@Controller('admin')
+export class AdminController {
+ constructor(
+ private readonly adminService: AdminService,
+ private readonly exerciseService: ExerciseService,
+ private readonly activityService: ActivityService,
+ private readonly muscleGroupService: MuscleGroupService,
+ private readonly uploadService: UploadService,
+ @InjectRepository(ExerciseImage)
+ private readonly exerciseImageRepo: Repository,
+ ) {}
+
+ // --- Admin meta ---
+
+ @Get('me')
+ @ApiOperation({ summary: 'Get current superadmin profile' })
+ getMe(@Req() req: RequestWithUser) {
+ return this.adminService.getMe(req.user.id);
+ }
+
+ @Get('stats')
+ @ApiOperation({ summary: 'Get aggregate instance statistics' })
+ getStats() {
+ return this.adminService.getStats();
+ }
+
+ // --- Users ---
+
+ @Get('users')
+ @ApiOperation({ summary: 'List all users with pagination and search' })
+ listUsers(
+ @Query('page') page?: string,
+ @Query('limit') limit?: string,
+ @Query('search') search?: string,
+ ) {
+ return this.adminService.listUsers(
+ parseInt(page ?? '1', 10),
+ parseInt(limit ?? '20', 10),
+ search,
+ );
+ }
+
+ @Get('users/:id')
+ @ApiOperation({ summary: 'Get a single user by ID' })
+ getUser(@Param('id', ParseIntPipe) id: number) {
+ return this.adminService.getUserById(id);
+ }
+
+ // --- Global Exercises ---
+
+ @Get('exercises')
+ @ApiOperation({ summary: 'List all global exercises' })
+ @ApiOkResponse({ type: [ExerciseResponseDto] })
+ listGlobalExercises(): Promise {
+ return this.exerciseService.findAll(0, 'global');
+ }
+
+ @Post('exercises')
+ @ApiOperation({ summary: 'Create a global exercise' })
+ @ApiCreatedResponse({ type: ExerciseResponseDto })
+ createGlobalExercise(
+ @Body() dto: CreateGlobalExerciseDto,
+ ): Promise {
+ return this.exerciseService.createGlobal(dto);
+ }
+
+ @Put('exercises/:id')
+ @ApiOperation({ summary: 'Update a global exercise' })
+ @ApiOkResponse({ type: ExerciseResponseDto })
+ updateGlobalExercise(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() dto: UpdateGlobalExerciseDto,
+ ): Promise {
+ return this.exerciseService.updateGlobal(id, dto);
+ }
+
+ @Delete('exercises/:id')
+ @ApiOperation({ summary: 'Soft-delete a global exercise (user data preserved)' })
+ @HttpCode(HttpStatus.NO_CONTENT)
+ deleteGlobalExercise(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise<{ message: string }> {
+ return this.exerciseService.deleteGlobal(id);
+ }
+
+ // --- Global Exercise Media ---
+
+ @Post('exercises/:id/media')
+ @ApiOperation({ summary: 'Upload instructional media (image or video) to a global exercise' })
+ @ApiCreatedResponse({ type: ExerciseResponseDto })
+ @ApiConsumes('multipart/form-data')
+ @UseInterceptors(FileInterceptor('file', { storage: undefined }))
+ async addGlobalExerciseMedia(
+ @Param('id', ParseIntPipe) id: number,
+ @UploadedFile() file: Express.Multer.File,
+ ): Promise {
+ const validation = this.uploadService.validateMediaFile(file);
+ if (!validation.valid) throw new BadRequestException(validation.error);
+
+ const { url, type } = await this.uploadService.processExerciseMedia(file);
+ return this.exerciseService.addGlobalMedia(id, url, type);
+ }
+
+ @Delete('exercises/:id/media/:mediaId')
+ @ApiOperation({ summary: 'Delete a media item from a global exercise' })
+ @HttpCode(HttpStatus.OK)
+ deleteGlobalExerciseMedia(
+ @Param('id', ParseIntPipe) id: number,
+ @Param('mediaId', ParseIntPipe) mediaId: number,
+ ): Promise {
+ return this.exerciseService.removeGlobalMedia(id, mediaId);
+ }
+
+ @Put('exercises/:id/media/reorder')
+ @ApiOperation({ summary: 'Reorder media items for a global exercise' })
+ reorderGlobalExerciseMedia(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() body: { mediaIds: number[] },
+ ): Promise {
+ return this.exerciseService.reorderGlobalMedia(id, body.mediaIds);
+ }
+
+ // --- Exercise Image Library ---
+
+ @Get('exercise-images')
+ @ApiOperation({ summary: 'List all images in the exercise image library' })
+ async listExerciseImages() {
+ const rows: {
+ id: number;
+ url: string;
+ fileSize: number | null;
+ createdAt: Date;
+ exercise_id: number | null;
+ exercise_title: Record | null;
+ }[] = await this.exerciseImageRepo.manager.query(`
+ SELECT
+ ei.id,
+ ei.url,
+ ei."fileSize",
+ ei."createdAt",
+ e.id AS exercise_id,
+ e.title AS exercise_title
+ FROM exercise_image ei
+ LEFT JOIN exercise e
+ ON e.image = ei.url AND e."deletedAt" IS NULL
+ ORDER BY ei."createdAt" DESC
+ `);
+
+ return rows.map((r) => ({
+ id: r.id,
+ url: r.url,
+ fileSize: r.fileSize,
+ createdAt: r.createdAt,
+ usedBy: r.exercise_id
+ ? { id: r.exercise_id, title: r.exercise_title }
+ : null,
+ }));
+ }
+
+ @Post('exercise-images')
+ @ApiOperation({ summary: 'Upload an image to the exercise image library' })
+ @ApiCreatedResponse({ type: ExerciseImageResponseDto })
+ @ApiConsumes('multipart/form-data')
+ @UseInterceptors(FileInterceptor('file', { storage: undefined }))
+ async uploadExerciseImage(
+ @UploadedFile() file: Express.Multer.File,
+ ): Promise {
+ const validation = this.uploadService.validateImageFile(file);
+ if (!validation.valid) throw new BadRequestException(validation.error);
+
+ const { url, fileSize } = await this.uploadService.processExerciseImage(file);
+ const record = this.exerciseImageRepo.create({ url, fileSize });
+ return this.exerciseImageRepo.save(record);
+ }
+
+ @Delete('exercise-images/:id')
+ @ApiOperation({ summary: 'Delete an image from the exercise image library' })
+ @HttpCode(HttpStatus.NO_CONTENT)
+ async deleteExerciseImage(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise {
+ const image = await this.exerciseImageRepo.findOne({ where: { id } });
+ if (!image) return;
+
+ // Block deletion if the image URL is still referenced by any exercise
+ const result: { count: string }[] = await this.exerciseImageRepo.manager.query(
+ `SELECT COUNT(*) AS count FROM exercise WHERE image = $1 AND "deletedAt" IS NULL`,
+ [image.url],
+ );
+ const inUse = parseInt(result[0]?.count ?? '0', 10);
+
+ if (inUse > 0) {
+ throw new ConflictException(
+ 'Image is currently assigned to one or more exercises. Remove it from those exercises first.',
+ );
+ }
+
+ await this.uploadService.deleteImage(image.url);
+ await this.exerciseImageRepo.remove(image);
+ }
+
+ // --- Global Activities ---
+
+ @Get('activities')
+ @ApiOperation({ summary: 'List all global activities' })
+ @ApiOkResponse({ type: [ActivityResponseDto] })
+ listGlobalActivities(): Promise {
+ return this.activityService.findAll(0, 'global');
+ }
+
+ @Post('activities')
+ @ApiOperation({ summary: 'Create a global activity' })
+ @ApiCreatedResponse({ type: ActivityResponseDto })
+ createGlobalActivity(
+ @Body() dto: CreateGlobalActivityDto,
+ ): Promise {
+ return this.activityService.createGlobal(dto);
+ }
+
+ @Put('activities/:id')
+ @ApiOperation({ summary: 'Update a global activity' })
+ @ApiOkResponse({ type: ActivityResponseDto })
+ updateGlobalActivity(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() dto: UpdateGlobalActivityDto,
+ ): Promise {
+ return this.activityService.updateGlobal(id, dto);
+ }
+
+ @Delete('activities/:id')
+ @ApiOperation({ summary: 'Soft-delete a global activity (user data preserved)' })
+ @HttpCode(HttpStatus.NO_CONTENT)
+ deleteGlobalActivity(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise<{ message: string }> {
+ return this.activityService.deleteGlobal(id);
+ }
+
+ // --- Muscle Groups ---
+
+ @Get('muscle-groups')
+ @ApiOperation({ summary: 'List all muscle groups' })
+ listMuscleGroups(): Promise {
+ return this.muscleGroupService.findAll();
+ }
+
+ @Post('muscle-groups')
+ @ApiOperation({ summary: 'Create a new muscle group' })
+ @ApiCreatedResponse({ type: MuscleGroup })
+ createMuscleGroup(
+ @Body() dto: CreateMuscleGroupAdminDto,
+ ): Promise {
+ return this.muscleGroupService.create({
+ name: dto.name,
+ nameI18n: dto.nameI18n ?? { default: dto.name },
+ descriptionI18n: dto.descriptionI18n,
+ } as any);
+ }
+
+ @Put('muscle-groups/:id')
+ @ApiOperation({ summary: 'Update muscle group translations' })
+ updateMuscleGroup(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() dto: UpdateMuscleGroupAdminDto,
+ ): Promise {
+ return this.muscleGroupService.update(id, dto as any);
+ }
+
+ @Get('muscle-groups/:id/exercises')
+ @ApiOperation({ summary: 'List exercises that reference a muscle group (for delete warnings)' })
+ async getMuscleGroupExercises(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise<{ id: number; title: any }[]> {
+ await this.muscleGroupService.findOne(id);
+ const exercises = await this.exerciseService.findAll(0, 'global');
+ return exercises
+ .filter(
+ (ex) =>
+ ex.muscleGroups.some((mg) => mg.id === id) ||
+ ex.primaryMuscleGroups?.some((mg) => mg.id === id),
+ )
+ .map((ex) => ({ id: ex.id, title: ex.title }));
+ }
+
+ @Delete('muscle-groups/:id')
+ @ApiOperation({ summary: 'Delete a muscle group (removes association from all exercises)' })
+ @HttpCode(HttpStatus.NO_CONTENT)
+ deleteMuscleGroup(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise<{ message: string }> {
+ return this.muscleGroupService.remove(id);
+ }
+
+ // --- Export ---
+
+ @Get('export/exercises')
+ @ApiOperation({ summary: 'Export all global exercises as a ZIP archive (one folder per exercise)' })
+ @Header('Content-Type', 'application/zip')
+ async exportExercises(): Promise {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const JSZip = require('jszip');
+ const zip = new JSZip();
+
+ const exercises = await this.exerciseService.findAll(0, 'global');
+
+ const sanitizeFolderName = (title: string): string =>
+ (title ?? '').replace(/[/\\:*?"<>|]/g, '-').trim().slice(0, 80) || 'exercise';
+
+ const usedNames = new Map();
+
+ for (const ex of exercises) {
+ const base = sanitizeFolderName(ex.title?.default ?? `exercise-${ex.id}`);
+ const count = usedNames.get(base) ?? 0;
+ usedNames.set(base, count + 1);
+ const folderName = count === 0 ? base : `${base}-${count + 1}`;
+
+ const mediaEntries: { order: number; type: string; file: string }[] = [];
+
+ // Cover image
+ let coverImagePath: string | null = null;
+ if (ex.image) {
+ const buffer = await this.uploadService.readFileAsBuffer(ex.image);
+ if (buffer) {
+ const ext = ex.image.split('.').pop() ?? 'webp';
+ zip.file(`${folderName}/images/cover.${ext}`, buffer);
+ coverImagePath = `images/cover.${ext}`;
+ }
+ }
+
+ // Media items
+ const sortedMedia = [...(ex.media ?? [])].sort((a, b) => a.order - b.order);
+ for (const mediaItem of sortedMedia) {
+ const buffer = await this.uploadService.readFileAsBuffer(mediaItem.url);
+ if (buffer) {
+ const ext = mediaItem.url.split('.').pop() ?? 'webp';
+ zip.file(`${folderName}/images/media-${mediaItem.order}.${ext}`, buffer);
+ mediaEntries.push({
+ order: mediaItem.order,
+ type: mediaItem.type,
+ file: `images/media-${mediaItem.order}.${ext}`,
+ });
+ }
+ }
+
+ zip.file(
+ `${folderName}/exercise.json`,
+ JSON.stringify(
+ {
+ title: ex.title,
+ description: ex.description ?? null,
+ exerciseType: ex.exerciseType ?? null,
+ muscleGroupNames: ex.muscleGroups.map((mg) => mg.name),
+ primaryMuscleGroupNames: ex.primaryMuscleGroups?.map((mg) => mg.name) ?? [],
+ equipmentI18n: ex.equipmentI18n ?? null,
+ instructionsI18n: ex.instructions ?? null,
+ proTipsI18n: ex.proTips ?? null,
+ mistakesI18n: ex.mistakes ?? null,
+ coverImage: coverImagePath,
+ media: mediaEntries,
+ },
+ null,
+ 2,
+ ),
+ );
+ }
+
+ const zipBuffer: Buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
+ const filename = `exercises-export-${new Date().toISOString().slice(0, 10)}.zip`;
+
+ return new StreamableFile(zipBuffer, {
+ type: 'application/zip',
+ disposition: `attachment; filename="${filename}"`,
+ length: zipBuffer.length,
+ });
+ }
+
+ @Get('export/activities')
+ @ApiOperation({ summary: 'Export all global activities as JSON' })
+ @Header('Content-Type', 'application/json')
+ async exportActivities(): Promise {
+ const activities = await this.activityService.findAll(0, 'global');
+
+ const payload = {
+ version: 1,
+ type: 'activities',
+ exportedAt: new Date().toISOString(),
+ count: activities.length,
+ activities: activities.map((act) => ({
+ title: act.title,
+ description: act.description ?? null,
+ icon: act.icon,
+ equipment: act.equipment ?? [],
+ trackDistance: act.trackDistance,
+ trackPace: act.trackPace,
+ trackElevation: act.trackElevation,
+ trackCalories: act.trackCalories,
+ })),
+ };
+
+ const jsonBuffer = Buffer.from(JSON.stringify(payload, null, 2), 'utf-8');
+ const filename = `activities-export-${new Date().toISOString().slice(0, 10)}.json`;
+
+ return new StreamableFile(jsonBuffer, {
+ type: 'application/json',
+ disposition: `attachment; filename="${filename}"`,
+ length: jsonBuffer.length,
+ });
+ }
+
+ // --- Import ---
+
+ @Post('import/exercises')
+ @ApiOperation({ summary: 'Import global exercises from a ZIP export archive' })
+ @ApiConsumes('multipart/form-data')
+ @UseInterceptors(FileInterceptor('file', { storage: undefined }))
+ async importExercises(
+ @UploadedFile() file: Express.Multer.File,
+ ): Promise<{ created: number; skipped: number; errors: string[] }> {
+ if (!file) throw new BadRequestException('No file provided');
+
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const JSZip = require('jszip');
+ let zip: any;
+ try {
+ zip = await JSZip.loadAsync(file.buffer);
+ } catch {
+ throw new BadRequestException('Invalid ZIP file');
+ }
+
+ // Find all exercise.json entries (not directories)
+ const exerciseJsonPaths: string[] = Object.keys(zip.files).filter(
+ (p: string) => p.endsWith('/exercise.json') && !zip.files[p].dir,
+ );
+
+ if (exerciseJsonPaths.length === 0) {
+ throw new BadRequestException('No exercise.json files found in ZIP — is this a valid exercises export?');
+ }
+
+ const allMuscleGroups = await this.muscleGroupService.findAll();
+ const mgByName = new Map(
+ allMuscleGroups.map((mg) => [mg.name.toLowerCase(), mg.id]),
+ );
+
+ const existingExercises = await this.exerciseService.findAll(0, 'global');
+ const existingTitles = new Set(
+ existingExercises.map((e) => (e.title?.default ?? '').toLowerCase()),
+ );
+
+ let created = 0;
+ let skipped = 0;
+ const errors: string[] = [];
+
+ for (const jsonPath of exerciseJsonPaths) {
+ const folderPrefix = jsonPath.slice(0, jsonPath.lastIndexOf('/') + 1); // e.g. "Bench Press/"
+ let item: any;
+ try {
+ const jsonContent = await zip.files[jsonPath].async('string');
+ item = JSON.parse(jsonContent);
+ } catch {
+ errors.push(`${jsonPath}: failed to parse exercise.json`);
+ continue;
+ }
+
+ const titleDefault = item.title?.default ?? '';
+ if (existingTitles.has(titleDefault.toLowerCase())) {
+ skipped++;
+ continue;
+ }
+
+ try {
+ // Cover image
+ let imageUrl: string | undefined;
+ if (item.coverImage) {
+ const coverPath = `${folderPrefix}${item.coverImage}`;
+ const coverFile = zip.files[coverPath];
+ if (coverFile && !coverFile.dir) {
+ const buffer: Buffer = await coverFile.async('nodebuffer');
+ const ext = coverPath.split('.').pop() ?? 'webp';
+ imageUrl = await this.uploadService.writeExerciseImageFromBuffer(buffer, ext);
+ }
+ }
+
+ const muscleGroupIds = (item.muscleGroupNames ?? [])
+ .map((name: string) => mgByName.get(name.toLowerCase()))
+ .filter((id: number | undefined): id is number => id !== undefined);
+
+ const primaryMuscleGroupIds = (item.primaryMuscleGroupNames ?? [])
+ .map((name: string) => mgByName.get(name.toLowerCase()))
+ .filter((id: number | undefined): id is number => id !== undefined);
+
+ const created_exercise = await this.exerciseService.createGlobal({
+ title: item.title,
+ description: item.description ?? undefined,
+ exerciseType: item.exerciseType ?? undefined,
+ muscleGroupIds,
+ primaryMuscleGroupIds,
+ equipmentI18n: item.equipmentI18n ?? undefined,
+ imageUrl,
+ instructionsI18n: item.instructionsI18n ?? undefined,
+ proTipsI18n: item.proTipsI18n ?? undefined,
+ mistakesI18n: item.mistakesI18n ?? undefined,
+ } as any);
+
+ // Media items
+ for (const mediaEntry of (item.media ?? []) as { order: number; type: string; file: string }[]) {
+ const mediaPath = `${folderPrefix}${mediaEntry.file}`;
+ const mediaFile = zip.files[mediaPath];
+ if (mediaFile && !mediaFile.dir) {
+ const buffer: Buffer = await mediaFile.async('nodebuffer');
+ const ext = mediaPath.split('.').pop() ?? 'webp';
+ const mediaUrl = await this.uploadService.writeExerciseMediaFromBuffer(buffer, ext);
+ await this.exerciseService.addGlobalMedia(
+ created_exercise.id,
+ mediaUrl,
+ mediaEntry.type as 'image' | 'video',
+ );
+ }
+ }
+
+ existingTitles.add(titleDefault.toLowerCase());
+ created++;
+ } catch (err: any) {
+ errors.push(`"${titleDefault}": ${err?.message ?? 'unknown error'}`);
+ }
+ }
+
+ return { created, skipped, errors };
+ }
+
+ @Post('import/activities')
+ @ApiOperation({ summary: 'Import global activities from a JSON export file' })
+ @ApiConsumes('multipart/form-data')
+ @UseInterceptors(FileInterceptor('file', { storage: undefined }))
+ async importActivities(
+ @UploadedFile() file: Express.Multer.File,
+ ): Promise<{ created: number; skipped: number; errors: string[] }> {
+ if (!file) throw new BadRequestException('No file provided');
+
+ let payload: any;
+ try {
+ payload = JSON.parse(file.buffer.toString('utf-8'));
+ } catch {
+ throw new BadRequestException('Invalid JSON file');
+ }
+
+ if (payload.type !== 'activities' || !Array.isArray(payload.activities)) {
+ throw new BadRequestException('File does not appear to be an activities export');
+ }
+
+ const existingActivities = await this.activityService.findAll(0, 'global');
+ const existingTitles = new Set(
+ existingActivities.map((a) => (a.title?.default ?? '').toLowerCase()),
+ );
+
+ let created = 0;
+ let skipped = 0;
+ const errors: string[] = [];
+
+ for (const item of payload.activities) {
+ const titleDefault = item.title?.default ?? '';
+ if (existingTitles.has(titleDefault.toLowerCase())) {
+ skipped++;
+ continue;
+ }
+
+ try {
+ await this.activityService.createGlobal({
+ title: item.title,
+ description: item.description ?? undefined,
+ icon: item.icon,
+ equipment: item.equipment ?? [],
+ trackDistance: item.trackDistance ?? false,
+ trackPace: item.trackPace ?? false,
+ trackElevation: item.trackElevation ?? false,
+ trackCalories: item.trackCalories ?? false,
+ });
+
+ existingTitles.add(titleDefault.toLowerCase());
+ created++;
+ } catch (err: any) {
+ errors.push(`"${titleDefault}": ${err?.message ?? 'unknown error'}`);
+ }
+ }
+
+ return { created, skipped, errors };
+ }
+}
diff --git a/backend/src/v1/admin/admin.module.ts b/backend/src/v1/admin/admin.module.ts
new file mode 100644
index 0000000..8b7edfc
--- /dev/null
+++ b/backend/src/v1/admin/admin.module.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { AdminController } from './admin.controller';
+import { AdminService } from './admin.service';
+import { User } from '../user/user.entity';
+import { Workout } from '../workout/workout.entity';
+import { WorkoutSession } from '../workoutSession/workoutSession.entity';
+import { ExerciseModule } from '../exercise/exercise.module';
+import { ActivityModule } from '../activity/activity.module';
+import { MuscleGroupModule } from '../muscleGroup/muscleGroup.module';
+import { ExerciseImage } from '../exercise/exerciseImage.entity';
+import { UploadModule } from '../upload/upload.module';
+
+@Module({
+ imports: [
+ TypeOrmModule.forFeature([User, Workout, WorkoutSession, ExerciseImage]),
+ ExerciseModule,
+ ActivityModule,
+ MuscleGroupModule,
+ UploadModule,
+ ],
+ controllers: [AdminController],
+ providers: [AdminService],
+})
+export class AdminModule {}
diff --git a/backend/src/v1/admin/admin.service.ts b/backend/src/v1/admin/admin.service.ts
new file mode 100644
index 0000000..2c4142d
--- /dev/null
+++ b/backend/src/v1/admin/admin.service.ts
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, ILike } from 'typeorm';
+import { User } from '../user/user.entity';
+import { Workout } from '../workout/workout.entity';
+import { WorkoutSession } from '../workoutSession/workoutSession.entity';
+
+@Injectable()
+export class AdminService {
+ constructor(
+ @InjectRepository(User)
+ private readonly userRepo: Repository,
+
+ @InjectRepository(Workout)
+ private readonly workoutRepo: Repository,
+
+ @InjectRepository(WorkoutSession)
+ private readonly sessionRepo: Repository,
+ ) {}
+
+ async listUsers(page: number, limit: number, search?: string) {
+ const where = search
+ ? [
+ { email: ILike(`%${search}%`) },
+ { firstName: ILike(`%${search}%`) },
+ { lastName: ILike(`%${search}%`) },
+ ]
+ : {};
+
+ const [users, total] = await this.userRepo.findAndCount({
+ where,
+ select: [
+ 'id',
+ 'email',
+ 'firstName',
+ 'lastName',
+ 'avatar',
+ 'role',
+ 'emailVerified',
+ 'currentStreak',
+ 'weeklyWorkoutGoal',
+ 'onboardingCompleted',
+ 'createdAt',
+ 'updatedAt',
+ ],
+ order: { createdAt: 'DESC' },
+ skip: (page - 1) * limit,
+ take: limit,
+ });
+
+ return { users, total, page, limit };
+ }
+
+ async getUserById(id: number) {
+ const user = await this.userRepo.findOne({
+ where: { id },
+ select: [
+ 'id',
+ 'email',
+ 'firstName',
+ 'lastName',
+ 'avatar',
+ 'role',
+ 'emailVerified',
+ 'currentStreak',
+ 'weeklyWorkoutGoal',
+ 'currentWeekWorkouts',
+ 'streakFreezes',
+ 'completedGoalWeeksCount',
+ 'unitScale',
+ 'weight',
+ 'height',
+ 'dateOfBirth',
+ 'gender',
+ 'primaryGoal',
+ 'onboardingCompleted',
+ 'showRpe',
+ 'showWeightTracking',
+ 'termsAcceptedAt',
+ 'termsVersion',
+ 'createdAt',
+ 'updatedAt',
+ ],
+ });
+
+ if (!user) throw new NotFoundException('User not found');
+ return user;
+ }
+
+ async getStats() {
+ const [totalUsers, totalWorkouts, totalSessions] = await Promise.all([
+ this.userRepo.count(),
+ this.workoutRepo.count(),
+ this.sessionRepo.count({ where: { status: 'finished' } }),
+ ]);
+
+ const recentUsers = await this.userRepo.find({
+ select: ['id', 'email', 'firstName', 'lastName', 'avatar', 'createdAt'],
+ order: { createdAt: 'DESC' },
+ take: 5,
+ });
+
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+ const newUsersLast30Days = await this.userRepo
+ .createQueryBuilder('user')
+ .where('user.createdAt >= :date', { date: thirtyDaysAgo })
+ .getCount();
+
+ return {
+ totalUsers,
+ totalWorkouts,
+ totalSessions,
+ newUsersLast30Days,
+ recentUsers,
+ };
+ }
+
+ async getMe(userId: number) {
+ return this.getUserById(userId);
+ }
+}
diff --git a/backend/src/v1/app.module.ts b/backend/src/v1/app.module.ts
index fb29d06..befbb68 100644
--- a/backend/src/v1/app.module.ts
+++ b/backend/src/v1/app.module.ts
@@ -31,6 +31,7 @@ import { ScheduledSessionModule } from './scheduledSession/scheduledSession.modu
import { StatisticsModule } from './statistics/statistics.module';
import { ProgressPhotoModule } from './progressPhoto/progressPhoto.module';
import { ReleasesModule } from './releases/releases.module';
+import { AdminModule } from './admin/admin.module';
@Module({
imports: [
@@ -47,7 +48,9 @@ import { ReleasesModule } from './releases/releases.module';
password: configService.get('DATABASE_PASSWORD'),
database: configService.get('DATABASE_NAME'),
autoLoadEntities: true,
- synchronize: true,
+ synchronize: false,
+ migrations: [__dirname + '/migrations/*.{ts,js}'],
+ migrationsRun: true,
logging: ['error', 'warn', 'query'],
}),
inject: [ConfigService],
@@ -65,6 +68,7 @@ import { ReleasesModule } from './releases/releases.module';
StatisticsModule,
ProgressPhotoModule,
ReleasesModule,
+ AdminModule,
],
providers: [AppService, JwtStrategy],
})
diff --git a/backend/src/v1/auth/auth.controller.ts b/backend/src/v1/auth/auth.controller.ts
index 5cb7e73..bc43710 100644
--- a/backend/src/v1/auth/auth.controller.ts
+++ b/backend/src/v1/auth/auth.controller.ts
@@ -143,6 +143,15 @@ export class AuthController {
return { user };
}
+ @Post('logout')
+ @HttpCode(HttpStatus.OK)
+ @ApiOkResponse({ description: 'Logged out successfully' })
+ logout(@Res({ passthrough: true }) res: Response) {
+ const cookiePath = process.env.AUTH_COOKIE_PATH || '/';
+ res.clearCookie('auth_token', { path: cookiePath });
+ return { ok: true };
+ }
+
@Post('resend-verification')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ description: 'Verification email sent' })
diff --git a/backend/src/v1/auth/auth.module.ts b/backend/src/v1/auth/auth.module.ts
index 757831b..e3ea18a 100644
--- a/backend/src/v1/auth/auth.module.ts
+++ b/backend/src/v1/auth/auth.module.ts
@@ -20,8 +20,6 @@ import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from '../user/user.module';
-import { ActivityModule } from '../activity/activity.module';
-import { ExerciseModule } from '../exercise/exercise.module';
import { EmailModule } from '../email/email.module';
import { GithubStrategy } from '../strategies/github.strategy';
import { GoogleStrategy } from '../strategies/google.strategy';
@@ -51,8 +49,6 @@ const googleStrategyProvider = {
@Module({
imports: [
UserModule,
- ActivityModule,
- ExerciseModule,
EmailModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
ConfigModule,
diff --git a/backend/src/v1/auth/auth.service.ts b/backend/src/v1/auth/auth.service.ts
index 815fc17..a7b2c3c 100644
--- a/backend/src/v1/auth/auth.service.ts
+++ b/backend/src/v1/auth/auth.service.ts
@@ -30,9 +30,6 @@ import { ConfigService } from '@nestjs/config';
import { UserWithoutPasswordDto } from './dto/UserWithoutPassword.dto';
import { User } from '../user/user.entity';
import { JwtService } from '@nestjs/jwt';
-import { ActivityService } from '../activity/activity.service';
-import { ExerciseSeedService } from '../exercise/exerciseSeed.service';
-import { activitiesToSeed } from '../seed/data/activities.data';
import { EmailService } from '../email/email.service';
@Injectable()
@@ -42,8 +39,6 @@ export class AuthService {
private readonly userRepo: Repository,
private readonly configService: ConfigService,
private jwtService: JwtService,
- private readonly activityService: ActivityService,
- private readonly exerciseSeedService: ExerciseSeedService,
private readonly emailService: EmailService,
) {}
@@ -98,38 +93,17 @@ export class AuthService {
});
const savedUser = await this.userRepo.save(user);
- // Seed default activities for new user
- await this.seedDefaultActivities(savedUser.id);
-
- // Seed default exercises for new user
- await this.exerciseSeedService.seedDefaultExercises(savedUser.id);
-
// Send verification email only when feature is enabled
if (requireVerification && verificationCode) {
- await this.emailService.sendVerificationEmail(savedUser.email, verificationCode);
+ await this.emailService.sendVerificationEmail(
+ savedUser.email,
+ verificationCode,
+ );
}
return new UserWithoutPasswordDto(savedUser);
}
- /**
- * Seed default activities for a new user.
- * Uses the shared activitiesToSeed list so updates only need to happen in one place.
- */
- private async seedDefaultActivities(userId: number): Promise {
- for (const activity of activitiesToSeed) {
- try {
- await this.activityService.create(activity, userId);
- } catch (error) {
- // Ignore errors (e.g., duplicate names) and continue
- console.error(
- `Failed to seed activity ${activity.name}:`,
- error.message,
- );
- }
- }
- }
-
async login(dto: LoginDto) {
const user = await this.userRepo.findOne({
where: { email: dto.email },
@@ -139,9 +113,26 @@ export class AuthService {
'firstName',
'lastName',
'password',
+ 'avatar',
'showRpe',
+ 'weeklyWorkoutGoal',
+ 'currentStreak',
+ 'currentWeekWorkouts',
+ 'unitScale',
+ 'weight',
+ 'height',
+ 'dateOfBirth',
+ 'gender',
+ 'primaryGoal',
+ 'targetWeight',
+ 'goalTimeframe',
'onboardingCompleted',
'emailVerified',
+ 'showWeightTracking',
+ 'weightGoalType',
+ 'startWeight',
+ 'role',
+ 'language',
],
});
@@ -159,7 +150,10 @@ export class AuthService {
return { token, user: userDto };
}
- async verifyEmail(email: string, code: string): Promise<{ token: string; user: UserWithoutPasswordDto }> {
+ async verifyEmail(
+ email: string,
+ code: string,
+ ): Promise<{ token: string; user: UserWithoutPasswordDto }> {
const user = await this.userRepo.findOne({
where: { email },
select: [
@@ -176,7 +170,8 @@ export class AuthService {
});
if (!user) throw new NotFoundException('User not found');
- if (user.emailVerified) throw new BadRequestException('Email already verified');
+ if (user.emailVerified)
+ throw new BadRequestException('Email already verified');
const inputHash = crypto.createHash('sha256').update(code).digest('hex');
@@ -203,22 +198,23 @@ export class AuthService {
async resendVerification(email: string): Promise {
const user = await this.userRepo.findOne({
where: { email },
- select: [
- 'id',
- 'email',
- 'emailVerified',
- 'emailVerificationExpires',
- ],
+ select: ['id', 'email', 'emailVerified', 'emailVerificationExpires'],
});
// Return silently to avoid user enumeration
if (!user) return;
- if (user.emailVerified) throw new BadRequestException('Email already verified');
+ if (user.emailVerified)
+ throw new BadRequestException('Email already verified');
// Rate-limit: block resend if a code was sent less than 1 minute ago
const oneMinuteFromNow = new Date(Date.now() + 14 * 60 * 1000);
- if (user.emailVerificationExpires && user.emailVerificationExpires > oneMinuteFromNow) {
- throw new BadRequestException('Please wait before requesting another code');
+ if (
+ user.emailVerificationExpires &&
+ user.emailVerificationExpires > oneMinuteFromNow
+ ) {
+ throw new BadRequestException(
+ 'Please wait before requesting another code',
+ );
}
const { code, hash, expires } = this.generateCode();
@@ -256,7 +252,9 @@ export class AuthService {
avatar?: string;
}): Promise<{ token: string; user: UserWithoutPasswordDto; isNew: boolean }> {
// 1. Try to find by githubId
- let user = await this.userRepo.findOne({ where: { githubId: profile.githubId } });
+ let user = await this.userRepo.findOne({
+ where: { githubId: profile.githubId },
+ });
if (!user && profile.email) {
// 2. Try to find by email — link GitHub to existing account
@@ -270,11 +268,14 @@ export class AuthService {
let isNew = false;
if (!user) {
// 3. Create new user
- const defaultShowRpeRaw = this.configService.get('DEFAULT_SHOW_RPE');
+ const defaultShowRpeRaw =
+ this.configService.get('DEFAULT_SHOW_RPE');
const defaultShowRpe =
defaultShowRpeRaw == null
? true
- : ['1', 'true', 'yes', 'on'].includes(defaultShowRpeRaw.toLowerCase());
+ : ['1', 'true', 'yes', 'on'].includes(
+ defaultShowRpeRaw.toLowerCase(),
+ );
user = this.userRepo.create({
githubId: profile.githubId,
@@ -289,8 +290,6 @@ export class AuthService {
});
user = await this.userRepo.save(user);
- await this.seedDefaultActivities(user.id);
- await this.exerciseSeedService.seedDefaultExercises(user.id);
isNew = true;
}
@@ -312,7 +311,9 @@ export class AuthService {
avatar?: string;
}): Promise<{ token: string; user: UserWithoutPasswordDto; isNew: boolean }> {
// 1. Try to find by googleId
- let user = await this.userRepo.findOne({ where: { googleId: profile.googleId } });
+ let user = await this.userRepo.findOne({
+ where: { googleId: profile.googleId },
+ });
if (!user && profile.email) {
// 2. Try to find by email — link Google to existing account
@@ -326,11 +327,14 @@ export class AuthService {
let isNew = false;
if (!user) {
// 3. Create new user
- const defaultShowRpeRaw = this.configService.get('DEFAULT_SHOW_RPE');
+ const defaultShowRpeRaw =
+ this.configService.get('DEFAULT_SHOW_RPE');
const defaultShowRpe =
defaultShowRpeRaw == null
? true
- : ['1', 'true', 'yes', 'on'].includes(defaultShowRpeRaw.toLowerCase());
+ : ['1', 'true', 'yes', 'on'].includes(
+ defaultShowRpeRaw.toLowerCase(),
+ );
user = this.userRepo.create({
googleId: profile.googleId,
@@ -345,8 +349,6 @@ export class AuthService {
});
user = await this.userRepo.save(user);
- await this.seedDefaultActivities(user.id);
- await this.exerciseSeedService.seedDefaultExercises(user.id);
isNew = true;
}
@@ -360,15 +362,14 @@ export class AuthService {
return { token, user: new UserWithoutPasswordDto(user), isNew };
}
- async resetPassword(email: string, code: string, newPassword: string): Promise {
+ async resetPassword(
+ email: string,
+ code: string,
+ newPassword: string,
+ ): Promise {
const user = await this.userRepo.findOne({
where: { email },
- select: [
- 'id',
- 'email',
- 'passwordResetToken',
- 'passwordResetExpires',
- ],
+ select: ['id', 'email', 'passwordResetToken', 'passwordResetExpires'],
});
if (!user) throw new BadRequestException('Invalid or expired reset code');
diff --git a/backend/src/v1/auth/dto/UserWithoutPassword.dto.ts b/backend/src/v1/auth/dto/UserWithoutPassword.dto.ts
index 6d22a57..00907e1 100644
--- a/backend/src/v1/auth/dto/UserWithoutPassword.dto.ts
+++ b/backend/src/v1/auth/dto/UserWithoutPassword.dto.ts
@@ -74,6 +74,9 @@ export class UserWithoutPasswordDto {
@ApiProperty({ default: false })
emailVerified: boolean;
+ @ApiProperty({ default: 'user' })
+ role: 'user' | 'superadmin';
+
@ApiProperty({ default: false })
showWeightTracking: boolean;
@@ -83,6 +86,9 @@ export class UserWithoutPasswordDto {
@ApiProperty({ required: false })
startWeight?: number;
+ @ApiProperty({ default: 'default' })
+ language: 'default' | 'eng' | 'swe';
+
constructor(user: User) {
this.id = user.id;
this.email = user.email;
@@ -106,5 +112,7 @@ export class UserWithoutPasswordDto {
this.showWeightTracking = user.showWeightTracking ?? false;
this.weightGoalType = user.weightGoalType ?? undefined;
this.startWeight = user.startWeight;
+ this.role = user.role ?? 'user';
+ this.language = user.language ?? 'default';
}
}
diff --git a/backend/src/v1/common/types/i18n.types.ts b/backend/src/v1/common/types/i18n.types.ts
new file mode 100644
index 0000000..c7f4f9a
--- /dev/null
+++ b/backend/src/v1/common/types/i18n.types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+export interface I18nString {
+ default: string | null;
+ eng?: string;
+ swe?: string;
+}
+
+export interface I18nStringArray {
+ default?: string[];
+ eng?: string[];
+ swe?: string[];
+}
+
+export type SupportedLanguage = 'default' | 'eng' | 'swe';
diff --git a/backend/src/v1/dataSource.ts b/backend/src/v1/dataSource.ts
index dd48860..0dd657e 100644
--- a/backend/src/v1/dataSource.ts
+++ b/backend/src/v1/dataSource.ts
@@ -20,6 +20,7 @@ import * as dotenv from 'dotenv';
import { User } from './user/user.entity';
import { Exercise } from './exercise/exercise.entity';
import { ExerciseMedia } from './exercise/exerciseMedia.entity';
+import { ExerciseImage } from './exercise/exerciseImage.entity';
import { Workout } from './workout/workout.entity';
import { WorkoutExercise } from './workout/workoutExercise.entity';
import { WorkoutSession } from './workoutSession/workoutSession.entity';
@@ -27,6 +28,11 @@ import { MuscleGroup } from './muscleGroup/muscleGroup.entity';
import { WorkoutSessionExercise } from './workoutSession/workoutSessionExercise.entity';
import { WorkoutSessionSet } from './workoutSession/workoutSessionSet.entity';
import { ScheduledSession } from './scheduledSession/scheduledSession.entity';
+import { Activity } from './activity/activity.entity';
+import { ActivityLog } from './activityLog/activityLog.entity';
+import { ExerciseRecord } from './statistics/exerciseRecord.entity';
+import { WeightLog } from './weightLog/weightLog.entity';
+import { ProgressPhoto } from './progressPhoto/progressPhoto.entity';
dotenv.config();
@@ -37,18 +43,25 @@ export const AppDataSource = new DataSource({
username: process.env.DATABASE_USER || 'user',
password: process.env.DATABASE_PASSWORD || 'password',
database: process.env.DATABASE_NAME || 'grindifydb',
- synchronize: true,
+ synchronize: false,
logging: ['error', 'warn'],
entities: [
User,
MuscleGroup,
Exercise,
ExerciseMedia,
+ ExerciseImage,
Workout,
WorkoutExercise,
WorkoutSession,
WorkoutSessionExercise,
WorkoutSessionSet,
ScheduledSession,
+ Activity,
+ ActivityLog,
+ ExerciseRecord,
+ WeightLog,
+ ProgressPhoto,
],
+ migrations: [__dirname + '/migrations/*.{ts,js}'],
});
diff --git a/backend/src/v1/exercise/dto/createGlobalExercise.dto.ts b/backend/src/v1/exercise/dto/createGlobalExercise.dto.ts
new file mode 100644
index 0000000..ba6e92a
--- /dev/null
+++ b/backend/src/v1/exercise/dto/createGlobalExercise.dto.ts
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import {
+ IsOptional,
+ IsString,
+ IsArray,
+ IsInt,
+ IsEnum,
+ IsObject,
+ ValidateNested,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+import { ApiProperty } from '@nestjs/swagger';
+import { ExerciseType } from '../exercise.entity';
+import { I18nString, I18nStringArray } from '../../common/types/i18n.types';
+
+export class I18nStringDto implements I18nString {
+ @ApiProperty()
+ @IsOptional()
+ @IsString()
+ default: string | null;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ eng?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ swe?: string;
+}
+
+export class I18nStringArrayDto implements I18nStringArray {
+ @ApiProperty({ required: false, type: [String] })
+ @IsOptional()
+ @IsArray()
+ default?: string[];
+
+ @ApiProperty({ required: false, type: [String] })
+ @IsOptional()
+ @IsArray()
+ eng?: string[];
+
+ @ApiProperty({ required: false, type: [String] })
+ @IsOptional()
+ @IsArray()
+ swe?: string[];
+}
+
+export class CreateGlobalExerciseDto {
+ @ApiProperty({ type: I18nStringDto })
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ title: I18nStringDto;
+
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ description?: I18nStringDto;
+
+ @ApiProperty({ enum: ExerciseType, required: false })
+ @IsOptional()
+ @IsEnum(ExerciseType)
+ exerciseType?: ExerciseType;
+
+ @ApiProperty({ type: [Number], required: false })
+ @IsOptional()
+ @IsArray()
+ @IsInt({ each: true })
+ muscleGroupIds?: number[];
+
+ @ApiProperty({ type: [Number], required: false })
+ @IsOptional()
+ @IsArray()
+ @IsInt({ each: true })
+ primaryMuscleGroupIds?: number[];
+
+ @ApiProperty({ type: I18nStringArrayDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringArrayDto)
+ equipmentI18n?: I18nStringArrayDto;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ imageUrl?: string;
+
+ @ApiProperty({ type: I18nStringArrayDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringArrayDto)
+ instructionsI18n?: I18nStringArrayDto;
+
+ @ApiProperty({ type: I18nStringArrayDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringArrayDto)
+ proTipsI18n?: I18nStringArrayDto;
+
+ @ApiProperty({ type: I18nStringArrayDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringArrayDto)
+ mistakesI18n?: I18nStringArrayDto;
+}
+
+export class UpdateGlobalExerciseDto extends CreateGlobalExerciseDto {}
diff --git a/backend/src/v1/exercise/dto/exerciseImageResponse.dto.ts b/backend/src/v1/exercise/dto/exerciseImageResponse.dto.ts
new file mode 100644
index 0000000..f19d257
--- /dev/null
+++ b/backend/src/v1/exercise/dto/exerciseImageResponse.dto.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { ApiProperty } from '@nestjs/swagger';
+
+export class ExerciseImageResponseDto {
+ @ApiProperty()
+ id: number;
+
+ @ApiProperty()
+ url: string;
+
+ @ApiProperty({ required: false, nullable: true })
+ fileSize: number | null;
+
+ @ApiProperty()
+ createdAt: Date;
+}
diff --git a/backend/src/v1/exercise/dto/exerciseResponse.dto.ts b/backend/src/v1/exercise/dto/exerciseResponse.dto.ts
index 6cdef18..554d101 100644
--- a/backend/src/v1/exercise/dto/exerciseResponse.dto.ts
+++ b/backend/src/v1/exercise/dto/exerciseResponse.dto.ts
@@ -16,6 +16,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { MuscleGroupResponseDto } from 'src/v1/muscleGroup/dto/muscleGroupResponse.dto';
import { ExerciseType } from '../exercise.entity';
+import { I18nString, I18nStringArray } from '../../common/types/i18n.types';
export class ExerciseMediaResponseDto {
@ApiProperty()
@@ -36,40 +37,40 @@ export class ExerciseResponseDto {
id: number;
@ApiProperty()
- name: string;
+ title: I18nString;
- @ApiProperty({
- required: false,
- description: 'Optional i18n key for the exercise name',
- })
- i18nKey?: string;
+ @ApiProperty({ required: false })
+ description?: I18nString;
- @ApiProperty({
- required: false,
- description: 'If true, the user has overridden the default translated name',
- })
- isNameCustom?: boolean;
+ @ApiProperty()
+ isGlobal: boolean;
@ApiProperty({ required: false })
- description?: string;
+ personalizedFromGlobalId?: number;
@ApiProperty({ required: false })
- image?: string;
+ personalizedAt?: Date;
+
+ @ApiProperty({ required: false, nullable: true })
+ image?: string | null;
@ApiProperty({ required: false, enum: ExerciseType })
exerciseType?: ExerciseType;
@ApiProperty({ required: false, type: [String] })
- equipment?: string[];
+ equipment: string[];
- @ApiProperty({ required: false, type: [String] })
- instructions?: string[];
+ @ApiProperty({ required: false })
+ equipmentI18n?: I18nStringArray;
- @ApiProperty({ required: false, type: [String] })
- proTips?: string[];
+ @ApiProperty({ required: false })
+ instructions?: I18nStringArray;
- @ApiProperty({ required: false, type: [String] })
- mistakes?: string[];
+ @ApiProperty({ required: false })
+ proTips?: I18nStringArray;
+
+ @ApiProperty({ required: false })
+ mistakes?: I18nStringArray;
@ApiProperty()
createdAt: Date;
diff --git a/backend/src/v1/exercise/exercise.controller.ts b/backend/src/v1/exercise/exercise.controller.ts
index 33e7b82..183ded9 100644
--- a/backend/src/v1/exercise/exercise.controller.ts
+++ b/backend/src/v1/exercise/exercise.controller.ts
@@ -21,6 +21,7 @@ import {
Delete,
Param,
Body,
+ Query,
Req,
UseGuards,
UnauthorizedException,
@@ -58,13 +59,16 @@ export class ExerciseController {
) {}
@Get()
- @ApiOperation({ summary: 'Get all exercises for the logged-in user' })
+ @ApiOperation({ summary: 'Get exercises (filter: all | global | mine)' })
@ApiOkResponse({ type: [ExerciseResponseDto] })
- getAllExercises(@Req() req: RequestWithUser): Promise {
+ getAllExercises(
+ @Req() req: RequestWithUser,
+ @Query('filter') filter?: 'all' | 'global' | 'mine',
+ ): Promise {
if (!req.user?.id) {
throw new UnauthorizedException('User not authenticated');
}
- return this.exerciseService.findAll(+req.user.id);
+ return this.exerciseService.findAll(+req.user.id, filter ?? 'all');
}
@Post()
@@ -153,12 +157,26 @@ export class ExerciseController {
}
// Process and save the image
- const imageUrl = await this.uploadService.processExerciseImage(file);
+ const { url: imageUrl } = await this.uploadService.processExerciseImage(file);
// Update the exercise with the new image URL
return this.exerciseService.updateImage(id, imageUrl, +req.user.id);
}
+ @Post(':id/duplicate')
+ @ApiOperation({ summary: 'Duplicate a global exercise for personal use' })
+ @ApiCreatedResponse({ type: ExerciseResponseDto })
+ duplicateExercise(
+ @Param('id', ParseIntPipe) id: number,
+ @Body() body: { transferStats?: boolean },
+ @Req() req: RequestWithUser,
+ ): Promise {
+ if (!req.user?.id) {
+ throw new UnauthorizedException('User not authenticated');
+ }
+ return this.exerciseService.duplicateGlobalExercise(id, +req.user.id, body.transferStats ?? false);
+ }
+
// --- Media endpoints ---
@Post(':id/media')
diff --git a/backend/src/v1/exercise/exercise.entity.ts b/backend/src/v1/exercise/exercise.entity.ts
index 70d03d6..9bc9255 100644
--- a/backend/src/v1/exercise/exercise.entity.ts
+++ b/backend/src/v1/exercise/exercise.entity.ts
@@ -27,6 +27,7 @@ import {
import { User } from '../user/user.entity';
import { MuscleGroup } from '../muscleGroup/muscleGroup.entity';
import { ExerciseMedia } from './exerciseMedia.entity';
+import { I18nString, I18nStringArray } from '../common/types/i18n.types';
export enum ExerciseType {
COMPOUND = 'compound',
@@ -39,45 +40,41 @@ export class Exercise {
@PrimaryGeneratedColumn()
id: number;
- @Column()
- name: string;
+ @Column({ type: 'jsonb' })
+ title: I18nString;
- /**
- * Optional i18n key (copied from the global exercise when imported).
- * Frontend can use this to translate the display name in the future.
- */
- @Column({ nullable: true })
- i18nKey?: string;
+ @Column({ name: 'descriptionI18n', type: 'jsonb', nullable: true })
+ descriptionI18n?: I18nString;
- /**
- * If true, user has edited the name and we should use `name` instead of the i18n translation.
- */
@Column({ default: false })
- isNameCustom: boolean;
+ isGlobal: boolean;
@Column({ nullable: true })
- description: string;
+ personalizedFromGlobalId?: number;
- @Column({ nullable: true })
- image: string;
+ @Column({ type: 'timestamptz', nullable: true })
+ personalizedAt?: Date;
+
+ @Column({ type: 'varchar', nullable: true })
+ image: string | null;
@Column({ type: 'enum', enum: ExerciseType, nullable: true })
exerciseType?: ExerciseType;
- @Column({ type: 'jsonb', nullable: true })
- equipment?: string[];
+ @Column({ name: 'equipment_i18n', type: 'jsonb', nullable: true })
+ equipmentI18n?: I18nStringArray;
- @Column({ type: 'jsonb', nullable: true })
- instructions?: string[];
+ @Column({ name: 'instructionsI18n', type: 'jsonb', nullable: true })
+ instructionsI18n?: I18nStringArray;
- @Column({ type: 'jsonb', nullable: true })
- proTips?: string[];
+ @Column({ name: 'proTipsI18n', type: 'jsonb', nullable: true })
+ proTipsI18n?: I18nStringArray;
- @Column({ type: 'jsonb', nullable: true })
- mistakes?: string[];
+ @Column({ name: 'mistakesI18n', type: 'jsonb', nullable: true })
+ mistakesI18n?: I18nStringArray;
- @ManyToOne(() => User, { onDelete: 'CASCADE' })
- createdBy: User;
+ @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
+ createdBy: User | null;
@ManyToMany(() => MuscleGroup)
@JoinTable()
diff --git a/backend/src/v1/exercise/exercise.service.spec.ts b/backend/src/v1/exercise/exercise.service.spec.ts
index 59b6063..97361fa 100644
--- a/backend/src/v1/exercise/exercise.service.spec.ts
+++ b/backend/src/v1/exercise/exercise.service.spec.ts
@@ -14,20 +14,662 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
+import { getRepositoryToken } from '@nestjs/typeorm';
+import { DataSource } from 'typeorm';
+import { NotFoundException } from '@nestjs/common';
import { ExerciseService } from './exercise.service';
+import { Exercise, ExerciseType } from './exercise.entity';
+import { ExerciseMedia } from './exerciseMedia.entity';
+import { MuscleGroupService } from '../muscleGroup/muscleGroup.service';
+import { UploadService } from '../upload/upload.service';
+
+// ── Helpers ──────────────────────────────────────────────────────────────────
+
+function makeExercise(overrides: Partial = {}): Exercise {
+ return {
+ id: 1,
+ title: { default: 'Bench Press' },
+ descriptionI18n: undefined,
+ isGlobal: false,
+ personalizedFromGlobalId: undefined,
+ personalizedAt: undefined,
+ image: undefined as any,
+ exerciseType: ExerciseType.COMPOUND,
+ equipmentI18n: undefined,
+ instructionsI18n: undefined,
+ proTipsI18n: undefined,
+ mistakesI18n: undefined,
+ createdBy: { id: 1 } as any,
+ primaryMuscleGroups: [],
+ muscleGroups: [],
+ media: [],
+ createdAt: new Date('2026-01-01'),
+ deletedAt: undefined,
+ ...overrides,
+ } as Exercise;
+}
+
+function makeGlobalExercise(overrides: Partial = {}): Exercise {
+ return makeExercise({
+ id: 100,
+ title: { default: 'Pull-up', eng: 'Pull-up', swe: 'Chins' },
+ isGlobal: true,
+ createdBy: null,
+ ...overrides,
+ });
+}
+
+function makeMockManager(affectedUsers: { userId: number }[] = [], copyId = 200) {
+ return {
+ query: jest.fn()
+ .mockResolvedValueOnce(affectedUsers)
+ .mockResolvedValue(undefined),
+ create: jest.fn((_entity: any, data: any) => ({ ...data })),
+ save: jest.fn((_entity: any, data: any) => Promise.resolve({ ...data, id: copyId++ })),
+ softRemove: jest.fn().mockResolvedValue(undefined),
+ findOne: jest.fn(),
+ };
+}
+
+// ── Test suite ────────────────────────────────────────────────────────────────
describe('ExerciseService', () => {
let service: ExerciseService;
+ let exerciseRepo: jest.Mocked;
+ let mediaRepo: jest.Mocked;
+ let muscleGroupService: jest.Mocked>;
+ let dataSource: { transaction: jest.Mock };
beforeEach(async () => {
+ exerciseRepo = {
+ find: jest.fn(),
+ findOne: jest.fn(),
+ create: jest.fn((_data: any) => ({ ..._data })),
+ save: jest.fn((e: any) => Promise.resolve({ ...e, id: e.id ?? 99 })),
+ softRemove: jest.fn().mockResolvedValue(undefined),
+ };
+
+ mediaRepo = {
+ findOne: jest.fn(),
+ create: jest.fn((data: any) => data),
+ save: jest.fn((m: any) => Promise.resolve(m)),
+ remove: jest.fn().mockResolvedValue(undefined),
+ update: jest.fn().mockResolvedValue(undefined),
+ };
+
+ muscleGroupService = { findByIds: jest.fn().mockResolvedValue([]) };
+
+ dataSource = { transaction: jest.fn() };
+
const module: TestingModule = await Test.createTestingModule({
- providers: [ExerciseService],
+ providers: [
+ ExerciseService,
+ { provide: getRepositoryToken(Exercise), useValue: exerciseRepo },
+ { provide: getRepositoryToken(ExerciseMedia), useValue: mediaRepo },
+ { provide: MuscleGroupService, useValue: muscleGroupService },
+ { provide: UploadService, useValue: { deleteImage: jest.fn() } },
+ { provide: DataSource, useValue: dataSource },
+ ],
}).compile();
service = module.get(ExerciseService);
});
- it('should be defined', () => {
- expect(service).toBeDefined();
+ // ── findAll ────────────────────────────────────────────────────────────────
+
+ describe('findAll', () => {
+ it('filter=mine returns only user exercises', async () => {
+ exerciseRepo.find.mockResolvedValue([makeExercise()]);
+
+ const result = await service.findAll(1, 'mine');
+
+ expect(exerciseRepo.find).toHaveBeenCalledTimes(1);
+ expect(exerciseRepo.find).toHaveBeenCalledWith(
+ expect.objectContaining({ where: { createdBy: { id: 1 }, isGlobal: false } }),
+ );
+ expect(result).toHaveLength(1);
+ expect(result[0].isGlobal).toBe(false);
+ });
+
+ it('filter=global returns only global exercises', async () => {
+ exerciseRepo.find.mockResolvedValue([makeGlobalExercise()]);
+
+ const result = await service.findAll(1, 'global');
+
+ expect(exerciseRepo.find).toHaveBeenCalledTimes(1);
+ expect(exerciseRepo.find).toHaveBeenCalledWith(
+ expect.objectContaining({ where: { isGlobal: true } }),
+ );
+ expect(result[0].isGlobal).toBe(true);
+ });
+
+ it('filter=all returns user exercises first, then globals', async () => {
+ const userEx = makeExercise({ id: 1 });
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.find
+ .mockResolvedValueOnce([userEx])
+ .mockResolvedValueOnce([globalEx]);
+
+ const result = await service.findAll(1, 'all');
+
+ expect(exerciseRepo.find).toHaveBeenCalledTimes(2);
+ expect(result).toHaveLength(2);
+ expect(result[0].id).toBe(1);
+ expect(result[1].id).toBe(100);
+ });
+
+ it('default filter is all (makes two parallel repo calls)', async () => {
+ exerciseRepo.find.mockResolvedValue([]);
+
+ await service.findAll(1);
+
+ expect(exerciseRepo.find).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ // ── findOne ────────────────────────────────────────────────────────────────
+
+ describe('findOne', () => {
+ it('returns exercise owned by user', async () => {
+ exerciseRepo.findOne.mockResolvedValue(makeExercise());
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.id).toBe(1);
+ expect(result.title).toEqual({ default: 'Bench Press' });
+ });
+
+ it('returns global exercise regardless of userId', async () => {
+ exerciseRepo.findOne.mockResolvedValue(makeGlobalExercise());
+
+ const result = await service.findOne(100, 999);
+
+ expect(result.isGlobal).toBe(true);
+ });
+
+ it('queries with [user-owned OR global] where conditions', async () => {
+ exerciseRepo.findOne.mockResolvedValue(makeExercise());
+
+ await service.findOne(1, 42);
+
+ expect(exerciseRepo.findOne).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: [
+ { id: 1, createdBy: { id: 42 } },
+ { id: 1, isGlobal: true },
+ ],
+ }),
+ );
+ });
+
+ it('throws NotFoundException when no exercise matches', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.findOne(999, 1)).rejects.toThrow(NotFoundException);
+ });
+
+ it('returns soft-deleted exercise (withDeleted: true)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({ deletedAt: new Date('2026-04-01') }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.deletedAt).toEqual(new Date('2026-04-01'));
+ });
+ });
+
+ // ── create ─────────────────────────────────────────────────────────────────
+
+ describe('create', () => {
+ beforeEach(() => {
+ exerciseRepo.save.mockResolvedValue(makeExercise({ id: 5 }));
+ exerciseRepo.findOne.mockResolvedValue(makeExercise({ id: 5 }));
+ });
+
+ it('wraps string name into title.default', async () => {
+ await service.create({ name: 'Squat' } as any, 1);
+
+ expect(exerciseRepo.create).toHaveBeenCalledWith(
+ expect.objectContaining({ title: { default: 'Squat' }, isGlobal: false }),
+ );
+ });
+
+ it('wraps optional description into descriptionI18n.default', async () => {
+ await service.create({ name: 'Squat', description: 'Leg exercise' } as any, 1);
+
+ expect(exerciseRepo.create).toHaveBeenCalledWith(
+ expect.objectContaining({ descriptionI18n: { default: 'Leg exercise' } }),
+ );
+ });
+
+ it('sets createdBy to the userId', async () => {
+ await service.create({ name: 'Deadlift' } as any, 42);
+
+ expect(exerciseRepo.create).toHaveBeenCalledWith(
+ expect.objectContaining({ createdBy: { id: 42 } }),
+ );
+ });
+
+ it('omits descriptionI18n when no description provided', async () => {
+ await service.create({ name: 'Plank' } as any, 1);
+
+ const callArg = exerciseRepo.create.mock.calls[0][0];
+ expect(callArg.descriptionI18n).toBeUndefined();
+ });
+ });
+
+ // ── update ─────────────────────────────────────────────────────────────────
+
+ describe('update', () => {
+ it('updates title.default when name is provided', async () => {
+ const existing = makeExercise({ title: { default: 'Old Name' } });
+ exerciseRepo.findOne.mockResolvedValue(existing);
+ exerciseRepo.save.mockResolvedValue({ ...existing, title: { default: 'New Name' } });
+
+ await service.update(1, { name: 'New Name' } as any, 1);
+
+ expect(exerciseRepo.findOne).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { id: 1, createdBy: { id: 1 }, isGlobal: false },
+ }),
+ );
+ expect(existing.title.default).toBe('New Name');
+ });
+
+ it('throws NotFoundException for global exercises (isGlobal:false guard)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.update(100, { name: 'X' } as any, 1)).rejects.toThrow(
+ NotFoundException,
+ );
+ });
+
+ it('throws NotFoundException when exercise belongs to another user', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.update(1, {} as any, 99)).rejects.toThrow(NotFoundException);
+ });
+ });
+
+ // ── remove ─────────────────────────────────────────────────────────────────
+
+ describe('remove', () => {
+ it('soft-deletes the user-owned exercise', async () => {
+ const ex = makeExercise();
+ exerciseRepo.findOne.mockResolvedValue(ex);
+
+ const result = await service.remove(1, 1);
+
+ expect(exerciseRepo.softRemove).toHaveBeenCalledWith(ex);
+ expect(result).toEqual({ message: 'Exercise deleted' });
+ });
+
+ it('throws NotFoundException for global exercises (isGlobal:false guard)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.remove(100, 1)).rejects.toThrow(NotFoundException);
+ });
+
+ it('does NOT hard-delete — only softRemove', async () => {
+ const ex = makeExercise();
+ exerciseRepo.findOne.mockResolvedValue(ex);
+ const removeSpy = jest.spyOn(exerciseRepo, 'softRemove');
+
+ await service.remove(1, 1);
+
+ expect(removeSpy).toHaveBeenCalled();
+ expect(exerciseRepo['delete']).toBeUndefined();
+ });
+ });
+
+ // ── deleteGlobal ──────────────────────────────────────────────────────────
+
+ describe('deleteGlobal', () => {
+ it('throws NotFoundException when global exercise does not exist', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.deleteGlobal(999)).rejects.toThrow(NotFoundException);
+ });
+
+ it('soft-deletes global exercise — never hard-deletes', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeMockManager([]);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.deleteGlobal(100);
+
+ expect(manager.softRemove).toHaveBeenCalledWith(Exercise, globalEx);
+ });
+
+ it('skips copy creation when no users have data on the exercise', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeMockManager([]); // no affected users
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.deleteGlobal(100);
+
+ expect(manager.create).not.toHaveBeenCalled();
+ expect(manager.save).not.toHaveBeenCalled();
+ expect(manager.softRemove).toHaveBeenCalledWith(Exercise, globalEx);
+ });
+
+ it('creates a personalized copy for each affected user', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeMockManager([{ userId: 1 }, { userId: 2 }]);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.deleteGlobal(100);
+
+ expect(manager.create).toHaveBeenCalledTimes(2);
+ expect(manager.save).toHaveBeenCalledTimes(2);
+ expect(manager.create).toHaveBeenCalledWith(
+ Exercise,
+ expect.objectContaining({
+ isGlobal: false,
+ personalizedFromGlobalId: 100,
+ personalizedAt: expect.any(Date),
+ title: globalEx.title,
+ }),
+ );
+ });
+
+ it('copies all translation fields from the global onto the user copy', async () => {
+ const globalEx = makeGlobalExercise({
+ id: 100,
+ title: { default: 'Pull-up', eng: 'Pull-up', swe: 'Chins' },
+ descriptionI18n: { default: 'Back exercise', swe: 'Ryggrörsel' },
+ instructionsI18n: { default: ['Grip bar', 'Pull up'] },
+ });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeMockManager([{ userId: 5 }]);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.deleteGlobal(100);
+
+ expect(manager.create).toHaveBeenCalledWith(
+ Exercise,
+ expect.objectContaining({
+ title: globalEx.title,
+ descriptionI18n: globalEx.descriptionI18n,
+ instructionsI18n: globalEx.instructionsI18n,
+ }),
+ );
+ });
+
+ it('updates FK refs in session_exercise, exercise_record, workout_exercise for each user', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ let savedId = 200;
+ const manager = {
+ query: jest.fn()
+ .mockResolvedValueOnce([{ userId: 7 }])
+ .mockResolvedValue(undefined),
+ create: jest.fn((_e: any, data: any) => data),
+ save: jest.fn((_e: any, data: any) => Promise.resolve({ ...data, id: savedId++ })),
+ softRemove: jest.fn().mockResolvedValue(undefined),
+ };
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.deleteGlobal(100);
+
+ // query calls: 1 (affected users) + 3 (FK updates for user 7) = 4
+ expect(manager.query).toHaveBeenCalledTimes(4);
+
+ // All FK updates pass [newCopyId=200, globalId=100, userId=7]
+ const fkCalls = manager.query.mock.calls.slice(1);
+ fkCalls.forEach((call: any[]) => {
+ expect(call[1]).toEqual([200, 100, 7]);
+ });
+ });
+
+ it('returns a success message', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeMockManager([]);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ const result = await service.deleteGlobal(100);
+
+ expect(result.message).toBeTruthy();
+ });
+ });
+
+ // ── duplicateGlobalExercise ───────────────────────────────────────────────
+
+ describe('duplicateGlobalExercise', () => {
+ function makeManagerForDuplicate(copyId = 200, globalEx: Exercise) {
+ const fullCopy = {
+ ...globalEx,
+ id: copyId,
+ isGlobal: false,
+ personalizedFromGlobalId: globalEx.id,
+ createdBy: { id: 1 },
+ };
+ return {
+ create: jest.fn((_e: any, data: any) => data),
+ save: jest.fn((_e: any, data: any) => Promise.resolve({ ...data, id: copyId })),
+ query: jest.fn().mockResolvedValue(undefined),
+ findOne: jest.fn().mockResolvedValue(fullCopy),
+ };
+ }
+
+ it('throws NotFoundException when global exercise does not exist', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.duplicateGlobalExercise(999, 1, false)).rejects.toThrow(
+ NotFoundException,
+ );
+ });
+
+ it('creates a personal copy with isGlobal=false and personalization metadata', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeManagerForDuplicate(200, globalEx);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ const result = await service.duplicateGlobalExercise(100, 1, false);
+
+ expect(manager.create).toHaveBeenCalledWith(
+ Exercise,
+ expect.objectContaining({
+ isGlobal: false,
+ createdBy: { id: 1 },
+ personalizedFromGlobalId: 100,
+ personalizedAt: expect.any(Date),
+ title: globalEx.title,
+ }),
+ );
+ expect(result.isGlobal).toBe(false);
+ expect(result.personalizedFromGlobalId).toBe(100);
+ });
+
+ it('does NOT run any FK-update queries when transferStats=false', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeManagerForDuplicate(200, globalEx);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.duplicateGlobalExercise(100, 1, false);
+
+ expect(manager.query).not.toHaveBeenCalled();
+ });
+
+ it('runs FK-update queries for all 3 tables when transferStats=true', async () => {
+ const globalEx = makeGlobalExercise({ id: 100 });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeManagerForDuplicate(200, globalEx);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.duplicateGlobalExercise(100, 1, true);
+
+ expect(manager.query).toHaveBeenCalledTimes(3);
+ manager.query.mock.calls.forEach((call: any[]) => {
+ expect(call[1]).toEqual([200, 100, 1]); // [newCopyId, globalId, userId]
+ });
+ });
+
+ it('copies all i18n translation fields from global source', async () => {
+ const globalEx = makeGlobalExercise({
+ id: 100,
+ title: { default: 'Pull-up', eng: 'Pull-up', swe: 'Chins' },
+ instructionsI18n: { default: ['Grab bar'], swe: ['Tag tag'] },
+ proTipsI18n: { default: ['Keep core tight'] },
+ mistakesI18n: { default: ['Kipping'] },
+ });
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+
+ const manager = makeManagerForDuplicate(200, globalEx);
+ dataSource.transaction.mockImplementation(async (cb: any) => cb(manager));
+
+ await service.duplicateGlobalExercise(100, 5, false);
+
+ expect(manager.create).toHaveBeenCalledWith(
+ Exercise,
+ expect.objectContaining({
+ instructionsI18n: globalEx.instructionsI18n,
+ proTipsI18n: globalEx.proTipsI18n,
+ mistakesI18n: globalEx.mistakesI18n,
+ }),
+ );
+ });
+ });
+
+ // ── serialization: I18nString fields don't crash ──────────────────────────
+
+ describe('toResponseDto (I18nString serialization)', () => {
+ it('maps multilingual title correctly', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({
+ title: { default: 'Bench Press', eng: 'Bench Press', swe: 'Bänkpress' },
+ }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.title).toEqual({
+ default: 'Bench Press',
+ eng: 'Bench Press',
+ swe: 'Bänkpress',
+ });
+ });
+
+ it('maps I18nStringArray fields (instructions, proTips, mistakes)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({
+ instructionsI18n: { default: ['Step 1', 'Step 2'], swe: ['Steg 1', 'Steg 2'] },
+ proTipsI18n: { default: ['Keep core tight'] },
+ mistakesI18n: { default: ['Flared elbows'] },
+ }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.instructions).toEqual({
+ default: ['Step 1', 'Step 2'],
+ swe: ['Steg 1', 'Steg 2'],
+ });
+ expect(result.proTips).toEqual({ default: ['Keep core tight'] });
+ expect(result.mistakes).toEqual({ default: ['Flared elbows'] });
+ });
+
+ it('handles migrated legacy exercise with only title.default (no optional fields)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({
+ title: { default: 'Squat' },
+ descriptionI18n: undefined,
+ instructionsI18n: undefined,
+ proTipsI18n: undefined,
+ mistakesI18n: undefined,
+ equipmentI18n: undefined,
+ }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.title).toEqual({ default: 'Squat' });
+ expect(result.description).toBeUndefined();
+ expect(result.instructions).toBeUndefined();
+ expect(result.proTips).toBeUndefined();
+ expect(result.mistakes).toBeUndefined();
+ expect(result.equipment).toEqual([]);
+ expect(result.equipmentI18n).toBeUndefined();
+ });
+
+ it('does not crash when exercise has a deletedAt timestamp (old session data)', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({
+ title: { default: 'Overhead Press' },
+ deletedAt: new Date('2026-03-15'),
+ }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.title).toEqual({ default: 'Overhead Press' });
+ expect(result.deletedAt).toEqual(new Date('2026-03-15'));
+ });
+
+ it('defaults equipment to empty array when equipmentI18n is null/undefined', async () => {
+ exerciseRepo.findOne.mockResolvedValue(
+ makeExercise({ equipmentI18n: undefined }),
+ );
+
+ const result = await service.findOne(1, 1);
+
+ expect(result.equipment).toEqual([]);
+ });
+ });
+
+ // ── createGlobal / updateGlobal ───────────────────────────────────────────
+
+ describe('createGlobal', () => {
+ it('creates with isGlobal=true and createdBy=null', async () => {
+ const saved = makeGlobalExercise({ id: 101 });
+ exerciseRepo.save.mockResolvedValue(saved);
+ exerciseRepo.findOne.mockResolvedValue(saved);
+
+ await service.createGlobal({
+ title: { default: 'New Global' },
+ } as any);
+
+ expect(exerciseRepo.create).toHaveBeenCalledWith(
+ expect.objectContaining({ isGlobal: true, createdBy: null }),
+ );
+ });
+ });
+
+ describe('updateGlobal', () => {
+ it('throws NotFoundException when global exercise does not exist', async () => {
+ exerciseRepo.findOne.mockResolvedValue(null);
+
+ await expect(service.updateGlobal(999, { title: { default: 'X' } } as any)).rejects.toThrow(
+ NotFoundException,
+ );
+ });
+
+ it('queries with isGlobal=true filter', async () => {
+ const globalEx = makeGlobalExercise();
+ exerciseRepo.findOne.mockResolvedValue(globalEx);
+ exerciseRepo.save.mockResolvedValue(globalEx);
+
+ await service.updateGlobal(100, { title: { default: 'Updated' } } as any);
+
+ expect(exerciseRepo.findOne).toHaveBeenCalledWith(
+ expect.objectContaining({ where: { id: 100, isGlobal: true } }),
+ );
+ });
});
});
diff --git a/backend/src/v1/exercise/exercise.service.ts b/backend/src/v1/exercise/exercise.service.ts
index 2492e5f..0da7328 100644
--- a/backend/src/v1/exercise/exercise.service.ts
+++ b/backend/src/v1/exercise/exercise.service.ts
@@ -15,14 +15,19 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
+import { DataSource, Repository } from 'typeorm';
import { Exercise } from './exercise.entity';
import { ExerciseMedia } from './exerciseMedia.entity';
import { CreateExerciseDto } from './dto/createExercise.dto';
import { UpdateExerciseDto } from './dto/updateExercise.dto';
import { ExerciseResponseDto } from './dto/exerciseResponse.dto';
+import { CreateGlobalExerciseDto, UpdateGlobalExerciseDto } from './dto/createGlobalExercise.dto';
import { MuscleGroupService } from '../muscleGroup/muscleGroup.service';
import { UploadService } from '../upload/upload.service';
+import { MuscleGroupResponseDto } from '../muscleGroup/dto/muscleGroupResponse.dto';
+import { MuscleGroup } from '../muscleGroup/muscleGroup.entity';
+
+export type ExerciseFilter = 'all' | 'global' | 'mine';
@Injectable()
export class ExerciseService {
@@ -33,48 +38,42 @@ export class ExerciseService {
private readonly mediaRepo: Repository,
private readonly muscleGroupService: MuscleGroupService,
private readonly uploadService: UploadService,
+ private readonly dataSource: DataSource,
) {}
+ private toMuscleGroupDto(mg: MuscleGroup): MuscleGroupResponseDto {
+ return {
+ id: mg.id,
+ name: mg.name,
+ nameI18n: mg.nameI18n,
+ descriptionI18n: mg.descriptionI18n,
+ createdAt: mg.createdAt,
+ updatedAt: mg.updatedAt,
+ };
+ }
+
private toResponseDto(exercise: Exercise): ExerciseResponseDto {
return {
id: exercise.id,
- name: exercise.name,
- i18nKey: exercise.i18nKey,
- isNameCustom: exercise.isNameCustom,
- description: exercise.description,
+ title: exercise.title,
+ description: exercise.descriptionI18n,
+ isGlobal: exercise.isGlobal,
+ personalizedFromGlobalId: exercise.personalizedFromGlobalId,
+ personalizedAt: exercise.personalizedAt,
image: exercise.image,
exerciseType: exercise.exerciseType,
createdAt: exercise.createdAt,
deletedAt: exercise.deletedAt,
- equipment: exercise.equipment || [],
- instructions: exercise.instructions || [],
- proTips: exercise.proTips || [],
- mistakes: exercise.mistakes || [],
- primaryMuscleGroups:
- exercise.primaryMuscleGroups?.map((mg) => ({
- id: mg.id,
- name: mg.name,
- description: mg.description,
- createdAt: mg.createdAt,
- updatedAt: mg.updatedAt,
- })) || [],
- muscleGroups:
- exercise.muscleGroups?.map((mg) => ({
- id: mg.id,
- name: mg.name,
- description: mg.description,
- createdAt: mg.createdAt,
- updatedAt: mg.updatedAt,
- })) || [],
- media:
- exercise.media
- ?.sort((a, b) => a.order - b.order)
- .map((m) => ({
- id: m.id,
- type: m.type,
- url: m.url,
- order: m.order,
- })) || [],
+ equipment: exercise.equipmentI18n?.default ?? [],
+ equipmentI18n: exercise.equipmentI18n,
+ instructions: exercise.instructionsI18n,
+ proTips: exercise.proTipsI18n,
+ mistakes: exercise.mistakesI18n,
+ primaryMuscleGroups: exercise.primaryMuscleGroups?.map((mg) => this.toMuscleGroupDto(mg)) || [],
+ muscleGroups: exercise.muscleGroups?.map((mg) => this.toMuscleGroupDto(mg)) || [],
+ media: exercise.media
+ ?.sort((a, b) => a.order - b.order)
+ .map((m) => ({ id: m.id, type: m.type, url: m.url, order: m.order })) || [],
};
}
@@ -82,19 +81,52 @@ export class ExerciseService {
return exercises.map((e) => this.toResponseDto(e));
}
- async findAll(userId: number): Promise {
- const exercises = await this.exerciseRepo.find({
- where: { createdBy: { id: userId } },
- relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
- });
+ async findAll(userId: number, filter: ExerciseFilter = 'all'): Promise {
+ const relations = ['muscleGroups', 'primaryMuscleGroups', 'media'];
+
+ if (filter === 'mine') {
+ const exercises = await this.exerciseRepo.find({
+ where: { createdBy: { id: userId }, isGlobal: false },
+ relations,
+ });
+ return this.toResponseList(exercises);
+ }
- return this.toResponseList(exercises);
+ if (filter === 'global') {
+ const exercises = await this.exerciseRepo.find({
+ where: { isGlobal: true },
+ relations,
+ order: { id: 'ASC' },
+ });
+ return this.toResponseList(exercises);
+ }
+
+ // 'all': user's own exercises first, then globals
+ const [userExercises, globalExercises] = await Promise.all([
+ this.exerciseRepo.find({
+ where: { createdBy: { id: userId }, isGlobal: false },
+ relations,
+ }),
+ this.exerciseRepo.find({
+ where: { isGlobal: true },
+ relations,
+ order: { id: 'ASC' },
+ }),
+ ]);
+
+ return this.toResponseList([...userExercises, ...globalExercises]);
}
async findOne(id: number, userId: number): Promise {
+ const relations = ['muscleGroups', 'primaryMuscleGroups', 'media'];
+
+ // Finds if the exercise belongs to user OR is global
const exercise = await this.exerciseRepo.findOne({
- where: { id, createdBy: { id: userId } },
- relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ where: [
+ { id, createdBy: { id: userId } },
+ { id, isGlobal: true },
+ ],
+ relations,
withDeleted: true,
});
@@ -105,75 +137,195 @@ export class ExerciseService {
return this.toResponseDto(exercise);
}
- async create(
- dto: CreateExerciseDto,
- userId: number,
- ): Promise {
- const { muscleGroupIds, primaryMuscleGroupIds, ...exerciseData } = dto;
+ async create(dto: CreateExerciseDto, userId: number): Promise {
+ const { muscleGroupIds, primaryMuscleGroupIds, name, description, instructions, proTips, mistakes, equipment, ...rest } = dto;
const exercise = this.exerciseRepo.create({
- ...exerciseData,
+ ...rest,
+ title: { default: name },
+ descriptionI18n: description ? { default: description } : undefined,
+ equipmentI18n: equipment?.length ? { default: equipment } : undefined,
+ instructionsI18n: instructions?.length ? { default: instructions } : undefined,
+ proTipsI18n: proTips?.length ? { default: proTips } : undefined,
+ mistakesI18n: mistakes?.length ? { default: mistakes } : undefined,
+ isGlobal: false,
createdBy: { id: userId } as any,
});
- if (muscleGroupIds && muscleGroupIds.length > 0) {
- exercise.muscleGroups =
- await this.muscleGroupService.findByIds(muscleGroupIds);
+ if (muscleGroupIds?.length) {
+ exercise.muscleGroups = await this.muscleGroupService.findByIds(muscleGroupIds);
}
+ exercise.primaryMuscleGroups = primaryMuscleGroupIds?.length
+ ? await this.muscleGroupService.findByIds(primaryMuscleGroupIds)
+ : [];
+
+ const saved = await this.exerciseRepo.save(exercise);
+ return this.findOne(saved.id, userId);
+ }
+
+ async createGlobal(dto: CreateGlobalExerciseDto): Promise {
+ const { muscleGroupIds, primaryMuscleGroupIds, imageUrl, description, ...rest } = dto;
- if (primaryMuscleGroupIds && primaryMuscleGroupIds.length > 0) {
- exercise.primaryMuscleGroups =
- await this.muscleGroupService.findByIds(primaryMuscleGroupIds);
- } else {
- exercise.primaryMuscleGroups = [];
+ const exercise = this.exerciseRepo.create({
+ ...rest,
+ descriptionI18n: description,
+ image: imageUrl ?? undefined,
+ isGlobal: true,
+ createdBy: null,
+ });
+
+ if (muscleGroupIds?.length) {
+ exercise.muscleGroups = await this.muscleGroupService.findByIds(muscleGroupIds);
}
+ exercise.primaryMuscleGroups = primaryMuscleGroupIds?.length
+ ? await this.muscleGroupService.findByIds(primaryMuscleGroupIds)
+ : [];
const saved = await this.exerciseRepo.save(exercise);
+ const full = await this.exerciseRepo.findOne({
+ where: { id: saved.id },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ return this.toResponseDto(full!);
+ }
- // Re-fetch with all relations
- return this.findOne(saved.id, userId);
+ async updateGlobal(id: number, dto: UpdateGlobalExerciseDto): Promise {
+ const existing = await this.exerciseRepo.findOne({
+ where: { id, isGlobal: true },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ if (!existing) throw new NotFoundException('Global exercise not found');
+
+ const { muscleGroupIds, primaryMuscleGroupIds, imageUrl, description, ...rest } = dto;
+ Object.assign(existing, rest);
+ if (description !== undefined) existing.descriptionI18n = description;
+ if (imageUrl !== undefined) existing.image = imageUrl;
+
+ if (muscleGroupIds !== undefined) {
+ existing.muscleGroups = await this.muscleGroupService.findByIds(muscleGroupIds);
+ }
+ if (primaryMuscleGroupIds !== undefined) {
+ existing.primaryMuscleGroups = primaryMuscleGroupIds.length
+ ? await this.muscleGroupService.findByIds(primaryMuscleGroupIds)
+ : [];
+ }
+
+ const updated = await this.exerciseRepo.save(existing);
+ return this.toResponseDto(updated);
}
- async update(
- id: number,
- dto: UpdateExerciseDto,
- userId: number,
- ): Promise {
- const { muscleGroupIds, primaryMuscleGroupIds, ...exerciseData } = dto;
+ async deleteGlobal(id: number): Promise<{ message: string }> {
+ const exercise = await this.exerciseRepo.findOne({
+ where: { id, isGlobal: true },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ if (!exercise) throw new NotFoundException('Global exercise not found');
+
+ await this.dataSource.transaction(async (manager) => {
+ // Find all users with workout data referencing this exercise
+ const affectedUsers: { userId: number }[] = await manager.query(`
+ SELECT DISTINCT ws."userId" AS "userId"
+ FROM workout_session_exercise wse
+ JOIN workout_session ws ON wse."sessionId" = ws.id
+ WHERE wse."exerciseId" = $1
+ UNION
+ SELECT DISTINCT "userId" AS "userId"
+ FROM exercise_record
+ WHERE "exerciseId" = $1
+ UNION
+ SELECT DISTINCT w."createdById" AS "userId"
+ FROM workout_exercise we
+ JOIN workout w ON we."workoutId" = w.id
+ WHERE we."exerciseId" = $1
+ `, [id]);
+
+ for (const { userId } of affectedUsers) {
+ // Create a personal copy for this user
+ const copy = manager.create(Exercise, {
+ title: exercise.title,
+ descriptionI18n: exercise.descriptionI18n,
+ isGlobal: false,
+ createdBy: { id: userId } as any,
+ personalizedFromGlobalId: exercise.id,
+ personalizedAt: new Date(),
+ image: exercise.image,
+ exerciseType: exercise.exerciseType,
+ equipmentI18n: exercise.equipmentI18n,
+ instructionsI18n: exercise.instructionsI18n,
+ proTipsI18n: exercise.proTipsI18n,
+ mistakesI18n: exercise.mistakesI18n,
+ muscleGroups: exercise.muscleGroups,
+ primaryMuscleGroups: exercise.primaryMuscleGroups,
+ });
+ const saved = await manager.save(Exercise, copy);
+
+ // Redirect all FK references from global → user copy
+ await manager.query(
+ `UPDATE workout_session_exercise wse
+ SET "exerciseId" = $1
+ FROM workout_session ws
+ WHERE wse."sessionId" = ws.id AND wse."exerciseId" = $2 AND ws."userId" = $3`,
+ [saved.id, id, userId],
+ );
+ await manager.query(
+ `UPDATE exercise_record
+ SET "exerciseId" = $1
+ WHERE "exerciseId" = $2 AND "userId" = $3`,
+ [saved.id, id, userId],
+ );
+ await manager.query(
+ `UPDATE workout_exercise we
+ SET "exerciseId" = $1
+ FROM workout w
+ WHERE we."workoutId" = w.id AND we."exerciseId" = $2 AND w."createdById" = $3`,
+ [saved.id, id, userId],
+ );
+ }
+
+ // Soft-delete the global exercise — never hard-delete
+ await manager.softRemove(Exercise, exercise);
+ });
+
+ return { message: 'Global exercise deleted and user data preserved' };
+ }
+
+ async update(id: number, dto: UpdateExerciseDto, userId: number): Promise {
+ const { muscleGroupIds, primaryMuscleGroupIds, name, description, instructions, proTips, mistakes, equipment, ...rest } = dto;
const existing = await this.exerciseRepo.findOne({
- where: { id, createdBy: { id: userId } },
+ where: { id, createdBy: { id: userId }, isGlobal: false },
relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
});
+ if (!existing) throw new NotFoundException('Exercise not found');
- if (!existing) {
- throw new NotFoundException('Exercise not found');
- }
+ Object.assign(existing, rest);
- // If the user changes the name on an exercise that has an i18n key,
- // mark it as custom so we stop using the translation.
- if (
- typeof exerciseData.name === 'string' &&
- existing.i18nKey &&
- exerciseData.name.trim() !== '' &&
- exerciseData.name !== existing.name
- ) {
- existing.isNameCustom = true;
+ if (name !== undefined) {
+ existing.title = { ...existing.title, default: name };
+ }
+ if (description !== undefined) {
+ existing.descriptionI18n = { ...(existing.descriptionI18n ?? {}), default: description };
+ }
+ if (instructions !== undefined) {
+ existing.instructionsI18n = { ...(existing.instructionsI18n ?? {}), default: instructions };
+ }
+ if (proTips !== undefined) {
+ existing.proTipsI18n = { ...(existing.proTipsI18n ?? {}), default: proTips };
+ }
+ if (mistakes !== undefined) {
+ existing.mistakesI18n = { ...(existing.mistakesI18n ?? {}), default: mistakes };
+ }
+ if (equipment !== undefined) {
+ existing.equipmentI18n = { ...(existing.equipmentI18n ?? {}), default: equipment };
}
- Object.assign(existing, exerciseData);
-
- // Check if muscleGroupIds were provided in the DTO
if (muscleGroupIds !== undefined) {
- existing.muscleGroups =
- await this.muscleGroupService.findByIds(muscleGroupIds);
+ existing.muscleGroups = await this.muscleGroupService.findByIds(muscleGroupIds);
}
-
if (primaryMuscleGroupIds !== undefined) {
- existing.primaryMuscleGroups =
- primaryMuscleGroupIds.length > 0
- ? await this.muscleGroupService.findByIds(primaryMuscleGroupIds)
- : [];
+ existing.primaryMuscleGroups = primaryMuscleGroupIds.length
+ ? await this.muscleGroupService.findByIds(primaryMuscleGroupIds)
+ : [];
}
const updated = await this.exerciseRepo.save(existing);
@@ -182,125 +334,201 @@ export class ExerciseService {
async remove(id: number, userId: number): Promise<{ message: string }> {
const exercise = await this.exerciseRepo.findOne({
- where: { id, createdBy: { id: userId } },
+ where: { id, createdBy: { id: userId }, isGlobal: false },
});
-
- if (!exercise) {
- throw new NotFoundException('Exercise not found');
- }
+ if (!exercise) throw new NotFoundException('Exercise not found');
await this.exerciseRepo.softRemove(exercise);
return { message: 'Exercise deleted' };
}
- async updateImage(
- id: number,
- imageUrl: string,
+ async duplicateGlobalExercise(
+ exerciseId: number,
userId: number,
+ transferStats: boolean = false,
): Promise {
+ const source = await this.exerciseRepo.findOne({
+ where: { id: exerciseId, isGlobal: true },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ if (!source) throw new NotFoundException('Global exercise not found');
+
+ return this.dataSource.transaction(async (manager) => {
+ const copy = manager.create(Exercise, {
+ title: source.title,
+ descriptionI18n: source.descriptionI18n,
+ isGlobal: false,
+ createdBy: { id: userId } as any,
+ personalizedFromGlobalId: source.id,
+ personalizedAt: new Date(),
+ image: source.image,
+ exerciseType: source.exerciseType,
+ equipmentI18n: source.equipmentI18n,
+ instructionsI18n: source.instructionsI18n,
+ proTipsI18n: source.proTipsI18n,
+ mistakesI18n: source.mistakesI18n,
+ muscleGroups: source.muscleGroups,
+ primaryMuscleGroups: source.primaryMuscleGroups,
+ });
+ const saved = await manager.save(Exercise, copy);
+
+ if (transferStats) {
+ await manager.query(
+ `UPDATE workout_session_exercise wse
+ SET "exerciseId" = $1
+ FROM workout_session ws
+ WHERE wse."sessionId" = ws.id AND wse."exerciseId" = $2 AND ws."userId" = $3`,
+ [saved.id, exerciseId, userId],
+ );
+ await manager.query(
+ `UPDATE exercise_record
+ SET "exerciseId" = $1
+ WHERE "exerciseId" = $2 AND "userId" = $3`,
+ [saved.id, exerciseId, userId],
+ );
+ await manager.query(
+ `UPDATE workout_exercise we
+ SET "exerciseId" = $1
+ FROM workout w
+ WHERE we."workoutId" = w.id AND we."exerciseId" = $2 AND w."createdById" = $3`,
+ [saved.id, exerciseId, userId],
+ );
+ }
+
+ const full = await manager.findOne(Exercise, {
+ where: { id: saved.id },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ return this.toResponseDto(full!);
+ });
+ }
+
+ async updateImage(id: number, imageUrl: string, userId: number): Promise {
const exercise = await this.exerciseRepo.findOne({
where: { id, createdBy: { id: userId } },
relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
});
+ if (!exercise) throw new NotFoundException('Exercise not found');
- if (!exercise) {
- throw new NotFoundException('Exercise not found');
- }
-
- // Delete old image if exists
if (exercise.image) {
await this.uploadService.deleteImage(exercise.image);
}
-
- // Update image URL
exercise.image = imageUrl;
const updated = await this.exerciseRepo.save(exercise);
-
return this.toResponseDto(updated);
}
- // --- Media management ---
-
- async addMedia(
- exerciseId: number,
- userId: number,
- mediaUrl: string,
- mediaType: string,
- ): Promise {
+ async addMedia(exerciseId: number, userId: number, mediaUrl: string, mediaType: string): Promise {
const exercise = await this.exerciseRepo.findOne({
where: { id: exerciseId, createdBy: { id: userId } },
relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
});
+ if (!exercise) throw new NotFoundException('Exercise not found');
- if (!exercise) {
- throw new NotFoundException('Exercise not found');
- }
+ const maxOrder = exercise.media?.length ? Math.max(...exercise.media.map((m) => m.order)) : -1;
+ const media = this.mediaRepo.create({ type: mediaType as any, url: mediaUrl, order: maxOrder + 1, exercise });
+ await this.mediaRepo.save(media);
- const maxOrder = exercise.media?.length
- ? Math.max(...exercise.media.map((m) => m.order))
- : -1;
+ return this.findOne(exerciseId, userId);
+ }
- const media = this.mediaRepo.create({
- type: mediaType as any,
- url: mediaUrl,
- order: maxOrder + 1,
- exercise,
+ async removeMedia(exerciseId: number, mediaId: number, userId: number): Promise {
+ const exercise = await this.exerciseRepo.findOne({
+ where: { id: exerciseId, createdBy: { id: userId } },
});
+ if (!exercise) throw new NotFoundException('Exercise not found');
- await this.mediaRepo.save(media);
+ const media = await this.mediaRepo.findOne({
+ where: { id: mediaId, exercise: { id: exerciseId } },
+ });
+ if (!media) throw new NotFoundException('Media not found');
+
+ await this.uploadService.deleteImage(media.url);
+ await this.mediaRepo.remove(media);
return this.findOne(exerciseId, userId);
}
- async removeMedia(
- exerciseId: number,
- mediaId: number,
- userId: number,
- ): Promise {
+ async reorderMedia(exerciseId: number, mediaIds: number[], userId: number): Promise {
const exercise = await this.exerciseRepo.findOne({
where: { id: exerciseId, createdBy: { id: userId } },
});
+ if (!exercise) throw new NotFoundException('Exercise not found');
- if (!exercise) {
- throw new NotFoundException('Exercise not found');
+ for (let i = 0; i < mediaIds.length; i++) {
+ await this.mediaRepo.update({ id: mediaIds[i], exercise: { id: exerciseId } }, { order: i });
}
+ return this.findOne(exerciseId, userId);
+ }
+
+ // --- Admin-only: global exercise image/media methods ---
+
+ async updateGlobalImage(id: number, imageUrl: string | null): Promise {
+ const exercise = await this.exerciseRepo.findOne({
+ where: { id, isGlobal: true },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ if (!exercise) throw new NotFoundException('Global exercise not found');
+
+ exercise.image = imageUrl;
+ const updated = await this.exerciseRepo.save(exercise);
+ return this.toResponseDto(updated);
+ }
+
+ async addGlobalMedia(exerciseId: number, mediaUrl: string, mediaType: string): Promise {
+ const exercise = await this.exerciseRepo.findOne({
+ where: { id: exerciseId, isGlobal: true },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ if (!exercise) throw new NotFoundException('Global exercise not found');
+
+ const maxOrder = exercise.media?.length ? Math.max(...exercise.media.map((m) => m.order)) : -1;
+ const media = this.mediaRepo.create({ type: mediaType as any, url: mediaUrl, order: maxOrder + 1, exercise });
+ await this.mediaRepo.save(media);
+
+ const full = await this.exerciseRepo.findOne({
+ where: { id: exerciseId },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ return this.toResponseDto(full!);
+ }
+
+ async removeGlobalMedia(exerciseId: number, mediaId: number): Promise {
+ const exercise = await this.exerciseRepo.findOne({
+ where: { id: exerciseId, isGlobal: true },
+ });
+ if (!exercise) throw new NotFoundException('Global exercise not found');
+
const media = await this.mediaRepo.findOne({
where: { id: mediaId, exercise: { id: exerciseId } },
});
+ if (!media) throw new NotFoundException('Media not found');
- if (!media) {
- throw new NotFoundException('Media not found');
- }
-
- // Delete the file from disk
await this.uploadService.deleteImage(media.url);
-
await this.mediaRepo.remove(media);
- return this.findOne(exerciseId, userId);
+ const full = await this.exerciseRepo.findOne({
+ where: { id: exerciseId },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ return this.toResponseDto(full!);
}
- async reorderMedia(
- exerciseId: number,
- mediaIds: number[],
- userId: number,
- ): Promise {
+ async reorderGlobalMedia(exerciseId: number, mediaIds: number[]): Promise {
const exercise = await this.exerciseRepo.findOne({
- where: { id: exerciseId, createdBy: { id: userId } },
+ where: { id: exerciseId, isGlobal: true },
});
-
- if (!exercise) {
- throw new NotFoundException('Exercise not found');
- }
+ if (!exercise) throw new NotFoundException('Global exercise not found');
for (let i = 0; i < mediaIds.length; i++) {
- await this.mediaRepo.update(
- { id: mediaIds[i], exercise: { id: exerciseId } },
- { order: i },
- );
+ await this.mediaRepo.update({ id: mediaIds[i], exercise: { id: exerciseId } }, { order: i });
}
- return this.findOne(exerciseId, userId);
+ const full = await this.exerciseRepo.findOne({
+ where: { id: exerciseId },
+ relations: ['muscleGroups', 'primaryMuscleGroups', 'media'],
+ });
+ return this.toResponseDto(full!);
}
}
diff --git a/backend/src/v1/exercise/exerciseImage.entity.ts b/backend/src/v1/exercise/exerciseImage.entity.ts
new file mode 100644
index 0000000..6393600
--- /dev/null
+++ b/backend/src/v1/exercise/exerciseImage.entity.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
+
+@Entity()
+export class ExerciseImage {
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @Column()
+ url: string;
+
+ @Column({ type: 'int', nullable: true })
+ fileSize: number | null;
+
+ @CreateDateColumn()
+ createdAt: Date;
+}
diff --git a/backend/src/v1/exercise/exerciseSeed.service.ts b/backend/src/v1/exercise/exerciseSeed.service.ts
index 5be4a21..e2931cb 100644
--- a/backend/src/v1/exercise/exerciseSeed.service.ts
+++ b/backend/src/v1/exercise/exerciseSeed.service.ts
@@ -13,7 +13,7 @@
* .
*/
-import { Injectable } from '@nestjs/common';
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Exercise } from './exercise.entity';
@@ -21,7 +21,9 @@ import { MuscleGroupService } from '../muscleGroup/muscleGroup.service';
import { exercisesToSeed } from '../seed/data/exercises.data';
@Injectable()
-export class ExerciseSeedService {
+export class ExerciseSeedService implements OnModuleInit {
+ private readonly logger = new Logger(ExerciseSeedService.name);
+
constructor(
@InjectRepository(Exercise)
private readonly exerciseRepo: Repository,
@@ -29,12 +31,19 @@ export class ExerciseSeedService {
) {}
/**
- * Seeds the default exercise catalog for a newly registered user.
- * Each exercise is created as a user-owned Exercise with an i18n key
- * for localized display names.
+ * Seeds the global exercise catalog on application start if none exist yet.
+ * Global exercises are shared across all users and managed via the admin panel.
*/
- async seedDefaultExercises(userId: number): Promise {
- // Fetch all muscle groups once and build a name→entity map
+ async onModuleInit(): Promise {
+ await this.seedGlobalExercises();
+ }
+
+ async seedGlobalExercises(): Promise {
+ const existing = await this.exerciseRepo.count({ where: { isGlobal: true } });
+ if (existing > 0) return;
+
+ this.logger.log('No global exercises found – seeding defaults…');
+
const allMuscleGroups = await this.muscleGroupService.findAll();
const mgMap = new Map(allMuscleGroups.map((mg) => [mg.name, mg]));
@@ -45,23 +54,42 @@ export class ExerciseSeedService {
.map((name) => mgMap.get(name))
.filter((mg): mg is NonNullable => !!mg);
+ const primaryMuscleGroups = def.primaryMuscleGroup
+ ? [mgMap.get(def.primaryMuscleGroup)].filter((mg): mg is NonNullable => !!mg)
+ : [];
+
const exercise = this.exerciseRepo.create({
- name: def.defaultName,
- i18nKey: def.i18nKey,
- isNameCustom: false,
- description: def.defaultDescription ?? '',
+ title: {
+ default: def.defaultName,
+ eng: def.defaultName,
+ swe: def.swedenName ?? def.defaultName,
+ },
+ descriptionI18n: {
+ default: def.defaultDescription,
+ eng: def.defaultDescription,
+ swe: def.swedenDescription ?? def.defaultDescription,
+ },
+ isGlobal: true,
+ createdBy: null,
exerciseType: def.exerciseType,
- equipment: def.equipment,
- instructions: def.instructions,
- proTips: def.proTips,
- mistakes: def.mistakes,
+ equipmentI18n: def.equipment ? { default: def.equipment, eng: def.equipment } : undefined,
+ instructionsI18n: def.instructions?.length
+ ? { default: def.instructions, eng: def.instructions }
+ : undefined,
+ proTipsI18n: def.proTips?.length
+ ? { default: def.proTips, eng: def.proTips }
+ : undefined,
+ mistakesI18n: def.mistakes?.length
+ ? { default: def.mistakes, eng: def.mistakes }
+ : undefined,
muscleGroups,
- createdBy: { id: userId } as any,
+ primaryMuscleGroups,
});
exercises.push(exercise);
}
await this.exerciseRepo.save(exercises);
+ this.logger.log(`Seeded ${exercises.length} global exercise(s)`);
}
}
diff --git a/backend/src/v1/guards/superAdmin.guard.ts b/backend/src/v1/guards/superAdmin.guard.ts
new file mode 100644
index 0000000..b935a09
--- /dev/null
+++ b/backend/src/v1/guards/superAdmin.guard.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import {
+ Injectable,
+ CanActivate,
+ ExecutionContext,
+ ForbiddenException,
+} from '@nestjs/common';
+
+@Injectable()
+export class SuperAdminGuard implements CanActivate {
+ canActivate(context: ExecutionContext): boolean {
+ const req = context.switchToHttp().getRequest();
+ if (req.user?.role !== 'superadmin') {
+ throw new ForbiddenException('Superadmin access required');
+ }
+ return true;
+ }
+}
diff --git a/backend/src/v1/migrations/1737300000000-AddCardioAndActivityTracking.ts b/backend/src/v1/migrations/1737300000000-AddCardioAndActivityTracking.ts
index 640c944..94fba84 100644
--- a/backend/src/v1/migrations/1737300000000-AddCardioAndActivityTracking.ts
+++ b/backend/src/v1/migrations/1737300000000-AddCardioAndActivityTracking.ts
@@ -48,28 +48,6 @@ export class AddCardioAndActivityTracking1737300000000
}),
);
- // 2. Add trackingMode and defaultDistance to global_exercise table
- await queryRunner.addColumn(
- 'global_exercise',
- new TableColumn({
- name: 'trackingMode',
- type: 'enum',
- enum: ['strength', 'cardio'],
- default: "'strength'",
- }),
- );
-
- await queryRunner.addColumn(
- 'global_exercise',
- new TableColumn({
- name: 'defaultDistance',
- type: 'decimal',
- precision: 6,
- scale: 2,
- isNullable: true,
- }),
- );
-
// 3. Add distance to workout_exercise table
await queryRunner.addColumn(
'workout_exercise',
@@ -367,10 +345,6 @@ export class AddCardioAndActivityTracking1737300000000
// Remove distance from workout_exercise
await queryRunner.dropColumn('workout_exercise', 'distance');
- // Remove columns from global_exercise
- await queryRunner.dropColumn('global_exercise', 'defaultDistance');
- await queryRunner.dropColumn('global_exercise', 'trackingMode');
-
// Remove columns from exercise
await queryRunner.dropColumn('exercise', 'defaultDistance');
await queryRunner.dropColumn('exercise', 'trackingMode');
diff --git a/backend/src/v1/migrations/1768754686-AddStreakToUser.ts b/backend/src/v1/migrations/1768754686000-AddStreakToUser.ts
similarity index 95%
rename from backend/src/v1/migrations/1768754686-AddStreakToUser.ts
rename to backend/src/v1/migrations/1768754686000-AddStreakToUser.ts
index 5aa3f0b..3f80532 100644
--- a/backend/src/v1/migrations/1768754686-AddStreakToUser.ts
+++ b/backend/src/v1/migrations/1768754686000-AddStreakToUser.ts
@@ -15,7 +15,7 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
-export class AddStreakToUser1768754686 implements MigrationInterface {
+export class AddStreakToUser1768754686000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
// Add weeklyWorkoutGoal column
await queryRunner.addColumn(
diff --git a/backend/src/v1/migrations/1768837747655-RemoveCardioFromExercises.ts b/backend/src/v1/migrations/1768837747655-RemoveCardioFromExercises.ts
index 888692d..70d2ab5 100644
--- a/backend/src/v1/migrations/1768837747655-RemoveCardioFromExercises.ts
+++ b/backend/src/v1/migrations/1768837747655-RemoveCardioFromExercises.ts
@@ -19,13 +19,9 @@ export class RemoveCardioFromExercises1768837747655
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise {
- // Remove trackingMode and defaultDistance from global_exercise table
- await queryRunner.dropColumn('global_exercise', 'defaultDistance');
- await queryRunner.dropColumn('global_exercise', 'trackingMode');
-
- // Remove trackingMode and defaultDistance from exercise table
- await queryRunner.dropColumn('exercise', 'defaultDistance');
- await queryRunner.dropColumn('exercise', 'trackingMode');
+ // Remove trackingMode and defaultDistance from exercise table (if they exist)
+ await queryRunner.query(`ALTER TABLE "exercise" DROP COLUMN IF EXISTS "defaultDistance"`);
+ await queryRunner.query(`ALTER TABLE "exercise" DROP COLUMN IF EXISTS "trackingMode"`);
}
public async down(queryRunner: QueryRunner): Promise {
@@ -40,15 +36,5 @@ export class RemoveCardioFromExercises1768837747655
ADD "defaultDistance" numeric(6,2)
`);
- // Add back trackingMode and defaultDistance to global_exercise table
- await queryRunner.query(`
- ALTER TABLE "global_exercise"
- ADD "trackingMode" character varying NOT NULL DEFAULT 'strength'
- `);
-
- await queryRunner.query(`
- ALTER TABLE "global_exercise"
- ADD "defaultDistance" numeric(6,2)
- `);
}
}
diff --git a/backend/src/v1/migrations/1769000000000-MigrateEquipmentToI18n.ts b/backend/src/v1/migrations/1769000000000-MigrateEquipmentToI18n.ts
new file mode 100644
index 0000000..3ef04fe
--- /dev/null
+++ b/backend/src/v1/migrations/1769000000000-MigrateEquipmentToI18n.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class MigrateEquipmentToI18n1769000000000 implements MigrationInterface {
+ name = 'MigrateEquipmentToI18n1769000000000';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "exercise" RENAME COLUMN "equipment" TO "equipment_i18n"`,
+ );
+ // Convert existing string[] to { default: string[] }
+ await queryRunner.query(`
+ UPDATE "exercise"
+ SET "equipment_i18n" = jsonb_build_object('default', "equipment_i18n")
+ WHERE "equipment_i18n" IS NOT NULL
+ AND "equipment_i18n"::text != 'null'
+ AND jsonb_typeof("equipment_i18n") = 'array'
+ `);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // Convert { default: string[] } back to string[]
+ await queryRunner.query(`
+ UPDATE "exercise"
+ SET "equipment_i18n" = "equipment_i18n"->'default'
+ WHERE "equipment_i18n" IS NOT NULL
+ AND jsonb_typeof("equipment_i18n") = 'object'
+ AND "equipment_i18n" ? 'default'
+ `);
+ await queryRunner.query(
+ `ALTER TABLE "exercise" RENAME COLUMN "equipment_i18n" TO "equipment"`,
+ );
+ }
+}
diff --git a/backend/src/v1/migrations/1769000001000-AddExerciseImageTable.ts b/backend/src/v1/migrations/1769000001000-AddExerciseImageTable.ts
new file mode 100644
index 0000000..07ae131
--- /dev/null
+++ b/backend/src/v1/migrations/1769000001000-AddExerciseImageTable.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddExerciseImageTable1769000001000 implements MigrationInterface {
+ name = 'AddExerciseImageTable1769000001000';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`
+ CREATE TABLE "exercise_image" (
+ "id" SERIAL NOT NULL,
+ "url" character varying NOT NULL,
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+ CONSTRAINT "PK_exercise_image" PRIMARY KEY ("id")
+ )
+ `);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP TABLE "exercise_image"`);
+ }
+}
diff --git a/backend/src/v1/migrations/1769000002000-AddExerciseImageFileSize.ts b/backend/src/v1/migrations/1769000002000-AddExerciseImageFileSize.ts
new file mode 100644
index 0000000..73ea029
--- /dev/null
+++ b/backend/src/v1/migrations/1769000002000-AddExerciseImageFileSize.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddExerciseImageFileSize1769000002000
+ implements MigrationInterface
+{
+ name = 'AddExerciseImageFileSize1769000002000';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "exercise_image" ADD COLUMN "fileSize" integer NULL`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "exercise_image" DROP COLUMN "fileSize"`,
+ );
+ }
+}
diff --git a/backend/src/v1/migrations/1775500000000-GlobalExercisesAndTranslations.ts b/backend/src/v1/migrations/1775500000000-GlobalExercisesAndTranslations.ts
new file mode 100644
index 0000000..08a7e7d
--- /dev/null
+++ b/backend/src/v1/migrations/1775500000000-GlobalExercisesAndTranslations.ts
@@ -0,0 +1,267 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class GlobalExercisesAndTranslations1775500000000
+ implements MigrationInterface
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ // ── EXERCISE TABLE ────────────────────────────────────────────────────────
+
+ // Allow global exercises (no owner)
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ALTER COLUMN "createdById" DROP NOT NULL`,
+ );
+
+ // Global flag — all existing exercises stay false (user-owned)
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "isGlobal" boolean NOT NULL DEFAULT false`,
+ );
+
+ // Personalization tracking — set when user duplicates or inherits a deleted global
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "personalizedFromGlobalId" integer NULL`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "personalizedAt" timestamptz NULL`,
+ );
+
+ // title: JSONB translation object — migrated from existing name string
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "title" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "exercise" SET "title" = jsonb_build_object('default', "name")`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ALTER COLUMN "title" SET NOT NULL`,
+ );
+
+ // descriptionI18n: JSONB — migrated from existing description string
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "descriptionI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "exercise" SET "descriptionI18n" = jsonb_build_object('default', "description")
+ WHERE "description" IS NOT NULL AND "description" != ''`,
+ );
+
+ // instructionsI18n: JSONB — migrated from existing jsonb array
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "instructionsI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "exercise" SET "instructionsI18n" = jsonb_build_object('default', "instructions")
+ WHERE "instructions" IS NOT NULL`,
+ );
+
+ // proTipsI18n: JSONB — migrated from existing jsonb array
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "proTipsI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "exercise" SET "proTipsI18n" = jsonb_build_object('default', "proTips")
+ WHERE "proTips" IS NOT NULL`,
+ );
+
+ // mistakesI18n: JSONB — migrated from existing jsonb array
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "mistakesI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "exercise" SET "mistakesI18n" = jsonb_build_object('default', "mistakes")
+ WHERE "mistakes" IS NOT NULL`,
+ );
+
+ // ── ACTIVITY TABLE ────────────────────────────────────────────────────────
+
+ // Drop the composite unique constraint (name, createdById) — replaced by partial indexes below
+ await queryRunner.query(`
+ DO $$
+ DECLARE
+ cname text;
+ BEGIN
+ SELECT c.conname INTO cname
+ FROM pg_constraint c
+ JOIN pg_class t ON t.oid = c.conrelid
+ WHERE t.relname = 'activity'
+ AND c.contype = 'u'
+ AND EXISTS (
+ SELECT 1 FROM pg_attribute a
+ WHERE a.attrelid = t.oid AND a.attname = 'name' AND a.attnum = ANY(c.conkey)
+ )
+ AND EXISTS (
+ SELECT 1 FROM pg_attribute a
+ WHERE a.attrelid = t.oid AND a.attname = 'createdById' AND a.attnum = ANY(c.conkey)
+ );
+ IF cname IS NOT NULL THEN
+ EXECUTE 'ALTER TABLE activity DROP CONSTRAINT "' || cname || '"';
+ END IF;
+ END $$;
+ `);
+
+ // Allow global activities (no owner)
+ await queryRunner.query(
+ `ALTER TABLE "activity" ALTER COLUMN "createdById" DROP NOT NULL`,
+ );
+
+ // Global flag + personalization tracking
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "isGlobal" boolean NOT NULL DEFAULT false`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "personalizedFromGlobalId" integer NULL`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "personalizedAt" timestamptz NULL`,
+ );
+
+ // title: JSONB — migrated from existing name string
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "title" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "activity" SET "title" = jsonb_build_object('default', "name")`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" ALTER COLUMN "title" SET NOT NULL`,
+ );
+
+ // descriptionI18n: JSONB — migrated from existing description string
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "descriptionI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "activity" SET "descriptionI18n" = jsonb_build_object('default', "description")
+ WHERE "description" IS NOT NULL AND "description" != ''`,
+ );
+
+ // Partial unique indexes replacing the old composite constraint
+ await queryRunner.query(`
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_activity_name_user"
+ ON "activity" ("name", "createdById")
+ WHERE "createdById" IS NOT NULL
+ `);
+ await queryRunner.query(`
+ CREATE UNIQUE INDEX IF NOT EXISTS "UQ_activity_name_global"
+ ON "activity" ("name")
+ WHERE "createdById" IS NULL
+ `);
+
+ // ── USER TABLE ────────────────────────────────────────────────────────────
+
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "language" varchar(10) NOT NULL DEFAULT 'default'`,
+ );
+
+ // ── MUSCLE_GROUP TABLE ────────────────────────────────────────────────────
+
+ // nameI18n: JSONB display name — migrated from existing name string (name stays as internal key)
+ await queryRunner.query(
+ `ALTER TABLE "muscle_group" ADD COLUMN IF NOT EXISTS "nameI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "muscle_group" SET "nameI18n" = jsonb_build_object('default', "name")`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "muscle_group" ALTER COLUMN "nameI18n" SET NOT NULL`,
+ );
+
+ // descriptionI18n: JSONB — migrated from existing description string
+ await queryRunner.query(
+ `ALTER TABLE "muscle_group" ADD COLUMN IF NOT EXISTS "descriptionI18n" jsonb`,
+ );
+ await queryRunner.query(
+ `UPDATE "muscle_group" SET "descriptionI18n" = jsonb_build_object('default', "description")
+ WHERE "description" IS NOT NULL`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // ── MUSCLE_GROUP ──────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `ALTER TABLE "muscle_group" DROP COLUMN IF EXISTS "descriptionI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "muscle_group" DROP COLUMN IF EXISTS "nameI18n"`,
+ );
+
+ // ── USER ──────────────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `ALTER TABLE "user" DROP COLUMN IF EXISTS "language"`,
+ );
+
+ // ── ACTIVITY ──────────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `DROP INDEX IF EXISTS "UQ_activity_name_global"`,
+ );
+ await queryRunner.query(
+ `DROP INDEX IF EXISTS "UQ_activity_name_user"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "descriptionI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "title"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "personalizedAt"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "personalizedFromGlobalId"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "isGlobal"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" ALTER COLUMN "createdById" SET NOT NULL`,
+ );
+ // Restore original unique constraint
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD CONSTRAINT "UQ_activity_name_createdBy"
+ UNIQUE ("name", "createdById")`,
+ );
+
+ // ── EXERCISE ──────────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "mistakesI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "proTipsI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "instructionsI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "descriptionI18n"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "title"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "personalizedAt"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "personalizedFromGlobalId"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "isGlobal"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ALTER COLUMN "createdById" SET NOT NULL`,
+ );
+ }
+}
diff --git a/backend/src/v1/migrations/1775600000000-DropLegacyColumns.ts b/backend/src/v1/migrations/1775600000000-DropLegacyColumns.ts
new file mode 100644
index 0000000..9f5636e
--- /dev/null
+++ b/backend/src/v1/migrations/1775600000000-DropLegacyColumns.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+/**
+ * Phase 11 cleanup: drops legacy string columns that were superseded by JSONB
+ * i18n columns in 1775500000000-GlobalExercisesAndTranslations.
+ *
+ * Safe to run once the new JSONB columns have been in production and confirmed
+ * stable. The down() migration cannot recover the string data once dropped, so
+ * run a DB backup before applying.
+ */
+export class DropLegacyColumns1775600000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ // ── EXERCISE TABLE ────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "name"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "description"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "instructions"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "proTips"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "mistakes"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "i18nKey"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" DROP COLUMN IF EXISTS "isNameCustom"`,
+ );
+
+ // ── ACTIVITY TABLE ────────────────────────────────────────────────────────
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "name"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" DROP COLUMN IF EXISTS "description"`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ // Restore exercise legacy columns (data is lost — columns will be empty)
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "name" varchar`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "description" text`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "instructions" jsonb`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "proTips" jsonb`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "mistakes" jsonb`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "i18nKey" varchar`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "exercise" ADD COLUMN IF NOT EXISTS "isNameCustom" boolean NOT NULL DEFAULT false`,
+ );
+
+ // Restore activity legacy columns (data is lost)
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "name" varchar`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "activity" ADD COLUMN IF NOT EXISTS "description" text`,
+ );
+ }
+}
diff --git a/backend/src/v1/migrations/1776000000000-AddRoleToUser.ts b/backend/src/v1/migrations/1776000000000-AddRoleToUser.ts
new file mode 100644
index 0000000..6582ece
--- /dev/null
+++ b/backend/src/v1/migrations/1776000000000-AddRoleToUser.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class AddRoleToUser1776000000000 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumn(
+ 'user',
+ new TableColumn({
+ name: 'role',
+ type: 'varchar',
+ length: '20',
+ default: "'user'",
+ }),
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropColumn('user', 'role');
+ }
+}
diff --git a/backend/src/v1/muscleGroup/dto/createMuscleGroupAdmin.dto.ts b/backend/src/v1/muscleGroup/dto/createMuscleGroupAdmin.dto.ts
new file mode 100644
index 0000000..4d0120c
--- /dev/null
+++ b/backend/src/v1/muscleGroup/dto/createMuscleGroupAdmin.dto.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { IsNotEmpty, IsOptional, IsObject, IsString, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { ApiProperty } from '@nestjs/swagger';
+import { I18nStringDto } from '../../exercise/dto/createGlobalExercise.dto';
+
+export class CreateMuscleGroupAdminDto {
+ @ApiProperty({ example: 'chest', description: 'Unique internal key, e.g. "chest", "lowerBack"' })
+ @IsString()
+ @IsNotEmpty()
+ name: string;
+
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ nameI18n?: I18nStringDto;
+
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ descriptionI18n?: I18nStringDto;
+}
diff --git a/backend/src/v1/muscleGroup/dto/muscleGroupResponse.dto.ts b/backend/src/v1/muscleGroup/dto/muscleGroupResponse.dto.ts
index 2ed464e..1eb78b4 100644
--- a/backend/src/v1/muscleGroup/dto/muscleGroupResponse.dto.ts
+++ b/backend/src/v1/muscleGroup/dto/muscleGroupResponse.dto.ts
@@ -14,16 +14,20 @@
*/
import { ApiProperty } from '@nestjs/swagger';
+import { I18nString } from '../../common/types/i18n.types';
export class MuscleGroupResponseDto {
@ApiProperty()
id: number;
- @ApiProperty()
+ @ApiProperty({ description: 'Internal key identifier, e.g. muscleGroups.chest' })
name: string;
+ @ApiProperty()
+ nameI18n: I18nString;
+
@ApiProperty({ required: false })
- description?: string;
+ descriptionI18n?: I18nString;
@ApiProperty()
createdAt: Date;
diff --git a/backend/src/v1/muscleGroup/dto/updateMuscleGroupAdmin.dto.ts b/backend/src/v1/muscleGroup/dto/updateMuscleGroupAdmin.dto.ts
new file mode 100644
index 0000000..fa2d2f8
--- /dev/null
+++ b/backend/src/v1/muscleGroup/dto/updateMuscleGroupAdmin.dto.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 FalkenDev
+ *
+ * This file is part of Grindify.
+ *
+ * Grindify is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Grindify. If not, see
+ * .
+ */
+
+import { IsOptional, IsObject, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { ApiProperty } from '@nestjs/swagger';
+import { I18nStringDto } from '../../exercise/dto/createGlobalExercise.dto';
+
+export class UpdateMuscleGroupAdminDto {
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ nameI18n?: I18nStringDto;
+
+ @ApiProperty({ type: I18nStringDto, required: false })
+ @IsOptional()
+ @IsObject()
+ @ValidateNested()
+ @Type(() => I18nStringDto)
+ descriptionI18n?: I18nStringDto;
+}
diff --git a/backend/src/v1/muscleGroup/muscleGroup.entity.ts b/backend/src/v1/muscleGroup/muscleGroup.entity.ts
index f77887a..ffed863 100644
--- a/backend/src/v1/muscleGroup/muscleGroup.entity.ts
+++ b/backend/src/v1/muscleGroup/muscleGroup.entity.ts
@@ -22,17 +22,22 @@ import {
ManyToMany,
} from 'typeorm';
import { Exercise } from '../exercise/exercise.entity';
+import { I18nString } from '../common/types/i18n.types';
@Entity()
export class MuscleGroup {
@PrimaryGeneratedColumn()
id: number;
+ // Internal identifier key, e.g. 'chest', 'muscleGroups.back' — not shown directly to users
@Column({ unique: true })
name: string;
- @Column({ nullable: true })
- description: string;
+ @Column({ name: 'nameI18n', type: 'jsonb' })
+ nameI18n: I18nString;
+
+ @Column({ name: 'descriptionI18n', type: 'jsonb', nullable: true })
+ descriptionI18n?: I18nString;
@ManyToMany(() => Exercise, (exercise) => exercise.muscleGroups)
exercises: Exercise[];
diff --git a/backend/src/v1/muscleGroup/muscleGroup.service.ts b/backend/src/v1/muscleGroup/muscleGroup.service.ts
index 4bf26dd..3a754cc 100644
--- a/backend/src/v1/muscleGroup/muscleGroup.service.ts
+++ b/backend/src/v1/muscleGroup/muscleGroup.service.ts
@@ -47,6 +47,19 @@ export class MuscleGroupService implements OnModuleInit {
this.logger.log('No muscle groups found – seeding defaults…');
await this.muscleGroupRepo.save(muscleGroupsToSeed);
this.logger.log(`Seeded ${muscleGroupsToSeed.length} muscle group(s)`);
+ return;
+ }
+ // Backfill nameI18n for muscle groups that don't have it yet (migration may have added the column)
+ const missing = await this.muscleGroupRepo
+ .createQueryBuilder('mg')
+ .where('mg.nameI18n IS NULL')
+ .getMany();
+ if (missing.length > 0) {
+ for (const mg of missing) {
+ mg.nameI18n = { default: mg.name };
+ }
+ await this.muscleGroupRepo.save(missing);
+ this.logger.log(`Backfilled nameI18n for ${missing.length} muscle group(s)`);
}
}
diff --git a/backend/src/v1/seed/data/activities.data.ts b/backend/src/v1/seed/data/activities.data.ts
index db07c46..f8451f8 100644
--- a/backend/src/v1/seed/data/activities.data.ts
+++ b/backend/src/v1/seed/data/activities.data.ts
@@ -15,10 +15,24 @@
import { ActivityIcon } from '../../activity/activity.entity';
-export const activitiesToSeed = [
+export interface ActivitySeedDef {
+ name: string;
+ swedenName?: string;
+ description?: string;
+ swedenDescription?: string;
+ icon: ActivityIcon;
+ trackDistance: boolean;
+ trackPace: boolean;
+ trackElevation: boolean;
+ trackCalories: boolean;
+}
+
+export const activitiesToSeed: ActivitySeedDef[] = [
{
name: 'Running',
+ swedenName: 'Löpning',
description: 'Outdoor running sessions',
+ swedenDescription: 'Utomhuslöpning',
icon: ActivityIcon.RUNNING,
trackDistance: true,
trackPace: true,
@@ -27,7 +41,9 @@ export const activitiesToSeed = [
},
{
name: 'Walking',
+ swedenName: 'Promenad',
description: 'Walking and hiking',
+ swedenDescription: 'Promenader och vandring',
icon: ActivityIcon.WALKING,
trackDistance: true,
trackPace: true,
@@ -36,7 +52,9 @@ export const activitiesToSeed = [
},
{
name: 'Cycling',
+ swedenName: 'Cykling',
description: 'Road and mountain biking',
+ swedenDescription: 'Landsvägscykling och mountainbike',
icon: ActivityIcon.CYCLING,
trackDistance: true,
trackPace: false,
@@ -45,7 +63,9 @@ export const activitiesToSeed = [
},
{
name: 'Floorball',
+ swedenName: 'Innebandy',
description: 'Floorball training and matches',
+ swedenDescription: 'Innebandyträning och matcher',
icon: ActivityIcon.OTHER,
trackDistance: false,
trackPace: false,
@@ -54,7 +74,9 @@ export const activitiesToSeed = [
},
{
name: 'Football',
+ swedenName: 'Fotboll',
description: 'Football training and matches',
+ swedenDescription: 'Fotbollsträning och matcher',
icon: ActivityIcon.FOOTBALL,
trackDistance: false,
trackPace: false,
@@ -63,7 +85,9 @@ export const activitiesToSeed = [
},
{
name: 'Swimming',
+ swedenName: 'Simning',
description: 'Swimming sessions',
+ swedenDescription: 'Simträning',
icon: ActivityIcon.SWIMMING,
trackDistance: true,
trackPace: false,
@@ -72,7 +96,9 @@ export const activitiesToSeed = [
},
{
name: 'Kayaking',
+ swedenName: 'Kajakpaddling',
description: 'Kayaking and canoeing',
+ swedenDescription: 'Kajak och kanot',
icon: ActivityIcon.KAYAKING,
trackDistance: true,
trackPace: false,
diff --git a/backend/src/v1/seed/data/exercises.data.ts b/backend/src/v1/seed/data/exercises.data.ts
index f96bb17..d497d96 100644
--- a/backend/src/v1/seed/data/exercises.data.ts
+++ b/backend/src/v1/seed/data/exercises.data.ts
@@ -16,9 +16,11 @@
import { ExerciseType } from '../../exercise/exercise.entity';
export interface ExerciseSeedDef {
- i18nKey: string;
+ i18nKey?: string;
defaultName: string;
defaultDescription: string;
+ swedenName?: string;
+ swedenDescription?: string;
muscleGroups: string[];
primaryMuscleGroup?: string;
exerciseType?: ExerciseType;
@@ -26,6 +28,9 @@ export interface ExerciseSeedDef {
instructions?: string[];
proTips?: string[];
mistakes?: string[];
+ swedenInstructions?: string[];
+ swedenProTips?: string[];
+ swedenMistakes?: string[];
}
export const exercisesToSeed: ExerciseSeedDef[] = [
diff --git a/backend/src/v1/seed/data/muscleGroups.data.ts b/backend/src/v1/seed/data/muscleGroups.data.ts
index 8a3580d..bd0e2e0 100644
--- a/backend/src/v1/seed/data/muscleGroups.data.ts
+++ b/backend/src/v1/seed/data/muscleGroups.data.ts
@@ -14,22 +14,22 @@
*/
export const muscleGroupsToSeed = [
- { name: 'chest' },
- { name: 'back' },
- { name: 'shoulders' },
- { name: 'biceps' },
- { name: 'triceps' },
- { name: 'legs' },
- { name: 'abs' },
- { name: 'forearms' },
- { name: 'glutes' },
- { name: 'hamstrings' },
- { name: 'quads' },
- { name: 'calves' },
- { name: 'rearDelts' },
- { name: 'core' },
- { name: 'traps' },
- { name: 'lowerBack' },
- { name: 'upperChest' },
- { name: 'hipFlexors' },
+ { name: 'chest', nameI18n: { default: 'Chest', eng: 'Chest', swe: 'Bröst' } },
+ { name: 'back', nameI18n: { default: 'Back', eng: 'Back', swe: 'Rygg' } },
+ { name: 'shoulders', nameI18n: { default: 'Shoulders', eng: 'Shoulders', swe: 'Axlar' } },
+ { name: 'biceps', nameI18n: { default: 'Biceps', eng: 'Biceps', swe: 'Biceps' } },
+ { name: 'triceps', nameI18n: { default: 'Triceps', eng: 'Triceps', swe: 'Triceps' } },
+ { name: 'legs', nameI18n: { default: 'Legs', eng: 'Legs', swe: 'Ben' } },
+ { name: 'abs', nameI18n: { default: 'Abs', eng: 'Abs', swe: 'Mage' } },
+ { name: 'forearms', nameI18n: { default: 'Forearms', eng: 'Forearms', swe: 'Underarmar' } },
+ { name: 'glutes', nameI18n: { default: 'Glutes', eng: 'Glutes', swe: 'Gluteus' } },
+ { name: 'hamstrings', nameI18n: { default: 'Hamstrings', eng: 'Hamstrings', swe: 'Baksida lår' } },
+ { name: 'quads', nameI18n: { default: 'Quads', eng: 'Quads', swe: 'Framsida lår' } },
+ { name: 'calves', nameI18n: { default: 'Calves', eng: 'Calves', swe: 'Vader' } },
+ { name: 'rearDelts', nameI18n: { default: 'Rear Delts', eng: 'Rear Delts', swe: 'Bakre deltoid' } },
+ { name: 'core', nameI18n: { default: 'Core', eng: 'Core', swe: 'Core' } },
+ { name: 'traps', nameI18n: { default: 'Traps', eng: 'Traps', swe: 'Trapezius' } },
+ { name: 'lowerBack', nameI18n: { default: 'Lower Back', eng: 'Lower Back', swe: 'Ländrygg' } },
+ { name: 'upperChest', nameI18n: { default: 'Upper Chest', eng: 'Upper Chest', swe: 'Övre bröst' } },
+ { name: 'hipFlexors', nameI18n: { default: 'Hip Flexors', eng: 'Hip Flexors', swe: 'Höftböjare' } },
];
diff --git a/backend/src/v1/seed/seeders/exercises.seeder.ts b/backend/src/v1/seed/seeders/exercises.seeder.ts
index 31c7e4d..f3eb7af 100644
--- a/backend/src/v1/seed/seeders/exercises.seeder.ts
+++ b/backend/src/v1/seed/seeders/exercises.seeder.ts
@@ -28,15 +28,13 @@ export async function seedUserExercises(
for (const ex of exercisesToSeed) {
const exercise = exerciseRepo.create({
- name: ex.defaultName,
- i18nKey: ex.i18nKey,
- isNameCustom: false,
- description: ex.defaultDescription ?? '',
+ title: { default: ex.defaultName },
+ descriptionI18n: { default: ex.defaultDescription ?? '' },
exerciseType: ex.exerciseType,
- equipment: ex.equipment,
- instructions: ex.instructions,
- proTips: ex.proTips,
- mistakes: ex.mistakes,
+ equipmentI18n: ex.equipment ? { default: ex.equipment } : undefined,
+ instructionsI18n: ex.instructions ? { default: ex.instructions } : undefined,
+ proTipsI18n: ex.proTips ? { default: ex.proTips } : undefined,
+ mistakesI18n: ex.mistakes ? { default: ex.mistakes } : undefined,
createdBy: user,
muscleGroups: ex.muscleGroups
.map((name) => mgMap.get(name))
diff --git a/backend/src/v1/statistics/statistics.service.ts b/backend/src/v1/statistics/statistics.service.ts
index 294df5c..0bdc358 100644
--- a/backend/src/v1/statistics/statistics.service.ts
+++ b/backend/src/v1/statistics/statistics.service.ts
@@ -313,7 +313,7 @@ export class StatisticsService {
for (const ex of s.exercises ?? []) {
if (ex.exercise) {
const curr = exerciseCount.get(ex.exercise.id) ?? {
- name: ex.exercise.name,
+ name: ex.exercise.title?.default ?? '',
count: 0,
};
curr.count++;
@@ -388,7 +388,7 @@ export class StatisticsService {
.map((mg) => ({ name: mg.name, volume: Math.round(mg.volume) })),
recentPRs: recentPRs.map((r) => ({
exerciseId: r.exercise?.id,
- exerciseName: r.exercise?.name,
+ exerciseName: r.exercise?.title?.default ?? '',
recordType: r.recordType,
value: Number(r.value),
date: r.achievedAt,
diff --git a/backend/src/v1/strategies/Jwt.strategy.ts b/backend/src/v1/strategies/Jwt.strategy.ts
index edfa499..b8e91e3 100644
--- a/backend/src/v1/strategies/Jwt.strategy.ts
+++ b/backend/src/v1/strategies/Jwt.strategy.ts
@@ -50,6 +50,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
if (!user) {
throw new UnauthorizedException('User not found');
}
- return { id: user.id, email: user.email };
+ return { id: user.id, email: user.email, role: user.role };
}
}
diff --git a/backend/src/v1/upload/upload.service.ts b/backend/src/v1/upload/upload.service.ts
index 1243389..6c32b75 100644
--- a/backend/src/v1/upload/upload.service.ts
+++ b/backend/src/v1/upload/upload.service.ts
@@ -50,12 +50,14 @@ export class UploadService {
* Process and optimize an exercise image
* Optimized for mobile - smaller dimensions and size
*/
- async processExerciseImage(file: Express.Multer.File): Promise {
+ async processExerciseImage(
+ file: Express.Multer.File,
+ ): Promise<{ url: string; fileSize: number }> {
const filename = `${randomBytes(16).toString('hex')}.webp`;
const filepath = path.join(this.exercisesDir, filename);
// Optimize for mobile: max 800px width, high compression
- await sharp(file.buffer)
+ const info = await sharp(file.buffer)
.rotate()
.resize(800, 800, {
fit: 'inside',
@@ -64,7 +66,7 @@ export class UploadService {
.webp({ quality: 80 })
.toFile(filepath);
- return `/uploads/exercises/${filename}`;
+ return { url: `/uploads/exercises/${filename}`, fileSize: info.size };
}
/**
@@ -116,6 +118,30 @@ export class UploadService {
return { url: `/uploads/exercises/media/${filename}`, type: 'image' };
}
+ async readFileAsBuffer(relativeUrl: string): Promise {
+ if (!relativeUrl) return null;
+ try {
+ const filepath = path.join(process.cwd(), relativeUrl);
+ return await fs.readFile(filepath);
+ } catch {
+ return null;
+ }
+ }
+
+ async writeExerciseImageFromBuffer(buffer: Buffer, ext: string): Promise {
+ const filename = `${randomBytes(16).toString('hex')}.${ext}`;
+ const filepath = path.join(this.exercisesDir, filename);
+ await fs.writeFile(filepath, buffer);
+ return `/uploads/exercises/${filename}`;
+ }
+
+ async writeExerciseMediaFromBuffer(buffer: Buffer, ext: string): Promise {
+ const filename = `${randomBytes(16).toString('hex')}.${ext}`;
+ const filepath = path.join(this.mediaDir, filename);
+ await fs.writeFile(filepath, buffer);
+ return `/uploads/exercises/media/${filename}`;
+ }
+
/**
* Delete an image file from the filesystem
*/
diff --git a/backend/src/v1/user/dto/UpdateUserPreferences.dto.ts b/backend/src/v1/user/dto/UpdateUserPreferences.dto.ts
index bfd0189..b7a5479 100644
--- a/backend/src/v1/user/dto/UpdateUserPreferences.dto.ts
+++ b/backend/src/v1/user/dto/UpdateUserPreferences.dto.ts
@@ -19,6 +19,7 @@ import {
IsNumber,
IsDateString,
IsBoolean,
+ IsIn,
ValidateIf,
} from 'class-validator';
@@ -81,4 +82,8 @@ export class UpdateUserPreferencesDto {
@IsOptional()
@IsNumber()
startWeight?: number;
+
+ @IsOptional()
+ @IsIn(['default', 'eng', 'swe'])
+ language?: 'default' | 'eng' | 'swe';
}
diff --git a/backend/src/v1/user/user.entity.ts b/backend/src/v1/user/user.entity.ts
index 5e0ce83..0e101f9 100644
--- a/backend/src/v1/user/user.entity.ts
+++ b/backend/src/v1/user/user.entity.ts
@@ -114,6 +114,12 @@ export class User {
@Column({ default: false })
emailVerified: boolean;
+ @Column({ type: 'varchar', length: 20, default: 'user' })
+ role: 'user' | 'superadmin';
+
+ @Column({ type: 'varchar', length: 10, default: 'default' })
+ language: 'default' | 'eng' | 'swe';
+
@Column({ nullable: true, select: false })
emailVerificationToken: string;
diff --git a/backend/src/v1/user/user.service.ts b/backend/src/v1/user/user.service.ts
index 9203f0e..c3ca263 100644
--- a/backend/src/v1/user/user.service.ts
+++ b/backend/src/v1/user/user.service.ts
@@ -623,6 +623,7 @@ export class UserService {
if (dto.weightGoalType !== undefined)
user.weightGoalType = dto.weightGoalType;
if (dto.startWeight !== undefined) user.startWeight = dto.startWeight;
+ if (dto.language !== undefined) user.language = dto.language;
const updated = await this.userRepo.save(user);
return new UserWithoutPasswordDto(updated);
diff --git a/backend/src/v1/workout/dto/workoutExerciseSnapshot.dto.ts b/backend/src/v1/workout/dto/workoutExerciseSnapshot.dto.ts
index 34f662f..666e82b 100644
--- a/backend/src/v1/workout/dto/workoutExerciseSnapshot.dto.ts
+++ b/backend/src/v1/workout/dto/workoutExerciseSnapshot.dto.ts
@@ -15,6 +15,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { MuscleGroupResponseDto } from 'src/v1/muscleGroup/dto/muscleGroupResponse.dto';
+import { I18nString } from 'src/v1/common/types/i18n.types';
export class PrimaryMuscleGroupSnapshotDto {
@ApiProperty()
@@ -29,10 +30,19 @@ export class ExerciseSnapshotDto {
id!: number;
@ApiProperty()
- name!: string;
+ title!: I18nString;
+
+ @ApiProperty({ required: false, nullable: true })
+ description?: I18nString | null;
+
+ @ApiProperty()
+ isGlobal!: boolean;
+
+ @ApiProperty({ required: false, nullable: true })
+ personalizedFromGlobalId?: number | null;
@ApiProperty({ required: false })
- description?: string;
+ deletedAt?: Date;
@ApiProperty({ type: [PrimaryMuscleGroupSnapshotDto] })
primaryMuscleGroups!: PrimaryMuscleGroupSnapshotDto[];
diff --git a/backend/src/v1/workout/workout.service.ts b/backend/src/v1/workout/workout.service.ts
index 933a7e6..39ddd35 100644
--- a/backend/src/v1/workout/workout.service.ts
+++ b/backend/src/v1/workout/workout.service.ts
@@ -341,7 +341,8 @@ export class WorkoutService {
workout.targetMuscleGroups?.map((mg) => ({
id: mg.id,
name: mg.name,
- description: mg.description,
+ nameI18n: mg.nameI18n,
+ descriptionI18n: mg.descriptionI18n,
createdAt: mg.createdAt,
updatedAt: mg.updatedAt,
})) ?? [],
@@ -357,8 +358,10 @@ export class WorkoutService {
pauseSeconds: e.pauseSeconds,
exercise: {
id: e.exercise.id,
- name: e.exercise.name,
- description: e.exercise.description,
+ title: e.exercise.title,
+ description: e.exercise.descriptionI18n ?? null,
+ isGlobal: e.exercise.isGlobal,
+ personalizedFromGlobalId: e.exercise.personalizedFromGlobalId ?? null,
primaryMuscleGroups:
e.exercise.primaryMuscleGroups?.map((mg) => ({
id: mg.id,
@@ -369,6 +372,8 @@ export class WorkoutService {
e.exercise.muscleGroups?.map((mg) => ({
id: mg.id,
name: mg.name,
+ nameI18n: mg.nameI18n,
+ descriptionI18n: mg.descriptionI18n,
createdAt: mg.createdAt,
updatedAt: mg.updatedAt,
})) ?? [],
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 94756d4..1f38a66 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -53,6 +53,22 @@ services:
env_file:
- .env
+ adminpanel:
+ build:
+ context: .
+ dockerfile: Dockerfile.adminpanel
+ target: production
+ args:
+ VITE_API_URL: ${VITE_API_URL}
+ container_name: grindify_adminpanel
+ restart: unless-stopped
+ depends_on:
+ - backend
+ ports:
+ - "${ADMINPANEL_PORT:-3001}:3001"
+ env_file:
+ - .env
+
volumes:
postgres_data:
uploads_data:
diff --git a/docker-compose.yml b/docker-compose.yml
index 9218ec3..4887528 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -68,6 +68,29 @@ services:
path: ./frontend
target: /app
+ adminpanel:
+ build:
+ context: .
+ dockerfile: Dockerfile.adminpanel
+ target: development
+ args:
+ VITE_API_URL: ${VITE_API_URL}
+ container_name: grindify_adminpanel
+ ports:
+ - "${ADMINPANEL_PORT:-3001}:3001"
+ volumes:
+ - ./adminpanel:/app
+ - /app/node_modules
+ depends_on:
+ - backend
+ env_file:
+ - .env
+ develop:
+ watch:
+ - action: sync
+ path: ./adminpanel
+ target: /app
+
volumes:
postgres_data:
uploads_data:
diff --git a/frontend/env.d.ts b/frontend/env.d.ts
index debc1ad..1a178cc 100644
--- a/frontend/env.d.ts
+++ b/frontend/env.d.ts
@@ -6,7 +6,7 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
- const component: DefineComponent, Record, any>
+ const component: DefineComponent, Record, unknown>
export default component
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 15ff7b5..6e67cc3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -49,7 +49,7 @@
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^28.4.1",
"unplugin-vue-router": "^0.12.0",
- "vite": "^6.0.0",
+ "vite": "^6.4.2",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-vue-layouts-next": "^0.0.8",
"vite-plugin-vuetify": "^2.1.1",
@@ -1037,11 +1037,10 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
- "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
+ "version": "7.29.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
+ "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
@@ -1642,7 +1641,7 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
- "devOptional": true,
+ "dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@canvas/image-data": {
@@ -1668,6 +1667,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1684,6 +1684,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1700,6 +1701,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1716,6 +1718,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1732,6 +1735,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1748,6 +1752,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1764,6 +1769,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1780,6 +1786,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1796,6 +1803,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1812,6 +1820,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1828,6 +1837,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1844,6 +1854,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1860,6 +1871,7 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1876,6 +1888,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1892,6 +1905,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1908,6 +1922,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1924,6 +1939,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1940,6 +1956,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1956,6 +1973,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1972,6 +1990,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1988,6 +2007,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2004,6 +2024,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2020,6 +2041,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2036,6 +2058,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2052,6 +2075,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2068,6 +2092,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2118,34 +2143,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@eslint/config-array/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==",
- "license": "MIT"
- },
- "node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@eslint/config-array/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@eslint/config-helpers": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
@@ -2193,22 +2190,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/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==",
- "license": "MIT"
- },
- "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2218,18 +2199,6 @@
"node": ">= 4"
}
},
- "node_modules/@eslint/eslintrc/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@eslint/js": {
"version": "9.39.4",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
@@ -2310,34 +2279,6 @@
"node": ">=10.10.0"
}
},
- "node_modules/@humanwhocodes/config-array/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==",
- "license": "MIT"
- },
- "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -2819,7 +2760,7 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -2841,7 +2782,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -2851,7 +2792,7 @@
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -2868,7 +2809,7 @@
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2934,24 +2875,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
- "node_modules/@microsoft/eslint-formatter-sarif/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@microsoft/eslint-formatter-sarif/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@microsoft/eslint-formatter-sarif/node_modules/eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -3098,19 +3021,6 @@
"node": ">= 4"
}
},
- "node_modules/@microsoft/eslint-formatter-sarif/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@microsoft/eslint-formatter-sarif/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -3163,6 +3073,7 @@
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -3202,6 +3113,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3222,6 +3134,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3242,6 +3155,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3262,6 +3176,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3282,6 +3197,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3302,6 +3218,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3322,6 +3239,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3342,6 +3260,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3362,6 +3281,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3382,6 +3302,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3402,6 +3323,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3422,6 +3344,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3442,6 +3365,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3483,12 +3407,37 @@
}
]
},
+ "node_modules/@rollup/plugin-babel": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz",
+ "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.18.6",
+ "@rollup/pluginutils": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "@types/babel__core": "^7.1.9",
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/babel__core": {
+ "optional": true
+ },
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/plugin-node-resolve": {
- "version": "15.3.1",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
- "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
+ "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@@ -3508,19 +3457,39 @@
}
}
},
+ "node_modules/@rollup/plugin-replace": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz",
+ "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "magic-string": "^0.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/plugin-terser": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
- "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz",
+ "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "serialize-javascript": "^6.0.1",
+ "serialize-javascript": "^7.0.3",
"smob": "^1.0.0",
"terser": "^5.17.4"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=20.0.0"
},
"peerDependencies": {
"rollup": "^2.0.0||^3.0.0||^4.0.0"
@@ -3531,12 +3500,20 @@
}
}
},
+ "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@@ -3558,8 +3535,7 @@
"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,
- "license": "MIT"
+ "dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
@@ -3568,6 +3544,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3581,6 +3558,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3594,6 +3572,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3607,6 +3586,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3620,6 +3600,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3633,6 +3614,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3646,6 +3628,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3659,6 +3642,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3672,6 +3656,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3685,6 +3670,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3698,6 +3684,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3711,6 +3698,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3724,6 +3712,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3737,6 +3726,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3750,6 +3740,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3763,6 +3754,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3776,6 +3768,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3789,6 +3782,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3802,6 +3796,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3815,6 +3810,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3828,6 +3824,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3841,6 +3838,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3854,6 +3852,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3867,6 +3866,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3880,6 +3880,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3931,27 +3932,19 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@surma/rollup-plugin-off-main-thread": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
- "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "ejs": "^3.1.6",
- "json5": "^2.2.0",
- "magic-string": "^0.25.0",
- "string.prototype.matchall": "^4.0.6"
- }
- },
- "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
- "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "node_modules/@trickfilm400/rollup-plugin-off-main-thread": {
+ "version": "3.0.0-pre1",
+ "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz",
+ "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "sourcemap-codec": "^1.4.8"
+ "ejs": "^3.1.10",
+ "json5": "^2.2.3",
+ "magic-string": "^0.30.21",
+ "string.prototype.matchall": "^4.0.12"
+ },
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/@tsconfig/node22": {
@@ -3993,7 +3986,7 @@
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -4003,8 +3996,7 @@
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
@@ -4779,7 +4771,6 @@
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
"integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"is-array-buffer": "^3.0.5"
@@ -4805,7 +4796,6 @@
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
"integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.1",
"call-bind": "^1.0.8",
@@ -4866,15 +4856,13 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
"integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -4894,7 +4882,6 @@
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
@@ -4961,7 +4948,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
@@ -5014,7 +5000,6 @@
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -5073,7 +5058,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/cac": {
@@ -5086,15 +5071,14 @@
}
},
"node_modules/call-bind": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.0",
- "es-define-property": "^1.0.0",
- "get-intrinsic": "^1.2.4",
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
"set-function-length": "^1.2.2"
},
"engines": {
@@ -5109,7 +5093,6 @@
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@@ -5123,7 +5106,6 @@
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
@@ -5206,7 +5188,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
@@ -5269,14 +5251,14 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/common-tags": {
@@ -5288,12 +5270,6 @@
"node": ">=4.0.0"
}
},
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "license": "MIT"
- },
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
@@ -5401,7 +5377,6 @@
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
"integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
@@ -5419,7 +5394,6 @@
"resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
"integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
@@ -5437,7 +5411,6 @@
"resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
"integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -5505,7 +5478,6 @@
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -5515,7 +5487,6 @@
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@@ -5533,7 +5504,6 @@
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
@@ -5547,16 +5517,15 @@
}
},
"node_modules/defu": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
- "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
- "license": "MIT"
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -5606,7 +5575,6 @@
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@@ -5621,7 +5589,6 @@
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
@@ -5652,11 +5619,10 @@
}
},
"node_modules/es-abstract": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
- "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.2",
"arraybuffer.prototype.slice": "^1.0.4",
@@ -5725,7 +5691,6 @@
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -5735,7 +5700,6 @@
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -5751,7 +5715,6 @@
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
@@ -5764,7 +5727,6 @@
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@@ -5780,7 +5742,6 @@
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
"integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"is-callable": "^1.2.7",
"is-date-object": "^1.0.5",
@@ -6040,22 +6001,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/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==",
- "license": "MIT"
- },
- "node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -6077,18 +6022,6 @@
"node": ">= 4"
}
},
- "node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -6170,6 +6103,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eta": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz",
+ "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/bgub/eta?sponsor=1"
+ }
+ },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -6233,9 +6178,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
@@ -6246,8 +6191,7 @@
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
- ],
- "license": "BSD-3-Clause"
+ ]
},
"node_modules/fastq": {
"version": "1.20.1",
@@ -6293,41 +6237,10 @@
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
"integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
- "node_modules/filelist/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
- "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/filelist/node_modules/minimatch": {
- "version": "5.1.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
- "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -6380,7 +6293,6 @@
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
@@ -6434,6 +6346,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -6459,7 +6372,6 @@
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
"integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
@@ -6480,7 +6392,6 @@
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
- "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6490,7 +6401,6 @@
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -6510,7 +6420,6 @@
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@@ -6542,7 +6451,6 @@
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@@ -6556,7 +6464,6 @@
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
"integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
@@ -6623,7 +6530,6 @@
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
@@ -6669,7 +6575,6 @@
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -6716,7 +6621,6 @@
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
"integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -6738,7 +6642,6 @@
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
@@ -6751,7 +6654,6 @@
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
"integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.0"
},
@@ -6767,7 +6669,6 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -6780,7 +6681,6 @@
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -6821,8 +6721,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
- "dev": true,
- "license": "ISC"
+ "dev": true
},
"node_modules/ignore": {
"version": "7.0.5",
@@ -6838,7 +6737,7 @@
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -6897,7 +6796,6 @@
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
"integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"hasown": "^2.0.2",
@@ -6912,7 +6810,6 @@
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
"integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
@@ -6936,7 +6833,6 @@
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
"integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"async-function": "^1.0.0",
"call-bound": "^1.0.3",
@@ -6956,7 +6852,6 @@
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
"integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has-bigints": "^1.0.2"
},
@@ -6985,7 +6880,6 @@
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
"integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"has-tostringtag": "^1.0.2"
@@ -7002,7 +6896,6 @@
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -7031,7 +6924,6 @@
"resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
"integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"get-intrinsic": "^1.2.6",
@@ -7049,7 +6941,6 @@
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
@@ -7075,7 +6966,6 @@
"resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
"integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3"
},
@@ -7091,7 +6981,6 @@
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
@@ -7123,7 +7012,6 @@
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
"integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -7135,15 +7023,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -7165,7 +7051,6 @@
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
"integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"has-tostringtag": "^1.0.2"
@@ -7201,7 +7086,6 @@
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
@@ -7230,7 +7114,6 @@
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
"integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -7243,7 +7126,6 @@
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
"integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3"
},
@@ -7272,7 +7154,6 @@
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
"integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"has-tostringtag": "^1.0.2"
@@ -7289,7 +7170,6 @@
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
"integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-symbols": "^1.1.0",
@@ -7307,7 +7187,6 @@
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
@@ -7323,7 +7202,6 @@
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
"integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -7336,7 +7214,6 @@
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
"integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3"
},
@@ -7352,7 +7229,6 @@
"resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
"integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"get-intrinsic": "^1.2.6"
@@ -7381,8 +7257,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/isexe": {
"version": "2.0.0",
@@ -7411,7 +7286,6 @@
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
"async": "^3.2.6",
"filelist": "^1.0.4",
@@ -7613,10 +7487,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "license": "MIT"
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@@ -7777,7 +7650,6 @@
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -7826,13 +7698,11 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
- "dev": true,
- "license": "BlueOak-1.0.0",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dependencies": {
- "brace-expansion": "^5.0.2"
+ "brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -7904,16 +7774,15 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -7931,6 +7800,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
"license": "MIT",
"optional": true
},
@@ -8045,7 +7915,6 @@
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -8058,7 +7927,6 @@
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -8068,7 +7936,6 @@
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
@@ -8125,7 +7992,6 @@
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
"integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.6",
"object-keys": "^1.1.1",
@@ -8374,15 +8240,14 @@
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"funding": [
{
"type": "opencollective",
@@ -8397,7 +8262,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -8506,28 +8370,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/prettier-eslint/node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/prettier-eslint/node_modules/@eslint/eslintrc/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/prettier-eslint/node_modules/@eslint/js": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
@@ -8640,21 +8482,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/prettier-eslint/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==",
- "license": "MIT"
- },
- "node_modules/prettier-eslint/node_modules/brace-expansion": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
- "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/prettier-eslint/node_modules/eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -8727,28 +8554,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/prettier-eslint/node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/prettier-eslint/node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/prettier-eslint/node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -8816,21 +8621,6 @@
"node": ">= 4"
}
},
- "node_modules/prettier-eslint/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/prettier-eslint/node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -8969,7 +8759,6 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -8998,7 +8787,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
@@ -9013,7 +8802,6 @@
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
"integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
@@ -9056,7 +8844,6 @@
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
@@ -9199,22 +8986,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/rimraf/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==",
- "license": "MIT"
- },
- "node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -9236,18 +9007,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/rimraf/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
@@ -9320,22 +9079,21 @@
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
- "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.2",
- "get-intrinsic": "^1.2.6",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
"has-symbols": "^1.1.0",
"isarray": "^2.0.5"
},
@@ -9364,15 +9122,13 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ],
- "license": "MIT"
+ ]
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
"integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"isarray": "^2.0.5"
@@ -9389,7 +9145,6 @@
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -9406,6 +9161,7 @@
"version": "1.98.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
+ "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -9427,7 +9183,7 @@
"version": "1.98.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz",
"integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
@@ -9475,6 +9231,7 @@
"!riscv64",
"!x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -9488,6 +9245,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9504,6 +9262,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9520,6 +9279,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9536,6 +9296,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9552,6 +9313,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9568,6 +9330,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9584,6 +9347,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9600,6 +9364,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9616,6 +9381,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9632,6 +9398,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9648,6 +9415,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9664,6 +9432,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9680,6 +9449,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9696,6 +9466,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9709,6 +9480,7 @@
"version": "1.98.0",
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz",
"integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==",
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9728,6 +9500,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9744,6 +9517,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9757,7 +9531,7 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -9788,22 +9562,11 @@
"node": ">=10"
}
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@@ -9821,7 +9584,6 @@
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@@ -9837,7 +9599,6 @@
"resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
"integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-errors": "^1.3.0",
@@ -9936,7 +9697,6 @@
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
@@ -9952,14 +9712,13 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -9973,7 +9732,6 @@
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -9992,7 +9750,6 @@
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -10045,11 +9802,10 @@
}
},
"node_modules/smob": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz",
- "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz",
+ "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=20.0.0"
}
@@ -10087,7 +9843,7 @@
"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==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@@ -10098,20 +9854,12 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/sourcemap-codec": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
- "deprecated": "Please use @jridgewell/sourcemap-codec instead",
- "dev": true,
- "license": "MIT"
- },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
@@ -10139,7 +9887,6 @@
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
"integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"internal-slot": "^1.1.0"
@@ -10153,7 +9900,6 @@
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
"integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
@@ -10181,7 +9927,6 @@
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
"integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.2",
@@ -10203,7 +9948,6 @@
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
"integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.2",
@@ -10222,7 +9966,6 @@
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
"integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -10339,7 +10082,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
@@ -10352,7 +10095,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz",
"integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
@@ -10391,7 +10134,7 @@
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -10530,7 +10273,6 @@
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
@@ -10545,7 +10287,6 @@
"resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
"integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"for-each": "^0.3.3",
@@ -10565,7 +10306,6 @@
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
"integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
@@ -10587,7 +10327,6 @@
"resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
"integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"for-each": "^0.3.3",
@@ -10652,7 +10391,6 @@
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
"integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"has-bigints": "^1.0.2",
@@ -10731,7 +10469,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -11143,15 +10881,14 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"devOptional": true,
- "license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -11650,7 +11387,6 @@
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
"integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"is-bigint": "^1.1.0",
"is-boolean-object": "^1.2.1",
@@ -11670,7 +11406,6 @@
"resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
"integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"function.prototype.name": "^1.1.6",
@@ -11698,7 +11433,6 @@
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
"integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"is-map": "^2.0.3",
"is-set": "^2.0.3",
@@ -11717,7 +11451,6 @@
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
@@ -11760,70 +11493,67 @@
}
},
"node_modules/workbox-background-sync": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz",
- "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz",
+ "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"idb": "^7.0.1",
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-broadcast-update": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz",
- "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz",
+ "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-build": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz",
- "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz",
+ "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.1",
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.11.0",
"@babel/runtime": "^7.11.2",
- "@rollup/plugin-babel": "^5.2.0",
- "@rollup/plugin-node-resolve": "^15.2.3",
- "@rollup/plugin-replace": "^2.4.1",
- "@rollup/plugin-terser": "^0.4.3",
- "@surma/rollup-plugin-off-main-thread": "^2.2.3",
+ "@rollup/plugin-babel": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "@rollup/plugin-replace": "^6.0.3",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1",
"ajv": "^8.6.0",
"common-tags": "^1.8.0",
+ "eta": "^4.5.1",
"fast-json-stable-stringify": "^2.1.0",
"fs-extra": "^9.0.1",
"glob": "^11.0.1",
- "lodash": "^4.17.20",
"pretty-bytes": "^5.3.0",
- "rollup": "^2.79.2",
+ "rollup": "^4.53.3",
"source-map": "^0.8.0-beta.0",
"stringify-object": "^3.3.0",
"strip-comments": "^2.0.1",
"tempy": "^0.6.0",
"upath": "^1.2.0",
- "workbox-background-sync": "7.4.0",
- "workbox-broadcast-update": "7.4.0",
- "workbox-cacheable-response": "7.4.0",
- "workbox-core": "7.4.0",
- "workbox-expiration": "7.4.0",
- "workbox-google-analytics": "7.4.0",
- "workbox-navigation-preload": "7.4.0",
- "workbox-precaching": "7.4.0",
- "workbox-range-requests": "7.4.0",
- "workbox-recipes": "7.4.0",
- "workbox-routing": "7.4.0",
- "workbox-strategies": "7.4.0",
- "workbox-streams": "7.4.0",
- "workbox-sw": "7.4.0",
- "workbox-window": "7.4.0"
+ "workbox-background-sync": "7.4.1",
+ "workbox-broadcast-update": "7.4.1",
+ "workbox-cacheable-response": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-expiration": "7.4.1",
+ "workbox-google-analytics": "7.4.1",
+ "workbox-navigation-preload": "7.4.1",
+ "workbox-precaching": "7.4.1",
+ "workbox-range-requests": "7.4.1",
+ "workbox-recipes": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1",
+ "workbox-streams": "7.4.1",
+ "workbox-sw": "7.4.1",
+ "workbox-window": "7.4.1"
},
"engines": {
"node": ">=20.0.0"
@@ -11847,69 +11577,6 @@
"ajv": ">=8"
}
},
- "node_modules/workbox-build/node_modules/@rollup/plugin-babel": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
- "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.10.4",
- "@rollup/pluginutils": "^3.1.0"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0",
- "@types/babel__core": "^7.1.9",
- "rollup": "^1.20.0||^2.0.0"
- },
- "peerDependenciesMeta": {
- "@types/babel__core": {
- "optional": true
- }
- }
- },
- "node_modules/workbox-build/node_modules/@rollup/plugin-replace": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
- "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rollup/pluginutils": "^3.1.0",
- "magic-string": "^0.25.7"
- },
- "peerDependencies": {
- "rollup": "^1.20.0 || ^2.0.0"
- }
- },
- "node_modules/workbox-build/node_modules/@rollup/pluginutils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
- "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "0.0.39",
- "estree-walker": "^1.0.1",
- "picomatch": "^2.2.2"
- },
- "engines": {
- "node": ">= 8.0.0"
- },
- "peerDependencies": {
- "rollup": "^1.20.0||^2.0.0"
- }
- },
- "node_modules/workbox-build/node_modules/@types/estree": {
- "version": "0.0.39",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
- "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/workbox-build/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
@@ -11927,13 +11594,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/workbox-build/node_modules/estree-walker": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
- "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/workbox-build/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -11941,29 +11601,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/workbox-build/node_modules/magic-string": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
- "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "sourcemap-codec": "^1.4.8"
- }
- },
- "node_modules/workbox-build/node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/workbox-build/node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -11977,22 +11614,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/workbox-build/node_modules/rollup": {
- "version": "2.80.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
- "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=10.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
"node_modules/workbox-build/node_modules/upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -12005,140 +11626,127 @@
}
},
"node_modules/workbox-cacheable-response": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz",
- "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz",
+ "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-core": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz",
- "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==",
- "dev": true,
- "license": "MIT"
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz",
+ "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==",
+ "dev": true
},
"node_modules/workbox-expiration": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz",
- "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz",
+ "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"idb": "^7.0.1",
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-google-analytics": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz",
- "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz",
+ "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-background-sync": "7.4.0",
- "workbox-core": "7.4.0",
- "workbox-routing": "7.4.0",
- "workbox-strategies": "7.4.0"
+ "workbox-background-sync": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
}
},
"node_modules/workbox-navigation-preload": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz",
- "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz",
+ "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-precaching": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz",
- "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz",
+ "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0",
- "workbox-routing": "7.4.0",
- "workbox-strategies": "7.4.0"
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
}
},
"node_modules/workbox-range-requests": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz",
- "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz",
+ "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-recipes": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz",
- "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz",
+ "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-cacheable-response": "7.4.0",
- "workbox-core": "7.4.0",
- "workbox-expiration": "7.4.0",
- "workbox-precaching": "7.4.0",
- "workbox-routing": "7.4.0",
- "workbox-strategies": "7.4.0"
+ "workbox-cacheable-response": "7.4.1",
+ "workbox-core": "7.4.1",
+ "workbox-expiration": "7.4.1",
+ "workbox-precaching": "7.4.1",
+ "workbox-routing": "7.4.1",
+ "workbox-strategies": "7.4.1"
}
},
"node_modules/workbox-routing": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz",
- "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz",
+ "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-strategies": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz",
- "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz",
+ "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/workbox-streams": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz",
- "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz",
+ "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "workbox-core": "7.4.0",
- "workbox-routing": "7.4.0"
+ "workbox-core": "7.4.1",
+ "workbox-routing": "7.4.1"
}
},
"node_modules/workbox-sw": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz",
- "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==",
- "dev": true,
- "license": "MIT"
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz",
+ "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==",
+ "dev": true
},
"node_modules/workbox-window": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz",
- "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz",
+ "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/trusted-types": "^2.0.2",
- "workbox-core": "7.4.0"
+ "workbox-core": "7.4.1"
}
},
"node_modules/wrappy": {
@@ -12165,11 +11773,10 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "devOptional": true,
- "license": "ISC",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "dev": true,
"bin": {
"yaml": "bin.mjs"
},
diff --git a/frontend/package.json b/frontend/package.json
index babd9ff..aca3e0f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -59,12 +59,22 @@
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^28.4.1",
"unplugin-vue-router": "^0.12.0",
- "vite": "^6.0.0",
+ "vite": "^6.4.2",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-vue-layouts-next": "^0.0.8",
"vite-plugin-vuetify": "^2.1.1",
"vitest": "^4.1.3",
"vue-router": "^4.5.0",
"vue-tsc": "^3.2.5"
+ },
+ "overrides": {
+ "fast-uri": "^3.1.2",
+ "defu": "^6.1.7",
+ "minimatch": "^10.2.1",
+ "postcss": "^8.5.14",
+ "serialize-javascript": "6.0.2",
+ "lodash": "4.17.21",
+ "@babel/plugin-transform-modules-systemjs": "^7.27.0",
+ "yaml": "^2.8.3"
}
}
diff --git a/frontend/src/components.d.ts b/frontend/src/components.d.ts
index 0891e68..f0869a7 100644
--- a/frontend/src/components.d.ts
+++ b/frontend/src/components.d.ts
@@ -24,6 +24,8 @@ declare module 'vue' {
CreateExercise: typeof import('./components/Exercise/CreateExercise.vue')['default']
CreateWorkout: typeof import('./components/Workout/CreateWorkout.vue')['default']
DraggableTextList: typeof import('./components/basicUI/DraggableTextList.vue')['default']
+ DuplicateActivityDialog: typeof import('./components/Activity/DuplicateActivityDialog.vue')['default']
+ DuplicateExerciseDialog: typeof import('./components/Exercise/DuplicateExerciseDialog.vue')['default']
EditActivity: typeof import('./components/Activity/EditActivity.vue')['default']
EditActivityLog: typeof import('./components/Activity/EditActivityLog.vue')['default']
EditExercise: typeof import('./components/Exercise/EditExercise.vue')['default']
diff --git a/frontend/src/components/Activity/ActivityDetails.vue b/frontend/src/components/Activity/ActivityDetails.vue
index b0ad62a..885a247 100644
--- a/frontend/src/components/Activity/ActivityDetails.vue
+++ b/frontend/src/components/Activity/ActivityDetails.vue
@@ -24,21 +24,28 @@
"
>
-
+
-
- {{ $t('common.edit') }}
-
-
- {{
- $t('activity.deleteActivity')
- }}
-
+
+
+ {{ $t('activity.personalize') }}
+
+
+
+
+ {{ $t('common.edit') }}
+
+
+ {{
+ $t('activity.deleteActivity')
+ }}
+
+
@@ -52,7 +59,13 @@
{{ $t('settings.activities') }}
-
{{ activity.name }}
+
{{ activityName }}
+
+ {{ $t('activity.global') }}
+
+
+ {{ $t('activity.personalized') }}
+
@@ -99,9 +112,9 @@
-
+
{{ $t('exerciseDetails.about') }}
-
{{ activity.description }}
+
{{ activityDescription }}
@@ -236,11 +249,19 @@
/>
+
+
+
@@ -254,22 +275,30 @@ import type { Activity, ActivityLog } from '@/interfaces/Activity.interface'
import BackHeader from '@/components/BackHeader.vue'
import EditActivity from './EditActivity.vue'
import EditActivityLog from './EditActivityLog.vue'
+import DuplicateActivityDialog from './DuplicateActivityDialog.vue'
import AcceptDialog from '@/components/basicUI/AcceptDialog.vue'
import { toast } from 'vuetify-sonner'
import { useI18n } from 'vue-i18n'
+import { displayActivityName, resolveI18n } from '@/utils/exerciseDisplay'
+import { useUserLanguage } from '@/composables/useUserLanguage'
const props = defineProps<{ activity: Activity }>()
const emit = defineEmits<{ close: [] }>()
const { t } = useI18n()
+const { lang } = useUserLanguage()
const activityStore = useActivityStore()
const activity = computed(
() => activityStore.activities.find(a => a.id === props.activity.id) ?? props.activity
)
+const activityName = computed(() => displayActivityName(activity.value, lang.value))
+const activityDescription = computed(() => resolveI18n(activity.value?.description, lang.value))
+
const isEditOpen = ref(false)
const isEditLogOpen = ref(false)
const isDeleteDialogOpen = ref(false)
+const isDuplicateDialogOpen = ref(false)
const selectedLog = ref
(null)
diff --git a/frontend/src/components/Activity/DuplicateActivityDialog.vue b/frontend/src/components/Activity/DuplicateActivityDialog.vue
new file mode 100644
index 0000000..d4305c3
--- /dev/null
+++ b/frontend/src/components/Activity/DuplicateActivityDialog.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+ {{ $t('activity.personalizeTitle', { name: activityName }) }}
+
+
+ {{ $t('activity.personalizeBody') }}
+
+
+
+ {{ $t('activity.transferHistory') }}
+
+
+ {{ $t('activity.startFresh') }}
+
+
+ {{ $t('common.cancel') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Activity/EditActivity.vue b/frontend/src/components/Activity/EditActivity.vue
index abf8a7c..21956f5 100644
--- a/frontend/src/components/Activity/EditActivity.vue
+++ b/frontend/src/components/Activity/EditActivity.vue
@@ -129,7 +129,7 @@
@@ -148,10 +148,13 @@ import type { Activity } from '@/interfaces/Activity.interface'
import AcceptDialog from '@/components/basicUI/AcceptDialog.vue'
import { toast } from 'vuetify-sonner'
import { useI18n } from 'vue-i18n'
+import { resolveI18n } from '@/utils/exerciseDisplay'
+import { useUserLanguage } from '@/composables/useUserLanguage'
const props = defineProps<{ activity: Activity }>()
const emit = defineEmits<{ close: []; deleted: [] }>()
const { t } = useI18n()
+const { lang } = useUserLanguage()
const activityStore = useActivityStore()
const formRef = ref()
const isSaving = ref(false)
@@ -184,8 +187,8 @@ const iconOptions = computed(() => [
const form = ref({
- name: props.activity.name,
- description: props.activity.description ?? '',
+ name: resolveI18n(props.activity.title, lang.value),
+ description: resolveI18n(props.activity.description, lang.value),
icon: props.activity.icon,
equipment: props.activity.equipment ? [...props.activity.equipment] : ([] as string[]),
trackDistance: props.activity.trackDistance,
diff --git a/frontend/src/components/Exercise/AddExerciseList.vue b/frontend/src/components/Exercise/AddExerciseList.vue
index 88aff0e..a446bbf 100644
--- a/frontend/src/components/Exercise/AddExerciseList.vue
+++ b/frontend/src/components/Exercise/AddExerciseList.vue
@@ -107,36 +107,84 @@
-
-
-
-
-
- {{ displayName(exercise) }}
-
+
+
+ {{ $t('exercise.myExercises') }}
+
+
+
+
+
+
+
+ {{ displayName(exercise) }}
+
+
+ {{ $t('exercise.myExercise') }}
+
+
+
+
+ mdi-information-outline
+
+
+
+
+
+
+
+ {{ $t('exercise.globalExercises') }}
+
+
+
+
+
+
+
+ {{ displayName(exercise) }}
+
+
+ {{ $t('exercise.global') }}
+
+
+
+
+ mdi-information-outline
+
-
- mdi-information-outline
-
-
-
+
+
-
+
mdi-dumbbell
{{ $t('exerciseCatalog.noExercisesFound') }}
{{ $t('exerciseCatalog.adjustSearch') }}
@@ -165,7 +213,8 @@ import type { MuscleGroup } from '@/interfaces/MuscleGroup.interface'
import { useExerciseStore } from '@/stores/exercise.store'
import { useMuscleGroupStore } from '@/stores/muscleGroup.store'
import { useI18n } from 'vue-i18n'
-import { displayExerciseName } from '@/utils/exerciseDisplay'
+import { displayExerciseName, resolveI18n } from '@/utils/exerciseDisplay'
+import { useUserLanguage } from '@/composables/useUserLanguage'
const props = defineProps<{
initialSelectedIds: number[]
@@ -179,6 +228,7 @@ const emit = defineEmits<{
const muscleGroupStore = useMuscleGroupStore()
const exerciseStore = useExerciseStore()
const { t } = useI18n({ useScope: 'global' })
+const { lang } = useUserLanguage()
const searchQuery = ref('')
const selectedIds = ref
([...props.initialSelectedIds])
@@ -189,7 +239,7 @@ const isFilterMenuOpen = ref(false)
const selectedMuscleGroups = ref([])
const selectedTypes = ref([])
-const displayName = (exercise: Exercise) => displayExerciseName({ t }, exercise)
+const displayName = (exercise: Exercise) => displayExerciseName(exercise, lang.value)
const activeFilterCount = computed(
() => selectedMuscleGroups.value.length + selectedTypes.value.length
@@ -209,10 +259,10 @@ const muscleGroups = computed(() =>
muscleGroupStore.muscleGroups.map(g => ({ name: g.name, translatedName: t(g.name), id: g.id }))
)
-const exercises = computed(() =>
+const filteredExercises = computed(() =>
exerciseStore.exercises.filter((exercise: Exercise) => {
const name = displayName(exercise).toLowerCase()
- const desc = (exercise.description ?? '').toLowerCase()
+ const desc = resolveI18n(exercise.description, lang.value).toLowerCase()
const query = (searchQuery.value || '').toLowerCase()
const matchesSearch = name.includes(query) || desc.includes(query)
@@ -228,6 +278,9 @@ const exercises = computed(() =>
})
)
+const ownExercises = computed(() => filteredExercises.value.filter(e => !e.isGlobal))
+const globalExercises = computed(() => filteredExercises.value.filter(e => e.isGlobal))
+
const onCreateExerciseClose = async () => {
isCreateExerciseOpen.value = false
await exerciseStore.setExercises(true)
diff --git a/frontend/src/components/Exercise/DuplicateExerciseDialog.vue b/frontend/src/components/Exercise/DuplicateExerciseDialog.vue
new file mode 100644
index 0000000..85d984a
--- /dev/null
+++ b/frontend/src/components/Exercise/DuplicateExerciseDialog.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+ {{ $t('exercise.personalizeTitle', { name: exerciseName }) }}
+
+
+ {{ $t('exercise.personalizeBody') }}
+
+
+
+ {{ $t('exercise.transferHistory') }}
+
+
+ {{ $t('exercise.startFresh') }}
+
+
+ {{ $t('common.cancel') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Exercise/EditExercise.vue b/frontend/src/components/Exercise/EditExercise.vue
index 307db2e..9e01a97 100644
--- a/frontend/src/components/Exercise/EditExercise.vue
+++ b/frontend/src/components/Exercise/EditExercise.vue
@@ -193,6 +193,8 @@ import { useMuscleGroupStore } from '@/stores/muscleGroup.store'
import { useExerciseStore } from '@/stores/exercise.store'
import { toast } from 'vuetify-sonner'
import { useI18n } from 'vue-i18n'
+import { resolveI18n, resolveI18nArray } from '@/utils/exerciseDisplay'
+import { useUserLanguage } from '@/composables/useUserLanguage'
const props = defineProps<{
exercise: Exercise
@@ -206,6 +208,7 @@ const emit = defineEmits<{
const muscleGroupStore = useMuscleGroupStore()
const exerciseStore = useExerciseStore()
const { t } = useI18n({ useScope: 'global' })
+const { lang } = useUserLanguage()
const isSaving = ref(false)
const isDeleting = ref(false)
@@ -219,15 +222,15 @@ const exerciseTypeItems = [
]
const form = ref({
- name: props.exercise.name || '',
- description: props.exercise.description || '',
+ name: resolveI18n(props.exercise.title, lang.value),
+ description: resolveI18n(props.exercise.description, lang.value),
exerciseType: props.exercise.exerciseType || (null as ExerciseType | null | undefined),
muscleGroupIds: props.exercise.muscleGroups?.map(mg => mg.id) || ([] as number[]),
primaryMuscleGroupIds: props.exercise.primaryMuscleGroups?.map(mg => mg.id) || ([] as number[]),
equipment: props.exercise.equipment || ([] as string[]),
- instructions: props.exercise.instructions || ([] as string[]),
- proTips: props.exercise.proTips || ([] as string[]),
- mistakes: props.exercise.mistakes || ([] as string[]),
+ instructions: resolveI18nArray(props.exercise.instructions, lang.value),
+ proTips: resolveI18nArray(props.exercise.proTips, lang.value),
+ mistakes: resolveI18nArray(props.exercise.mistakes, lang.value),
})
const muscleGroupItems = computed(() =>
diff --git a/frontend/src/components/Exercise/ExerciseDetails.vue b/frontend/src/components/Exercise/ExerciseDetails.vue
index 2885482..d7ef589 100644
--- a/frontend/src/components/Exercise/ExerciseDetails.vue
+++ b/frontend/src/components/Exercise/ExerciseDetails.vue
@@ -23,19 +23,26 @@
overscroll-behavior: none;
"
>
-
+
-
- {{ $t('common.edit') }}
-
-
- {{ $t('common.delete') }}
-
+
+
+ {{ $t('exercise.personalize') }}
+
+
+
+
+ {{ $t('common.edit') }}
+
+
+ {{ $t('common.delete') }}
+
+
@@ -50,9 +57,30 @@
{{ exercise.exerciseType }}
- {{ exercise.name }}
+ {{ exerciseName }}
+
+ {{ $t('exercise.global') }}
+
+
+ {{ $t('exercise.personalized') }}
+
+
+ {{ $t('exercise.myExercise') }}
+
+
+
+ {{ $t('exercise.personalizedBanner') }}
+
+
-
+
{{ $t('exerciseDetails.about') }}
-
{{ exercise.description }}
+
{{ exerciseDescription }}
@@ -127,10 +155,10 @@
-
+
{{ $t('exerciseDetails.howToPerform') }}
-
+
{{
i + 1
}}
@@ -140,13 +168,13 @@
-
+
{{ $t('exerciseDetails.proTips') }}
-
+
mdi-lightbulb-on-outline
{{ tip }}
@@ -154,13 +182,13 @@
-
+
{{ $t('exerciseDetails.mistakes') }}
-
+
@@ -174,6 +202,14 @@
+
+
+
@@ -194,11 +230,14 @@