From ea3155677e63f1a230fcada22e670183ba65c1ef Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Mon, 20 Jul 2026 10:50:58 +0200 Subject: [PATCH 01/33] Setup new dashboard project --- dashboard/.gitignore | 24 + dashboard/index.html | 13 + dashboard/package-lock.json | 2407 ++++++++++++++++++++ dashboard/package.json | 26 + dashboard/public/128x128.png | Bin 0 -> 3591 bytes dashboard/public/icon.ico | Bin 0 -> 14493 bytes dashboard/src/config.ts | 16 + dashboard/src/main.css | 72 + dashboard/src/main.ts | 9 + dashboard/src/views/DashboardView.model.ts | 14 + dashboard/src/views/DashboardView.vue | 7 + dashboard/tsconfig.app.json | 28 + dashboard/tsconfig.json | 11 + dashboard/tsconfig.node.json | 27 + dashboard/vite.config.ts | 15 + 15 files changed, 2669 insertions(+) create mode 100644 dashboard/.gitignore create mode 100644 dashboard/index.html create mode 100644 dashboard/package-lock.json create mode 100644 dashboard/package.json create mode 100644 dashboard/public/128x128.png create mode 100644 dashboard/public/icon.ico create mode 100644 dashboard/src/config.ts create mode 100644 dashboard/src/main.css create mode 100644 dashboard/src/main.ts create mode 100644 dashboard/src/views/DashboardView.model.ts create mode 100644 dashboard/src/views/DashboardView.vue create mode 100644 dashboard/tsconfig.app.json create mode 100644 dashboard/tsconfig.json create mode 100644 dashboard/tsconfig.node.json create mode 100644 dashboard/vite.config.ts diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..cc0a70c --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + AniStrema + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..abd0a31 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,2407 @@ +{ + "name": "dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "daisyui": "^5.6.18", + "tailwindcss": "^4.3.3", + "vue": "^3.5.39", + "vue-mvvm": "^0.10.0" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } + }, + "node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.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==", + "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", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", + "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^8.1.5" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", + "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-shared": "^8.1.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", + "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@vue/language-core": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.7.tgz", + "integrity": "sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "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/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", + "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.29.2", + "@babel/types": "^7.29.0", + "ast-kit": "^2.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "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", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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", + "peer": true + }, + "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/daisyui": { + "version": "5.6.18", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.18.tgz", + "integrity": "sha512-6y9rboRQl8fosnusy/5pg11yp+H0LtK3YdtfS4Vf2QVnaYOKasNdYm6hDxeiT6bwZPdl4WRPzVIlIApdo2ssBg==", + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "license": "MIT", + "peer": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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", + "peer": true + }, + "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", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "peer": true, + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "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", + "peer": 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", + "peer": 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", + "peer": 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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT", + "peer": 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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT", + "peer": 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", + "peer": 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": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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", + "peer": true, + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "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", + "peer": true + }, + "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/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/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", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "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", + "peer": true + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/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.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-mvvm": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/vue-mvvm/-/vue-mvvm-0.10.0.tgz", + "integrity": "sha512-TY2LDtw8cei0/z+X6o231/hDywmzvgHLI91a3zNwXRhMQ+OGKu0abnDGYnv/3oyn9rfYJSUNZ99fF9rZIkEV6Q==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.5.24", + "vue-router": "^5.0.3" + } + }, + "node_modules/vue-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.9.0", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.2.1", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.1.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", + "yaml": "^2.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.7.tgz", + "integrity": "sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.7" + }, + "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", + "peer": true + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..c12cacf --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,26 @@ +{ + "name": "dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "daisyui": "^5.6.18", + "tailwindcss": "^4.3.3", + "vue": "^3.5.39", + "vue-mvvm": "^0.10.0" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } +} diff --git a/dashboard/public/128x128.png b/dashboard/public/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..b166f825011ba5c6c4dc752ca2337fdcacda5c2b GIT binary patch literal 3591 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 AT>t<8 literal 0 HcmV?d00001 diff --git a/dashboard/public/icon.ico b/dashboard/public/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cfe199ea019dfd3f5bbb8df1ac0d19c6f33d9e6c GIT binary patch literal 14493 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 DWF<9& literal 0 HcmV?d00001 diff --git a/dashboard/src/config.ts b/dashboard/src/config.ts new file mode 100644 index 0000000..c095f99 --- /dev/null +++ b/dashboard/src/config.ts @@ -0,0 +1,16 @@ +import type {AppShell, WritableGlobalContext} from "vue-mvvm"; + +import {DashboardViewModel} from "@views/DashboardView.model.ts"; + +export class AppConfig implements AppShell { + public router: AppShell.RouterConfig = { + views: [ + DashboardViewModel + ] + } + + + public configureServices(_ctx: WritableGlobalContext): void { + } + +} \ No newline at end of file diff --git a/dashboard/src/main.css b/dashboard/src/main.css new file mode 100644 index 0000000..95fdf2f --- /dev/null +++ b/dashboard/src/main.css @@ -0,0 +1,72 @@ +@import "tailwindcss"; +@plugin "daisyui"; + +@plugin "daisyui/theme" { + name: "aniworld-light"; + default: true; + prefersdark: false; + color-scheme: "light"; + --color-base-100: oklch(99% 0 0); + --color-base-200: oklch(97% 0 0); + --color-base-300: oklch(93% 0 0); + --color-base-content: oklch(18% 0 0); + --color-primary: oklch(65% 0.22 280); + --color-primary-content: oklch(97% 0.014 285); + --color-secondary: oklch(62% 0.265 303.9); + --color-secondary-content: oklch(97% 0.014 308.299); + --color-accent: oklch(72% 0.219 149.579); + --color-accent-content: oklch(98% 0.018 155.826); + --color-neutral: oklch(37% 0 0); + --color-neutral-content: oklch(98% 0 0); + --color-info: oklch(62% 0.214 259.815); + --color-info-content: oklch(97% 0.014 254.604); + --color-success: oklch(72% 0.219 149.579); + --color-success-content: oklch(98% 0.018 155.826); + --color-warning: oklch(76% 0.188 70.08); + --color-warning-content: oklch(98% 0.022 95.277); + --color-error: oklch(63% 0.237 25.331); + --color-error-content: oklch(97% 0.013 17.38); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.25rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} + +@plugin "daisyui/theme" { + name: "aniworld-dark"; + default: false; + prefersdark: true; + color-scheme: "dark"; + --color-base-100: oklch(16% 0 0); + --color-base-200: oklch(20% 0 0); + --color-base-300: oklch(26% 0 0); + --color-base-content: oklch(95% 0 0); + --color-primary: oklch(68% 0.22 280); + --color-primary-content: oklch(15% 0.02 280); + --color-secondary: oklch(70% 0.27 303.9); + --color-secondary-content: oklch(15% 0.02 303.9); + --color-accent: oklch(75% 0.21 149.579); + --color-accent-content: oklch(16% 0.02 149.579); + --color-neutral: oklch(30% 0 0); + --color-neutral-content: oklch(96% 0 0); + --color-info: oklch(70% 0.214 259.815); + --color-info-content: oklch(16% 0.02 259.815); + --color-success: oklch(75% 0.219 149.579); + --color-success-content: oklch(16% 0.02 149.579); + --color-warning: oklch(78% 0.188 70.08); + --color-warning-content: oklch(18% 0.02 70.08); + --color-error: oklch(68% 0.237 25.331); + --color-error-content: oklch(16% 0.02 25.331); + --radius-selector: 0.5rem; + --radius-field: 0.25rem; + --radius-box: 0.25rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 0; + --noise: 0; +} diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts new file mode 100644 index 0000000..bf85f9d --- /dev/null +++ b/dashboard/src/main.ts @@ -0,0 +1,9 @@ +import { createApp } from "vue"; +import {MVVMApp, createMVVM} from "vue-mvvm"; + +import {AppConfig} from "./config.ts"; +import "./main.css"; + +createApp(MVVMApp) + .use(createMVVM(new AppConfig())) + .mount("#app"); diff --git a/dashboard/src/views/DashboardView.model.ts b/dashboard/src/views/DashboardView.model.ts new file mode 100644 index 0000000..09b14bd --- /dev/null +++ b/dashboard/src/views/DashboardView.model.ts @@ -0,0 +1,14 @@ +import type {Component} from "vue"; +import {ViewModel} from "vue-mvvm"; +import type {RouteAdapter} from "vue-mvvm/router"; + +import DashboardView from "@views/DashboardView.vue"; + +export class DashboardViewModel extends ViewModel { + public static readonly component: Component = DashboardView; + public static readonly route: RouteAdapter = { + path: "/" + } + + +} \ No newline at end of file diff --git a/dashboard/src/views/DashboardView.vue b/dashboard/src/views/DashboardView.vue new file mode 100644 index 0000000..69320c9 --- /dev/null +++ b/dashboard/src/views/DashboardView.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/dashboard/tsconfig.app.json b/dashboard/tsconfig.app.json new file mode 100644 index 0000000..d55ba38 --- /dev/null +++ b/dashboard/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": [ + "vite/client" + ], + "allowArbitraryExtensions": true, + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@views/*": [ + "./src/views/*" + ], + "@controls/*": [ + "./src/controls/*" + ] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue" + ] +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..41ccf0f --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/dashboard/tsconfig.node.json b/dashboard/tsconfig.node.json new file mode 100644 index 0000000..a3172f3 --- /dev/null +++ b/dashboard/tsconfig.node.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": [ + "ES2023" + ], + "types": [ + "node" + ], + "skipLibCheck": true, + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 0000000..b0f2ef8 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,15 @@ +import * as path from "path"; + +import {defineConfig} from "vite"; +import vue from "@vitejs/plugin-vue"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + "@views": path.join(__dirname, "src", "views"), + "@controls": path.join(__dirname, "src", "controls") + } + } +}); From e1325798dc49c88b862e3cdd981b788f3586b4bd Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Mon, 20 Jul 2026 12:07:58 +0200 Subject: [PATCH 02/33] Implement LoginView --- dashboard/.iconify.ini | 35 ++++++ dashboard/index.html | 4 +- dashboard/src/config.ts | 11 +- dashboard/src/contracts/profile.service.ts | 7 ++ dashboard/src/controls/UpdatePanel.model.ts | 5 + dashboard/src/controls/UpdatePanel.vue | 7 ++ dashboard/src/icons/LucideCircleAlert.vue | 13 ++ .../src/icons/SvgSpinners180RingWithBg.vue | 13 ++ dashboard/src/main.ts | 4 +- dashboard/src/services/loader.ts | 8 ++ dashboard/src/services/profile.service.ts | 13 ++ dashboard/src/services/shared.ts | 6 + dashboard/src/views/DashboardView.model.ts | 4 +- dashboard/src/views/LoginView.model.ts | 111 ++++++++++++++++++ dashboard/src/views/LoginView.vue | 92 +++++++++++++++ dashboard/tsconfig.app.json | 10 ++ dashboard/vite.config.ts | 5 +- 17 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 dashboard/.iconify.ini create mode 100644 dashboard/src/contracts/profile.service.ts create mode 100644 dashboard/src/controls/UpdatePanel.model.ts create mode 100644 dashboard/src/controls/UpdatePanel.vue create mode 100644 dashboard/src/icons/LucideCircleAlert.vue create mode 100644 dashboard/src/icons/SvgSpinners180RingWithBg.vue create mode 100644 dashboard/src/services/loader.ts create mode 100644 dashboard/src/services/profile.service.ts create mode 100644 dashboard/src/services/shared.ts create mode 100644 dashboard/src/views/LoginView.model.ts create mode 100644 dashboard/src/views/LoginView.vue diff --git a/dashboard/.iconify.ini b/dashboard/.iconify.ini new file mode 100644 index 0000000..7488ecd --- /dev/null +++ b/dashboard/.iconify.ini @@ -0,0 +1,35 @@ +[api] +; The Iconify API host. Default is 'api.iconify.design' +; host = api.iconify.design + +; The API port to connect to. Default is 443. +; port = 443 + +; Use proxy env variable: HTTP_PROXY, HTTPS_PROXY and NO_PROXY for all requests +enable_proxy = true + +[output] +; Directory where downloaded icons will be saved +; Path is relative to the config file. Default is '$CWD' +out_dir=./src/icons + +; Determines folder structure: +; - 'flat' all icons are stored in the configured folder +; - 'prefixed' like 'flat' but the filenames have their collection prefix in front +; - 'grouped' a subfolder for each collection's prefix is generated +placement=prefixed + +; Filename strategy for downloaded icons: +; - 'lower' Strategy for lowercase +; - 'upper' Strategy for UPPERCASE +; - 'camel' Strategy for camelCase +; - 'pascal' Strategy for PascalCase +; - 'kebab' Strategy for kebab-case +naming=pascal + +; Template or output format: +; - 'svg' A raw svg file template +; - 'raw' A raw ESM JavaScript template +; - 'raw-ts' A raw ESM TypeScript template +; - 'vue' A Vue.js template +template=vue \ No newline at end of file diff --git a/dashboard/index.html b/dashboard/index.html index cc0a70c..dfed269 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -2,9 +2,9 @@ - + - AniStrema + AniStream
diff --git a/dashboard/src/config.ts b/dashboard/src/config.ts index c095f99..1b5d284 100644 --- a/dashboard/src/config.ts +++ b/dashboard/src/config.ts @@ -1,16 +1,23 @@ import type {AppShell, WritableGlobalContext} from "vue-mvvm"; -import {DashboardViewModel} from "@views/DashboardView.model.ts"; +import {LoginViewModel} from "@views/LoginView.model"; +import {DashboardViewModel} from "@views/DashboardView.model"; + +import {services} from "@services/loader"; export class AppConfig implements AppShell { public router: AppShell.RouterConfig = { views: [ + LoginViewModel, DashboardViewModel ] } - public configureServices(_ctx: WritableGlobalContext): void { + public configureServices(ctx: WritableGlobalContext): void { + for (const service of services) { + ctx.registerService(service.key, ctx => new service.ctor(ctx)); + } } } \ No newline at end of file diff --git a/dashboard/src/contracts/profile.service.ts b/dashboard/src/contracts/profile.service.ts new file mode 100644 index 0000000..4c48559 --- /dev/null +++ b/dashboard/src/contracts/profile.service.ts @@ -0,0 +1,7 @@ +import {ServiceKey} from "vue-mvvm"; + +export interface ProfileService { + login(username: string, password: string): Promise; +} + +export const ProfileService: ServiceKey = new ServiceKey("profile.service"); \ No newline at end of file diff --git a/dashboard/src/controls/UpdatePanel.model.ts b/dashboard/src/controls/UpdatePanel.model.ts new file mode 100644 index 0000000..b8eb174 --- /dev/null +++ b/dashboard/src/controls/UpdatePanel.model.ts @@ -0,0 +1,5 @@ +import {UserControl} from "vue-mvvm"; + +export class UpdatePanelModel extends UserControl { + +} \ No newline at end of file diff --git a/dashboard/src/controls/UpdatePanel.vue b/dashboard/src/controls/UpdatePanel.vue new file mode 100644 index 0000000..36efb52 --- /dev/null +++ b/dashboard/src/controls/UpdatePanel.vue @@ -0,0 +1,7 @@ + + + diff --git a/dashboard/src/icons/LucideCircleAlert.vue b/dashboard/src/icons/LucideCircleAlert.vue new file mode 100644 index 0000000..7c4d24d --- /dev/null +++ b/dashboard/src/icons/LucideCircleAlert.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/SvgSpinners180RingWithBg.vue b/dashboard/src/icons/SvgSpinners180RingWithBg.vue new file mode 100644 index 0000000..b040187 --- /dev/null +++ b/dashboard/src/icons/SvgSpinners180RingWithBg.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index bf85f9d..ecc26c8 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -1,7 +1,7 @@ -import { createApp } from "vue"; +import {createApp} from "vue"; import {MVVMApp, createMVVM} from "vue-mvvm"; -import {AppConfig} from "./config.ts"; +import {AppConfig} from "./config"; import "./main.css"; createApp(MVVMApp) diff --git a/dashboard/src/services/loader.ts b/dashboard/src/services/loader.ts new file mode 100644 index 0000000..dff48d2 --- /dev/null +++ b/dashboard/src/services/loader.ts @@ -0,0 +1,8 @@ +import type {ServiceDeclaration} from "@services/shared.ts"; + +const modules: Record> = import.meta.glob("./**/*.service.ts", { + eager: true, + import: "default" +}); + +export const services: ServiceDeclaration[] = Object.values(modules); \ No newline at end of file diff --git a/dashboard/src/services/profile.service.ts b/dashboard/src/services/profile.service.ts new file mode 100644 index 0000000..03e1228 --- /dev/null +++ b/dashboard/src/services/profile.service.ts @@ -0,0 +1,13 @@ +import {ProfileService} from "@contracts/profile.service"; +import type {ServiceDeclaration} from "@services/shared"; + +class ProfileServiceImpl implements ProfileService { + public async login(_username: string, _password: string): Promise { + return false; + } +} + +export default { + key: ProfileService, + ctor: ProfileServiceImpl +} satisfies ServiceDeclaration; \ No newline at end of file diff --git a/dashboard/src/services/shared.ts b/dashboard/src/services/shared.ts new file mode 100644 index 0000000..2aaa8fc --- /dev/null +++ b/dashboard/src/services/shared.ts @@ -0,0 +1,6 @@ +import type {ReadableGlobalContext, ServiceKey} from "vue-mvvm"; + +export interface ServiceDeclaration { + key: ServiceKey; + ctor: new (ctx: ReadableGlobalContext) => T; +} \ No newline at end of file diff --git a/dashboard/src/views/DashboardView.model.ts b/dashboard/src/views/DashboardView.model.ts index 09b14bd..c2239de 100644 --- a/dashboard/src/views/DashboardView.model.ts +++ b/dashboard/src/views/DashboardView.model.ts @@ -7,8 +7,6 @@ import DashboardView from "@views/DashboardView.vue"; export class DashboardViewModel extends ViewModel { public static readonly component: Component = DashboardView; public static readonly route: RouteAdapter = { - path: "/" + path: "/home" } - - } \ No newline at end of file diff --git a/dashboard/src/views/LoginView.model.ts b/dashboard/src/views/LoginView.model.ts new file mode 100644 index 0000000..9ccebe0 --- /dev/null +++ b/dashboard/src/views/LoginView.model.ts @@ -0,0 +1,111 @@ +import {type Component, nextTick} from "vue"; +import {Delegate, ViewModel} from "vue-mvvm"; +import {type RouteAdapter, RouterService} from "vue-mvvm/router"; + +import LoginView from "@views/LoginView.vue"; +import {DashboardViewModel} from "@views/DashboardView.model"; + +import {ProfileService} from "@contracts/profile.service"; + +export class LoginViewModel extends ViewModel { + public static readonly component: Component = LoginView; + public static readonly route: RouteAdapter = { + path: "/" + } + + private readonly routerService: RouterService; + private readonly profileService: ProfileService; + + public readonly onFocus: Delegate<[number]> = new Delegate<[number]>(); + public readonly pinLength: number = 6; + + public error: string = this.ref(""); + public isTrying: boolean = this.ref(false); + public step: number = this.ref(0); + public username: string = this.ref(""); + public pin: string[] = this.ref(Array(6).fill("")); + + public readonly title: string = this.computed(() => { + if (this.step == 0) { + return "Enter username"; + } + + return "Enter password"; + }); + + public constructor() { + super(); + + this.routerService = this.ctx.getService(RouterService); + this.profileService = this.ctx.getService(ProfileService); + } + + public async onUsernameSubmitBtn(): Promise { + this.step++; + await nextTick(); + await this.onFocus.invoke(0); + } + + public async onPinInput(index: number, event: InputEvent): Promise { + if (!event.target) { + return; + } + + this.pin[index] = (event.target as HTMLInputElement).value.slice(-1); + await this.onFocus.invoke(index + 1); + + if (index == this.pinLength - 1) { + await this.onFullPinInput(); + } + } + + public async onPinKeyDown(index: number, event: KeyboardEvent): Promise { + if (event.key == "Backspace") { + event.preventDefault(); + this.pin[index] = ""; + await this.onFocus.invoke(index - 1); + return; + } + + if (event.key == "ArrowLeft") { + event.preventDefault(); + await this.onFocus.invoke(index - 1); + } + + if (event.key == "ArrowRight") { + event.preventDefault(); + await this.onFocus.invoke(index + 1); + } + } + + public async onPinPaste(event: ClipboardEvent): Promise { + event.preventDefault(); + + if (!event.clipboardData) { + return; + } + + const text: string = event.clipboardData.getData("text").slice(0, this.pinLength); + text.split("").forEach((c: string, i: number): void => { + this.pin[i] = c; + }); + + const next: number = Math.min(text.length, this.pinLength - 1); + await this.onFocus.invoke(next); + } + + public async onFullPinInput(): Promise { + this.error = ""; + this.isTrying = true; + + if (!await this.profileService.login(this.username, this.pin.join(""))) { + this.error = "Username or password is wrong"; + this.isTrying = false; + return; + } + + await this.routerService.navigateTo(DashboardViewModel); + this.isTrying = false; + } + +} \ No newline at end of file diff --git a/dashboard/src/views/LoginView.vue b/dashboard/src/views/LoginView.vue new file mode 100644 index 0000000..2d52459 --- /dev/null +++ b/dashboard/src/views/LoginView.vue @@ -0,0 +1,92 @@ + + + \ No newline at end of file diff --git a/dashboard/tsconfig.app.json b/dashboard/tsconfig.app.json index d55ba38..edc093f 100644 --- a/dashboard/tsconfig.app.json +++ b/dashboard/tsconfig.app.json @@ -6,6 +6,7 @@ "vite/client" ], "allowArbitraryExtensions": true, + "allowImportingTsExtensions": false, /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true, @@ -17,6 +18,15 @@ ], "@controls/*": [ "./src/controls/*" + ], + "@contracts/*": [ + "./src/contracts/*" + ], + "@services/*": [ + "./src/services/*" + ], + "@icons/*": [ + "./src/icons/*" ] } }, diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index b0f2ef8..d062714 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -9,7 +9,10 @@ export default defineConfig({ resolve: { alias: { "@views": path.join(__dirname, "src", "views"), - "@controls": path.join(__dirname, "src", "controls") + "@controls": path.join(__dirname, "src", "controls"), + "@contracts": path.join(__dirname, "src", "contracts"), + "@services": path.join(__dirname, "src", "services"), + "@icons": path.join(__dirname, "src", "icons") } } }); From 2078fe5b370c6417c3ff379281e3e1f66ea5cbfd Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Mon, 20 Jul 2026 12:24:38 +0200 Subject: [PATCH 03/33] Design Update card --- dashboard/index.html | 2 +- dashboard/src/controls/UpdatePanel.model.ts | 2 +- dashboard/src/controls/UpdatePanel.vue | 26 ++++++++++++++++++++- dashboard/src/icons/LucideExternalLink.vue | 13 +++++++++++ dashboard/src/icons/LucideServer.vue | 13 +++++++++++ dashboard/src/views/DashboardView.vue | 5 +++- 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 dashboard/src/icons/LucideExternalLink.vue create mode 100644 dashboard/src/icons/LucideServer.vue diff --git a/dashboard/index.html b/dashboard/index.html index dfed269..a677969 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -6,7 +6,7 @@ AniStream - +
diff --git a/dashboard/src/controls/UpdatePanel.model.ts b/dashboard/src/controls/UpdatePanel.model.ts index b8eb174..c121f2d 100644 --- a/dashboard/src/controls/UpdatePanel.model.ts +++ b/dashboard/src/controls/UpdatePanel.model.ts @@ -1,5 +1,5 @@ import {UserControl} from "vue-mvvm"; export class UpdatePanelModel extends UserControl { - + public updateAvailable: boolean = this.ref(true); } \ No newline at end of file diff --git a/dashboard/src/controls/UpdatePanel.vue b/dashboard/src/controls/UpdatePanel.vue index 36efb52..8b357c6 100644 --- a/dashboard/src/controls/UpdatePanel.vue +++ b/dashboard/src/controls/UpdatePanel.vue @@ -1,7 +1,31 @@ diff --git a/dashboard/src/icons/LucideExternalLink.vue b/dashboard/src/icons/LucideExternalLink.vue new file mode 100644 index 0000000..619a03b --- /dev/null +++ b/dashboard/src/icons/LucideExternalLink.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideServer.vue b/dashboard/src/icons/LucideServer.vue new file mode 100644 index 0000000..ba62045 --- /dev/null +++ b/dashboard/src/icons/LucideServer.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/views/DashboardView.vue b/dashboard/src/views/DashboardView.vue index 69320c9..4ae3ddc 100644 --- a/dashboard/src/views/DashboardView.vue +++ b/dashboard/src/views/DashboardView.vue @@ -1,7 +1,10 @@ \ No newline at end of file From d0d128e636ae90991bd26582b757aa204276063d Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Mon, 20 Jul 2026 12:32:54 +0200 Subject: [PATCH 04/33] Implement Audit panel --- dashboard/src/controls/AuditPanel.model.ts | 5 +++ dashboard/src/controls/AuditPanel.vue | 42 ++++++++++++++++++++++ dashboard/src/controls/UpdatePanel.vue | 2 +- dashboard/src/views/DashboardView.vue | 4 ++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 dashboard/src/controls/AuditPanel.model.ts create mode 100644 dashboard/src/controls/AuditPanel.vue diff --git a/dashboard/src/controls/AuditPanel.model.ts b/dashboard/src/controls/AuditPanel.model.ts new file mode 100644 index 0000000..8ac451a --- /dev/null +++ b/dashboard/src/controls/AuditPanel.model.ts @@ -0,0 +1,5 @@ +import {UserControl} from "vue-mvvm"; + +export class AuditPanelModel extends UserControl { + +} \ No newline at end of file diff --git a/dashboard/src/controls/AuditPanel.vue b/dashboard/src/controls/AuditPanel.vue new file mode 100644 index 0000000..2992c0b --- /dev/null +++ b/dashboard/src/controls/AuditPanel.vue @@ -0,0 +1,42 @@ + + + diff --git a/dashboard/src/controls/UpdatePanel.vue b/dashboard/src/controls/UpdatePanel.vue index 8b357c6..84c104b 100644 --- a/dashboard/src/controls/UpdatePanel.vue +++ b/dashboard/src/controls/UpdatePanel.vue @@ -17,7 +17,7 @@ const vm: UpdatePanelModel = useUserControl(UpdatePanelModel);

Server Update available

-

Version 2.4.1 is ready. You are on v2.3.8

+

Version 2.4.1 is ready. You are on v2.3.8

diff --git a/dashboard/src/views/DashboardView.vue b/dashboard/src/views/DashboardView.vue index 4ae3ddc..46dff4f 100644 --- a/dashboard/src/views/DashboardView.vue +++ b/dashboard/src/views/DashboardView.vue @@ -1,10 +1,12 @@ \ No newline at end of file From c343f4ee9fcb92ba79dbbab5583a49d1c51b36f8 Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Sat, 25 Jul 2026 10:52:14 +0200 Subject: [PATCH 05/33] Complete basic shape --- dashboard/src/controls/AuditPanel.model.ts | 358 ++++++++++++++++++- dashboard/src/controls/AuditPanel.vue | 178 ++++++++- dashboard/src/controls/ProfilePanel.model.ts | 210 +++++++++++ dashboard/src/controls/ProfilePanel.vue | 218 +++++++++++ dashboard/src/icons/LucideChevronLeft.vue | 13 + dashboard/src/icons/LucideChevronRight.vue | 13 + dashboard/src/icons/LucideCircleCheck.vue | 13 + dashboard/src/icons/LucideCircleX.vue | 13 + dashboard/src/icons/LucideClock.vue | 13 + dashboard/src/icons/LucideCopy.vue | 13 + dashboard/src/icons/LucideListChecks.vue | 13 + dashboard/src/icons/LucideLogOut.vue | 13 + dashboard/src/icons/LucideMonitor.vue | 13 + dashboard/src/icons/LucidePencil.vue | 13 + dashboard/src/icons/LucidePlus.vue | 13 + dashboard/src/icons/LucideRefreshCw.vue | 13 + dashboard/src/icons/LucideSearch.vue | 13 + dashboard/src/icons/LucideShield.vue | 13 + dashboard/src/icons/LucideTrash2.vue | 13 + dashboard/src/icons/LucideUsers.vue | 13 + dashboard/src/icons/LucideX.vue | 13 + dashboard/src/views/DashboardView.model.ts | 40 ++- dashboard/src/views/DashboardView.vue | 84 ++++- 23 files changed, 1283 insertions(+), 26 deletions(-) create mode 100644 dashboard/src/controls/ProfilePanel.model.ts create mode 100644 dashboard/src/controls/ProfilePanel.vue create mode 100644 dashboard/src/icons/LucideChevronLeft.vue create mode 100644 dashboard/src/icons/LucideChevronRight.vue create mode 100644 dashboard/src/icons/LucideCircleCheck.vue create mode 100644 dashboard/src/icons/LucideCircleX.vue create mode 100644 dashboard/src/icons/LucideClock.vue create mode 100644 dashboard/src/icons/LucideCopy.vue create mode 100644 dashboard/src/icons/LucideListChecks.vue create mode 100644 dashboard/src/icons/LucideLogOut.vue create mode 100644 dashboard/src/icons/LucideMonitor.vue create mode 100644 dashboard/src/icons/LucidePencil.vue create mode 100644 dashboard/src/icons/LucidePlus.vue create mode 100644 dashboard/src/icons/LucideRefreshCw.vue create mode 100644 dashboard/src/icons/LucideSearch.vue create mode 100644 dashboard/src/icons/LucideShield.vue create mode 100644 dashboard/src/icons/LucideTrash2.vue create mode 100644 dashboard/src/icons/LucideUsers.vue create mode 100644 dashboard/src/icons/LucideX.vue diff --git a/dashboard/src/controls/AuditPanel.model.ts b/dashboard/src/controls/AuditPanel.model.ts index 8ac451a..f842663 100644 --- a/dashboard/src/controls/AuditPanel.model.ts +++ b/dashboard/src/controls/AuditPanel.model.ts @@ -1,5 +1,359 @@ import {UserControl} from "vue-mvvm"; +/** + * Kind of a sync job. Maps to the two source tables: + * - `series` → `sync_series_job` (metadata sync for a whole series) + * - `provider` → `sync_provider_job` (stream provider resolve for a single episode) + */ +export type JobKind = "series" | "provider"; + +/** + * Job status as stored in the `status` integer column of the sync tables. + */ +export const JobStatus = { + Pending: 0, + Running: 1, + Completed: 2, + Failed: 3 +} as const; + +export type JobStatus = typeof JobStatus[keyof typeof JobStatus]; + +/** + * A flattened view of a row from either `sync_series_job` or `sync_provider_job`. + */ +export interface AuditJob { + id: number; + kind: JobKind; + /** Human readable target (series title, or "series — SxxExx" for provider jobs). */ + target: string; + status: JobStatus; + started: string; + completed: string | null; + error: string | null; + /** Only set for provider jobs (`sync_provider_job.expires`). */ + expires: string | null; +} + +interface AuditStats { + total: number; + running: number; + completed: number; + failed: number; +} + export class AuditPanelModel extends UserControl { - -} \ No newline at end of file + public search: string = this.ref(""); + public kindFilter: JobKind | "all" = this.ref("all"); + public statusFilter: JobStatus | "all" = this.ref("all"); + public refreshing: boolean = this.ref(false); + + public readonly pageSize: number = 8; + public page: number = this.ref(1); + + /** The job whose error is shown in the details dialog, or `null` when closed. */ + public errorJob: AuditJob | null = this.ref(null); + public copied: boolean = this.ref(false); + + public constructor() { + super(); + + // Whenever the filter set changes, jump back to the first page so the + // user never lands on an out-of-range (now empty) page. + this.watch( + (): string => `${this.search}|${this.kindFilter}|${this.statusFilter}`, + (): void => { + this.page = 1; + } + ); + } + + // Static placeholder data — mirrors rows of sync_series_job / sync_provider_job. + private readonly jobs: AuditJob[] = this.readonly([ + { + id: 5218, + kind: "provider", + target: "Frieren: Beyond Journey's End — S1E24", + status: JobStatus.Completed, + started: "2026-07-21T09:14:02Z", + completed: "2026-07-21T09:14:37Z", + error: null, + expires: "2026-07-28T09:14:37Z" + }, + { + id: 5217, + kind: "provider", + target: "Frieren: Beyond Journey's End — S1E23", + status: JobStatus.Completed, + started: "2026-07-21T09:13:41Z", + completed: "2026-07-21T09:14:01Z", + error: null, + expires: "2026-07-28T09:14:01Z" + }, + { + id: 4103, + kind: "series", + target: "Solo Leveling", + status: JobStatus.Running, + started: "2026-07-21T09:12:55Z", + completed: null, + error: null, + expires: null + }, + { + id: 5216, + kind: "provider", + target: "Solo Leveling — S2E08", + status: JobStatus.Running, + started: "2026-07-21T09:12:58Z", + completed: null, + error: null, + expires: null + }, + { + id: 5215, + kind: "provider", + target: "Dan Da Dan — S1E11", + status: JobStatus.Failed, + started: "2026-07-21T08:47:10Z", + completed: "2026-07-21T08:47:52Z", + error: "No provider returned a playable stream (429 Too Many Requests)", + expires: null + }, + { + id: 4102, + kind: "series", + target: "Dan Da Dan", + status: JobStatus.Completed, + started: "2026-07-21T08:46:30Z", + completed: "2026-07-21T08:47:09Z", + error: null, + expires: null + }, + { + id: 5214, + kind: "provider", + target: "Kaiju No. 8 — S2E03", + status: JobStatus.Pending, + started: "2026-07-21T09:15:12Z", + completed: null, + error: null, + expires: null + }, + { + id: 4101, + kind: "series", + target: "Kaiju No. 8", + status: JobStatus.Failed, + started: "2026-07-20T22:03:01Z", + completed: "2026-07-20T22:03:05Z", + error: "System.Net.Http.HttpRequestException: The metadata source did not respond within the configured timeout of 30s.\n" + + " ---> System.Threading.Tasks.TaskCanceledException: The operation was canceled.\n" + + " ---> System.TimeoutException: The operation timed out.\n" + + " at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" + + " --- End of inner exception stack trace ---\n" + + " at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" + + " at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" + + " at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)\n" + + " --- End of inner exception stack trace ---\n" + + " at AniStream.Sync.Providers.MetadataClient.FetchSeriesAsync(String guid, CancellationToken ct) in /src/AniStream.Sync/Providers/MetadataClient.cs:line 142\n" + + " at AniStream.Sync.Jobs.SyncSeriesJobHandler.HandleAsync(SyncSeriesJob job, CancellationToken ct) in /src/AniStream.Sync/Jobs/SyncSeriesJobHandler.cs:line 87\n" + + " at AniStream.Sync.Worker.SyncDispatcher.RunAsync(CancellationToken ct) in /src/AniStream.Sync/Worker/SyncDispatcher.cs:line 53", + expires: null + }, + { + id: 5213, + kind: "provider", + target: "Oshi no Ko — S2E13", + status: JobStatus.Completed, + started: "2026-07-20T19:31:44Z", + completed: "2026-07-20T19:32:19Z", + error: null, + expires: "2026-07-27T19:32:19Z" + }, + { + id: 4100, + kind: "series", + target: "Oshi no Ko", + status: JobStatus.Completed, + started: "2026-07-20T19:30:58Z", + completed: "2026-07-20T19:31:43Z", + error: null, + expires: null + }, + { + id: 5212, + kind: "provider", + target: "Jujutsu Kaisen — S2E23", + status: JobStatus.Completed, + started: "2026-07-20T18:02:11Z", + completed: "2026-07-20T18:02:49Z", + error: null, + expires: "2026-07-27T18:02:49Z" + }, + { + id: 5211, + kind: "provider", + target: "Jujutsu Kaisen — S2E22", + status: JobStatus.Failed, + started: "2026-07-20T18:01:30Z", + completed: "2026-07-20T18:02:10Z", + error: "Stream expired before download could complete", + expires: null + } + ]); + + public readonly stats: AuditStats = this.computed(() => ({ + total: this.jobs.length, + running: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Running).length, + completed: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Completed).length, + failed: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Failed).length + })); + + public readonly filtered: AuditJob[] = this.computed(() => { + const term: string = this.search.trim().toLowerCase(); + + return this.jobs.filter((job: AuditJob): boolean => { + if (this.kindFilter !== "all" && job.kind !== this.kindFilter) { + return false; + } + + if (this.statusFilter !== "all" && job.status !== this.statusFilter) { + return false; + } + + if (term.length > 0) { + return job.target.toLowerCase().includes(term) || String(job.id).includes(term); + } + + return true; + }); + }); + + public readonly pageCount: number = this.computed( + () => Math.max(1, Math.ceil(this.filtered.length / this.pageSize)) + ); + + public readonly paged: AuditJob[] = this.computed(() => { + const start: number = (this.page - 1) * this.pageSize; + return this.filtered.slice(start, start + this.pageSize); + }); + + public readonly rangeStart: number = this.computed( + () => (this.filtered.length === 0 ? 0 : (this.page - 1) * this.pageSize + 1) + ); + + public readonly rangeEnd: number = this.computed( + () => Math.min(this.page * this.pageSize, this.filtered.length) + ); + + public setKindFilter(kind: JobKind | "all"): void { + this.kindFilter = kind; + } + + public goToPage(page: number): void { + this.page = Math.min(Math.max(1, page), this.pageCount); + } + + public prevPage(): void { + this.goToPage(this.page - 1); + } + + public nextPage(): void { + this.goToPage(this.page + 1); + } + + public openError(job: AuditJob): void { + this.copied = false; + this.errorJob = job; + } + + public closeError(): void { + this.errorJob = null; + } + + public async copyError(): Promise { + if (!this.errorJob?.error) { + return; + } + + await navigator.clipboard.writeText(this.errorJob.error); + this.copied = true; + } + + public async refresh(): Promise { + if (this.refreshing) { + return; + } + + // No data layer yet — simulate a short reload so the UI feels responsive. + this.refreshing = true; + await new Promise((resolve): void => { + setTimeout(resolve, 600); + }); + this.refreshing = false; + } + + public statusLabel(status: JobStatus): string { + switch (status) { + case JobStatus.Pending: + return "Pending"; + case JobStatus.Running: + return "Running"; + case JobStatus.Completed: + return "Completed"; + case JobStatus.Failed: + return "Failed"; + default: + return "Unknown"; + } + } + + public statusBadgeClass(status: JobStatus): string { + switch (status) { + case JobStatus.Pending: + return "badge-soft badge-neutral"; + case JobStatus.Running: + return "badge-soft badge-info"; + case JobStatus.Completed: + return "badge-soft badge-success"; + case JobStatus.Failed: + return "badge-soft badge-error"; + default: + return "badge-soft"; + } + } + + public formatDateTime(iso: string | null): string { + if (!iso) { + return "—"; + } + + const date: Date = new Date(iso); + return date.toLocaleString(undefined, { + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit" + }); + } + + public duration(job: AuditJob): string { + if (!job.completed) { + return job.status === JobStatus.Running ? "running…" : "—"; + } + + const ms: number = new Date(job.completed).getTime() - new Date(job.started).getTime(); + if (ms < 0) { + return "—"; + } + + const seconds: number = Math.round(ms / 1000); + if (seconds < 60) { + return `${seconds}s`; + } + + const minutes: number = Math.floor(seconds / 60); + return `${minutes}m ${seconds % 60}s`; + } +} diff --git a/dashboard/src/controls/AuditPanel.vue b/dashboard/src/controls/AuditPanel.vue index 2992c0b..acccd3d 100644 --- a/dashboard/src/controls/AuditPanel.vue +++ b/dashboard/src/controls/AuditPanel.vue @@ -1,42 +1,186 @@ diff --git a/dashboard/src/controls/ProfilePanel.model.ts b/dashboard/src/controls/ProfilePanel.model.ts new file mode 100644 index 0000000..0e60331 --- /dev/null +++ b/dashboard/src/controls/ProfilePanel.model.ts @@ -0,0 +1,210 @@ +import {UserControl} from "vue-mvvm"; + +/** + * A flattened view of a row from the `profile` table plus the two access flags + * that gate the dashboard and the regular streaming client. + */ +export interface Profile { + id: number; + uuid: string; + name: string; + backgroundColor: string; + eye: string; + mouth: string; + theme: string; + lang: string; + tosAccepted: boolean; + syncCatalog: boolean; + /** Whether the profile may sign in to this admin dashboard. */ + dashboardAccess: boolean; + /** Whether the profile may sign in to the normal streaming client. */ + clientAccess: boolean; +} + +interface SelectOption { + value: string; + label: string; +} + +export class ProfilePanelModel extends UserControl { + public readonly themeOptions: SelectOption[] = this.readonly([ + {value: "aniworld-light", label: "Light"}, + {value: "aniworld-dark", label: "Dark"} + ]); + + public readonly langOptions: SelectOption[] = this.readonly([ + {value: "en", label: "English"}, + {value: "de", label: "Deutsch"} + ]); + + public readonly eyeOptions: string[] = this.readonly(["•", "◕", "^", "˘", "o", "-"]); + public readonly mouthOptions: string[] = this.readonly(["‿", "◡", "ω", "▽", "o", "-"]); + + public search: string = this.ref(""); + + /** The draft currently being edited/created, or `null` when the editor is closed. */ + public editing: Profile | null = this.ref(null); + public isNew: boolean = this.ref(false); + + private profiles: Profile[] = this.ref([ + { + id: 1, + uuid: "b1e7c0a2-1f3d-4a5b-9c6d-0e1f2a3b4c5d", + name: "Administrator", + backgroundColor: "#c4b5fd", + eye: "•", + mouth: "‿", + theme: "aniworld-dark", + lang: "en", + tosAccepted: true, + syncCatalog: true, + dashboardAccess: true, + clientAccess: true + }, + { + id: 2, + uuid: "a2f8d1b3-2e4c-5b6a-8d7e-1f0a2b3c4d5e", + name: "Christoph", + backgroundColor: "#a7f3d0", + eye: "◕", + mouth: "◡", + theme: "aniworld-dark", + lang: "de", + tosAccepted: true, + syncCatalog: true, + dashboardAccess: true, + clientAccess: true + }, + { + id: 3, + uuid: "c3a9e2c4-3f5d-6c7b-9e8f-2a1b3c4d5e6f", + name: "Living Room", + backgroundColor: "#fde68a", + eye: "^", + mouth: "ω", + theme: "aniworld-light", + lang: "en", + tosAccepted: true, + syncCatalog: false, + dashboardAccess: false, + clientAccess: true + }, + { + id: 4, + uuid: "d4baf3d5-4a6e-7d8c-af9a-3b2c4d5e6f7a", + name: "Mia", + backgroundColor: "#fbcfe8", + eye: "˘", + mouth: "▽", + theme: "aniworld-light", + lang: "de", + tosAccepted: false, + syncCatalog: false, + dashboardAccess: false, + clientAccess: true + }, + { + id: 5, + uuid: "e5cb04e6-5b7f-8e9d-b0ab-4c3d5e6f7a8b", + name: "Kids", + backgroundColor: "#bae6fd", + eye: "o", + mouth: "o", + theme: "aniworld-light", + lang: "en", + tosAccepted: true, + syncCatalog: false, + dashboardAccess: false, + clientAccess: true + }, + { + id: 6, + uuid: "f6dc15f7-6c8a-9f0e-c1bc-5d4e6f7a8b9c", + name: "Sync Bot", + backgroundColor: "#e5e7eb", + eye: "-", + mouth: "-", + theme: "aniworld-dark", + lang: "en", + tosAccepted: true, + syncCatalog: true, + dashboardAccess: true, + clientAccess: false + } + ]); + + public readonly filtered: Profile[] = this.computed(() => { + const term: string = this.search.trim().toLowerCase(); + if (term.length === 0) { + return this.profiles; + } + + return this.profiles.filter((p: Profile): boolean => p.name.toLowerCase().includes(term)); + }); + + public readonly total: number = this.computed(() => this.profiles.length); + public readonly dashboardUsers: number = this.computed( + () => this.profiles.filter((p: Profile): boolean => p.dashboardAccess).length + ); + + public themeLabel(theme: string): string { + return this.themeOptions.find((o: SelectOption): boolean => o.value === theme)?.label ?? theme; + } + + public langLabel(lang: string): string { + return this.langOptions.find((o: SelectOption): boolean => o.value === lang)?.label ?? lang; + } + + public openCreate(): void { + this.isNew = true; + this.editing = { + id: this.nextId(), + uuid: crypto.randomUUID(), + name: "", + backgroundColor: "#c4b5fd", + eye: "•", + mouth: "‿", + theme: "aniworld-light", + lang: "en", + tosAccepted: false, + syncCatalog: false, + dashboardAccess: false, + clientAccess: true + }; + } + + public openEdit(profile: Profile): void { + this.isNew = false; + // Work on a copy so the card only updates once the user saves. + this.editing = {...profile}; + } + + public closeEditor(): void { + this.editing = null; + } + + public save(): void { + if (!this.editing || this.editing.name.trim().length === 0) { + return; + } + + const draft: Profile = this.editing; + const index: number = this.profiles.findIndex((p: Profile): boolean => p.id === draft.id); + + if (index >= 0) { + this.profiles = this.profiles.map((p: Profile): Profile => (p.id === draft.id ? draft : p)); + } else { + this.profiles = [...this.profiles, draft]; + } + + this.editing = null; + } + + public remove(profile: Profile): void { + this.profiles = this.profiles.filter((p: Profile): boolean => p.id !== profile.id); + } + + private nextId(): number { + return this.profiles.reduce((max: number, p: Profile): number => Math.max(max, p.id), 0) + 1; + } +} diff --git a/dashboard/src/controls/ProfilePanel.vue b/dashboard/src/controls/ProfilePanel.vue new file mode 100644 index 0000000..53ae044 --- /dev/null +++ b/dashboard/src/controls/ProfilePanel.vue @@ -0,0 +1,218 @@ + + + diff --git a/dashboard/src/icons/LucideChevronLeft.vue b/dashboard/src/icons/LucideChevronLeft.vue new file mode 100644 index 0000000..96f481c --- /dev/null +++ b/dashboard/src/icons/LucideChevronLeft.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideChevronRight.vue b/dashboard/src/icons/LucideChevronRight.vue new file mode 100644 index 0000000..c989bd7 --- /dev/null +++ b/dashboard/src/icons/LucideChevronRight.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideCircleCheck.vue b/dashboard/src/icons/LucideCircleCheck.vue new file mode 100644 index 0000000..ed319d5 --- /dev/null +++ b/dashboard/src/icons/LucideCircleCheck.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideCircleX.vue b/dashboard/src/icons/LucideCircleX.vue new file mode 100644 index 0000000..49e6d6c --- /dev/null +++ b/dashboard/src/icons/LucideCircleX.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideClock.vue b/dashboard/src/icons/LucideClock.vue new file mode 100644 index 0000000..d56ac3e --- /dev/null +++ b/dashboard/src/icons/LucideClock.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideCopy.vue b/dashboard/src/icons/LucideCopy.vue new file mode 100644 index 0000000..fbd2cc5 --- /dev/null +++ b/dashboard/src/icons/LucideCopy.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideListChecks.vue b/dashboard/src/icons/LucideListChecks.vue new file mode 100644 index 0000000..34c45da --- /dev/null +++ b/dashboard/src/icons/LucideListChecks.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideLogOut.vue b/dashboard/src/icons/LucideLogOut.vue new file mode 100644 index 0000000..3f4039f --- /dev/null +++ b/dashboard/src/icons/LucideLogOut.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideMonitor.vue b/dashboard/src/icons/LucideMonitor.vue new file mode 100644 index 0000000..f42ab78 --- /dev/null +++ b/dashboard/src/icons/LucideMonitor.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucidePencil.vue b/dashboard/src/icons/LucidePencil.vue new file mode 100644 index 0000000..9589eb2 --- /dev/null +++ b/dashboard/src/icons/LucidePencil.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucidePlus.vue b/dashboard/src/icons/LucidePlus.vue new file mode 100644 index 0000000..357aaae --- /dev/null +++ b/dashboard/src/icons/LucidePlus.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideRefreshCw.vue b/dashboard/src/icons/LucideRefreshCw.vue new file mode 100644 index 0000000..a5fb792 --- /dev/null +++ b/dashboard/src/icons/LucideRefreshCw.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideSearch.vue b/dashboard/src/icons/LucideSearch.vue new file mode 100644 index 0000000..57e4142 --- /dev/null +++ b/dashboard/src/icons/LucideSearch.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideShield.vue b/dashboard/src/icons/LucideShield.vue new file mode 100644 index 0000000..ae85e79 --- /dev/null +++ b/dashboard/src/icons/LucideShield.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideTrash2.vue b/dashboard/src/icons/LucideTrash2.vue new file mode 100644 index 0000000..58092ee --- /dev/null +++ b/dashboard/src/icons/LucideTrash2.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideUsers.vue b/dashboard/src/icons/LucideUsers.vue new file mode 100644 index 0000000..285091a --- /dev/null +++ b/dashboard/src/icons/LucideUsers.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideX.vue b/dashboard/src/icons/LucideX.vue new file mode 100644 index 0000000..f198f6d --- /dev/null +++ b/dashboard/src/icons/LucideX.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/views/DashboardView.model.ts b/dashboard/src/views/DashboardView.model.ts index c2239de..a62e157 100644 --- a/dashboard/src/views/DashboardView.model.ts +++ b/dashboard/src/views/DashboardView.model.ts @@ -1,12 +1,48 @@ import type {Component} from "vue"; import {ViewModel} from "vue-mvvm"; -import type {RouteAdapter} from "vue-mvvm/router"; +import {type RouteAdapter, RouterService} from "vue-mvvm/router"; import DashboardView from "@views/DashboardView.vue"; +import {LoginViewModel} from "@views/LoginView.model"; + +export type DashboardTab = "audit" | "profiles"; + +interface OverviewStat { + label: string; + value: string; + hint: string; + tone: string; +} export class DashboardViewModel extends ViewModel { public static readonly component: Component = DashboardView; public static readonly route: RouteAdapter = { path: "/home" } -} \ No newline at end of file + + private readonly routerService: RouterService; + + public activeTab: DashboardTab = this.ref("audit"); + + // Static overview figures until the service layer is wired up. + public readonly stats: OverviewStat[] = this.readonly([ + {label: "Active profiles", value: "6", hint: "3 with dashboard access", tone: "text-primary"}, + {label: "Running jobs", value: "2", hint: "syncing right now", tone: "text-info"}, + {label: "Completed today", value: "146", hint: "+18% vs. yesterday", tone: "text-success"}, + {label: "Failed jobs", value: "3", hint: "needs attention", tone: "text-error"} + ]); + + public constructor() { + super(); + + this.routerService = this.ctx.getService(RouterService); + } + + public setTab(tab: DashboardTab): void { + this.activeTab = tab; + } + + public async logout(): Promise { + await this.routerService.navigateTo(LoginViewModel); + } +} diff --git a/dashboard/src/views/DashboardView.vue b/dashboard/src/views/DashboardView.vue index 46dff4f..7ec4967 100644 --- a/dashboard/src/views/DashboardView.vue +++ b/dashboard/src/views/DashboardView.vue @@ -1,12 +1,86 @@ \ No newline at end of file + From e6217d6ce7a60363057856756c70e5003a116cc8 Mon Sep 17 00:00:00 2001 From: Christoph Koschel Date: Sat, 25 Jul 2026 22:10:27 +0200 Subject: [PATCH 06/33] Make Login and Audit Panel functional --- dashboard/package-lock.json | 25 +- dashboard/package.json | 4 +- dashboard/src/AppEnv.ts | 3 + dashboard/src/contracts/api.service.ts | 22 ++ .../src/contracts/information.service.ts | 8 + .../src/controls/AuditErrorDialog.model.ts | 47 +++ dashboard/src/controls/AuditErrorDialog.vue | 52 +++ dashboard/src/controls/AuditPanel.model.ts | 348 +++--------------- dashboard/src/controls/AuditPanel.vue | 164 ++++----- dashboard/src/controls/AuditPanelRow.model.ts | 97 +++++ dashboard/src/controls/AuditPanelRow.vue | 77 ++++ dashboard/src/icons/LucideChevronsLeft.vue | 13 + dashboard/src/icons/LucideChevronsRight.vue | 13 + dashboard/src/models/audit.model.ts | 23 ++ dashboard/src/services/api.service.ts | 78 ++++ dashboard/src/services/information.service.ts | 26 ++ dashboard/src/services/profile.service.ts | 29 +- dashboard/src/services/shared.ts | 4 + dashboard/src/utils/http.ts | 133 +++++++ dashboard/src/views/LoginView.vue | 10 +- dashboard/tsconfig.app.json | 12 + dashboard/vite.config.ts | 6 +- .../AniStream.Models/Models/ProfileModel.cs | 16 +- .../Contracts/IProviderSyncService.cs | 4 +- .../Contracts/ISeasonService.cs | 2 +- .../Contracts/ISeriesSyncService.cs | 2 + .../Contracts/IUserService.cs | 6 +- .../Services/ProviderSyncServiceImpl.cs | 9 + .../Services/SeasonServiceImpl.cs | 7 +- .../Services/SeriesSyncServiceImpl.cs | 9 + .../Services/UserServiceImpl.cs | 17 +- server/AniStream.Tests/UserServiceTests.cs | 20 +- .../Controllers/CredentialsController.cs | 10 + .../Controllers/EpisodeController.cs | 2 +- .../AniStream/Controllers/GenreController.cs | 2 +- .../Controllers/InformationController.cs | 119 ++++++ .../AniStream/Controllers/ListController.cs | 2 +- .../Controllers/ProfileController.cs | 2 +- .../Controllers/ResourceController.cs | 2 +- .../AniStream/Controllers/SeasonController.cs | 2 +- .../AniStream/Controllers/SeriesController.cs | 2 +- .../Controllers/WatchListController.cs | 2 +- .../Controllers/WatchTimeController.cs | 2 +- server/AniStream/DTO/AuditModel.cs | 30 ++ server/AniStream/DTO/ProfileModel.cs | 5 +- server/AniStream/Program.cs | 23 +- .../AniStream/Services/CredentialsService.cs | 2 +- server/AniStream/Utils/Roles.cs | 7 + 48 files changed, 1042 insertions(+), 458 deletions(-) create mode 100644 dashboard/src/AppEnv.ts create mode 100644 dashboard/src/contracts/api.service.ts create mode 100644 dashboard/src/contracts/information.service.ts create mode 100644 dashboard/src/controls/AuditErrorDialog.model.ts create mode 100644 dashboard/src/controls/AuditErrorDialog.vue create mode 100644 dashboard/src/controls/AuditPanelRow.model.ts create mode 100644 dashboard/src/controls/AuditPanelRow.vue create mode 100644 dashboard/src/icons/LucideChevronsLeft.vue create mode 100644 dashboard/src/icons/LucideChevronsRight.vue create mode 100644 dashboard/src/models/audit.model.ts create mode 100644 dashboard/src/services/api.service.ts create mode 100644 dashboard/src/services/information.service.ts create mode 100644 dashboard/src/utils/http.ts create mode 100644 server/AniStream/DTO/AuditModel.cs create mode 100644 server/AniStream/Utils/Roles.cs diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index abd0a31..589c2d3 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -9,10 +9,12 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.3.3", + "@types/luxon": "^3.7.2", "daisyui": "^5.6.18", + "luxon": "^3.7.2", "tailwindcss": "^4.3.3", "vue": "^3.5.39", - "vue-mvvm": "^0.10.0" + "vue-mvvm": "^0.10.1" }, "devDependencies": { "@types/node": "^24.13.3", @@ -1053,6 +1055,12 @@ "license": "MIT", "peer": true }, + "node_modules/@types/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -1808,6 +1816,15 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2303,9 +2320,9 @@ } }, "node_modules/vue-mvvm": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/vue-mvvm/-/vue-mvvm-0.10.0.tgz", - "integrity": "sha512-TY2LDtw8cei0/z+X6o231/hDywmzvgHLI91a3zNwXRhMQ+OGKu0abnDGYnv/3oyn9rfYJSUNZ99fF9rZIkEV6Q==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/vue-mvvm/-/vue-mvvm-0.10.1.tgz", + "integrity": "sha512-Jz8U1zEppM+lOGRutZT36FsW38fKNnFyL9Wg32CM8ps5suJUoJ1mz8oC27FLLMs8d8zjoVaHOxTVDdtdImm5cA==", "license": "MIT", "peerDependencies": { "vue": "^3.5.24", diff --git a/dashboard/package.json b/dashboard/package.json index c12cacf..0812300 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -10,10 +10,12 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "@types/luxon": "^3.7.2", "daisyui": "^5.6.18", + "luxon": "^3.7.2", "tailwindcss": "^4.3.3", "vue": "^3.5.39", - "vue-mvvm": "^0.10.0" + "vue-mvvm": "^0.10.1" }, "devDependencies": { "@types/node": "^24.13.3", diff --git a/dashboard/src/AppEnv.ts b/dashboard/src/AppEnv.ts new file mode 100644 index 0000000..91bd255 --- /dev/null +++ b/dashboard/src/AppEnv.ts @@ -0,0 +1,3 @@ +export const isTesting: boolean = import.meta.env.MODE === "test"; +export const isDev: boolean = import.meta.env.MODE === "development"; +export const isProd: boolean = !isTesting && !isDev; diff --git a/dashboard/src/contracts/api.service.ts b/dashboard/src/contracts/api.service.ts new file mode 100644 index 0000000..c9f65df --- /dev/null +++ b/dashboard/src/contracts/api.service.ts @@ -0,0 +1,22 @@ +import {ServiceKey} from "vue-mvvm"; + +export type PathParts = Array; +export type QueryTypes = string | number | boolean; +export type PathParameter = PathParts | RouteBuilder; + +export interface RouteBuilder { + path: PathParts; + query: Record; +} + +export interface ApiService { + get(def: PathParameter): Promise; + + post(def: PathParameter, body: Body): Promise; + + put(def: PathParameter, body: Body): Promise; + + delete(def: PathParameter): Promise; +} + +export const ApiService: ServiceKey = new ServiceKey("api.service"); \ No newline at end of file diff --git a/dashboard/src/contracts/information.service.ts b/dashboard/src/contracts/information.service.ts new file mode 100644 index 0000000..79578c8 --- /dev/null +++ b/dashboard/src/contracts/information.service.ts @@ -0,0 +1,8 @@ +import {ServiceKey} from "vue-mvvm"; +import type {AuditModel} from "@models/audit.model"; + +export interface InformationService { + getAudits(): Promise +} + +export const InformationService: ServiceKey = new ServiceKey("information.service"); \ No newline at end of file diff --git a/dashboard/src/controls/AuditErrorDialog.model.ts b/dashboard/src/controls/AuditErrorDialog.model.ts new file mode 100644 index 0000000..e4f42df --- /dev/null +++ b/dashboard/src/controls/AuditErrorDialog.model.ts @@ -0,0 +1,47 @@ +import type {Component} from "vue"; +import {DialogControl} from "vue-mvvm/dialog"; + +import AuditErrorDialog from "@controls/AuditErrorDialog.vue"; + +import type {AuditModel} from "@models/audit.model"; + +export class AuditErrorDialogModel extends DialogControl { + public static readonly component: Component = AuditErrorDialog; + private readonly model: AuditModel; + + public copied: boolean = this.ref(false); + public isOpen: boolean = this.ref(false); + + public readonly id: number = this.computed(() => this.model.job_id); + public readonly title: string = this.computed(() => this.model.job_name); + public readonly error: string | null = this.computed(() => this.model.error); + + public constructor(model: AuditModel) { + super(); + + this.model = model; + } + + protected async onOpen(): Promise { + this.isOpen = true; + } + + protected async onClose(): Promise { + this.isOpen = false; + + await new Promise(resolve => setTimeout(resolve, 200)); + + this.destroy(); + } + + public async onCopyErrorBtn(): Promise { + if (!this.error) { + return; + } + + await navigator.clipboard.writeText(this.error); + this.copied = true; + } + + +} \ No newline at end of file diff --git a/dashboard/src/controls/AuditErrorDialog.vue b/dashboard/src/controls/AuditErrorDialog.vue new file mode 100644 index 0000000..8aa8b7a --- /dev/null +++ b/dashboard/src/controls/AuditErrorDialog.vue @@ -0,0 +1,52 @@ + + + + + \ No newline at end of file diff --git a/dashboard/src/controls/AuditPanel.model.ts b/dashboard/src/controls/AuditPanel.model.ts index f842663..da363f0 100644 --- a/dashboard/src/controls/AuditPanel.model.ts +++ b/dashboard/src/controls/AuditPanel.model.ts @@ -1,359 +1,101 @@ import {UserControl} from "vue-mvvm"; -/** - * Kind of a sync job. Maps to the two source tables: - * - `series` → `sync_series_job` (metadata sync for a whole series) - * - `provider` → `sync_provider_job` (stream provider resolve for a single episode) - */ -export type JobKind = "series" | "provider"; +import {InformationService} from "@contracts//information.service"; -/** - * Job status as stored in the `status` integer column of the sync tables. - */ -export const JobStatus = { - Pending: 0, - Running: 1, - Completed: 2, - Failed: 3 -} as const; +import {AuditKind, type AuditModel, AuditStatus} from "@models/audit.model"; -export type JobStatus = typeof JobStatus[keyof typeof JobStatus]; +export class AuditPanelModel extends UserControl { + private readonly informationService: InformationService; -/** - * A flattened view of a row from either `sync_series_job` or `sync_provider_job`. - */ -export interface AuditJob { - id: number; - kind: JobKind; - /** Human readable target (series title, or "series — SxxExx" for provider jobs). */ - target: string; - status: JobStatus; - started: string; - completed: string | null; - error: string | null; - /** Only set for provider jobs (`sync_provider_job.expires`). */ - expires: string | null; -} + private items: AuditModel[] = this.ref([]); -interface AuditStats { - total: number; - running: number; - completed: number; - failed: number; -} + public readonly pageSize: number = 20; -export class AuditPanelModel extends UserControl { public search: string = this.ref(""); - public kindFilter: JobKind | "all" = this.ref("all"); - public statusFilter: JobStatus | "all" = this.ref("all"); + public statusFilter: AuditStatus | -1 = this.ref(-1); public refreshing: boolean = this.ref(false); + public kindFilter: AuditKind | -1 = this.ref(-1); - public readonly pageSize: number = 8; public page: number = this.ref(1); - /** The job whose error is shown in the details dialog, or `null` when closed. */ - public errorJob: AuditJob | null = this.ref(null); - public copied: boolean = this.ref(false); - - public constructor() { - super(); - - // Whenever the filter set changes, jump back to the first page so the - // user never lands on an out-of-range (now empty) page. - this.watch( - (): string => `${this.search}|${this.kindFilter}|${this.statusFilter}`, - (): void => { - this.page = 1; - } - ); - } - - // Static placeholder data — mirrors rows of sync_series_job / sync_provider_job. - private readonly jobs: AuditJob[] = this.readonly([ - { - id: 5218, - kind: "provider", - target: "Frieren: Beyond Journey's End — S1E24", - status: JobStatus.Completed, - started: "2026-07-21T09:14:02Z", - completed: "2026-07-21T09:14:37Z", - error: null, - expires: "2026-07-28T09:14:37Z" - }, - { - id: 5217, - kind: "provider", - target: "Frieren: Beyond Journey's End — S1E23", - status: JobStatus.Completed, - started: "2026-07-21T09:13:41Z", - completed: "2026-07-21T09:14:01Z", - error: null, - expires: "2026-07-28T09:14:01Z" - }, - { - id: 4103, - kind: "series", - target: "Solo Leveling", - status: JobStatus.Running, - started: "2026-07-21T09:12:55Z", - completed: null, - error: null, - expires: null - }, - { - id: 5216, - kind: "provider", - target: "Solo Leveling — S2E08", - status: JobStatus.Running, - started: "2026-07-21T09:12:58Z", - completed: null, - error: null, - expires: null - }, - { - id: 5215, - kind: "provider", - target: "Dan Da Dan — S1E11", - status: JobStatus.Failed, - started: "2026-07-21T08:47:10Z", - completed: "2026-07-21T08:47:52Z", - error: "No provider returned a playable stream (429 Too Many Requests)", - expires: null - }, - { - id: 4102, - kind: "series", - target: "Dan Da Dan", - status: JobStatus.Completed, - started: "2026-07-21T08:46:30Z", - completed: "2026-07-21T08:47:09Z", - error: null, - expires: null - }, - { - id: 5214, - kind: "provider", - target: "Kaiju No. 8 — S2E03", - status: JobStatus.Pending, - started: "2026-07-21T09:15:12Z", - completed: null, - error: null, - expires: null - }, - { - id: 4101, - kind: "series", - target: "Kaiju No. 8", - status: JobStatus.Failed, - started: "2026-07-20T22:03:01Z", - completed: "2026-07-20T22:03:05Z", - error: "System.Net.Http.HttpRequestException: The metadata source did not respond within the configured timeout of 30s.\n" - + " ---> System.Threading.Tasks.TaskCanceledException: The operation was canceled.\n" - + " ---> System.TimeoutException: The operation timed out.\n" - + " at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" - + " --- End of inner exception stack trace ---\n" - + " at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" - + " at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)\n" - + " at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)\n" - + " --- End of inner exception stack trace ---\n" - + " at AniStream.Sync.Providers.MetadataClient.FetchSeriesAsync(String guid, CancellationToken ct) in /src/AniStream.Sync/Providers/MetadataClient.cs:line 142\n" - + " at AniStream.Sync.Jobs.SyncSeriesJobHandler.HandleAsync(SyncSeriesJob job, CancellationToken ct) in /src/AniStream.Sync/Jobs/SyncSeriesJobHandler.cs:line 87\n" - + " at AniStream.Sync.Worker.SyncDispatcher.RunAsync(CancellationToken ct) in /src/AniStream.Sync/Worker/SyncDispatcher.cs:line 53", - expires: null - }, - { - id: 5213, - kind: "provider", - target: "Oshi no Ko — S2E13", - status: JobStatus.Completed, - started: "2026-07-20T19:31:44Z", - completed: "2026-07-20T19:32:19Z", - error: null, - expires: "2026-07-27T19:32:19Z" - }, - { - id: 4100, - kind: "series", - target: "Oshi no Ko", - status: JobStatus.Completed, - started: "2026-07-20T19:30:58Z", - completed: "2026-07-20T19:31:43Z", - error: null, - expires: null - }, - { - id: 5212, - kind: "provider", - target: "Jujutsu Kaisen — S2E23", - status: JobStatus.Completed, - started: "2026-07-20T18:02:11Z", - completed: "2026-07-20T18:02:49Z", - error: null, - expires: "2026-07-27T18:02:49Z" - }, - { - id: 5211, - kind: "provider", - target: "Jujutsu Kaisen — S2E22", - status: JobStatus.Failed, - started: "2026-07-20T18:01:30Z", - completed: "2026-07-20T18:02:10Z", - error: "Stream expired before download could complete", - expires: null - } - ]); - - public readonly stats: AuditStats = this.computed(() => ({ - total: this.jobs.length, - running: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Running).length, - completed: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Completed).length, - failed: this.jobs.filter((j: AuditJob): boolean => j.status === JobStatus.Failed).length - })); + public readonly total: number = this.computed(() => this.items.length); + public readonly queued: number = this.computed(() => this.items.filter(item => item.status == AuditStatus.Queued).length); + public readonly running: number = this.computed(() => this.items.filter(item => item.status == AuditStatus.Processing).length); + public readonly completed: number = this.computed(() => this.items.filter(item => item.status == AuditStatus.Completed).length); + public readonly failed: number = this.computed(() => this.items.filter(item => item.status == AuditStatus.Failed).length); - public readonly filtered: AuditJob[] = this.computed(() => { + public readonly pageCount: number = this.computed(() => Math.max(1, Math.ceil(this.filtered.length / this.pageSize))); + public readonly filtered: AuditModel[] = this.computed(() => { const term: string = this.search.trim().toLowerCase(); - return this.jobs.filter((job: AuditJob): boolean => { - if (this.kindFilter !== "all" && job.kind !== this.kindFilter) { + return this.items.filter(item => { + if (this.kindFilter != -1 && item.kind != this.kindFilter) { return false; } - if (this.statusFilter !== "all" && job.status !== this.statusFilter) { + if (this.statusFilter != -1 && item.status != this.statusFilter) { return false; } if (term.length > 0) { - return job.target.toLowerCase().includes(term) || String(job.id).includes(term); + return item.job_name.toLowerCase().includes(term) || String(item.job_id).includes(term); } return true; }); }); - - public readonly pageCount: number = this.computed( - () => Math.max(1, Math.ceil(this.filtered.length / this.pageSize)) - ); - - public readonly paged: AuditJob[] = this.computed(() => { + public readonly paged: AuditModel[] = this.computed(() => { const start: number = (this.page - 1) * this.pageSize; return this.filtered.slice(start, start + this.pageSize); }); + public readonly rangeStart: number = this.computed(() => (this.filtered.length == 0 ? 0 : (this.page - 1) * this.pageSize + 1)); + public readonly rangeEnd: number = this.computed(() => Math.min(this.page * this.pageSize, this.filtered.length)); - public readonly rangeStart: number = this.computed( - () => (this.filtered.length === 0 ? 0 : (this.page - 1) * this.pageSize + 1) - ); - public readonly rangeEnd: number = this.computed( - () => Math.min(this.page * this.pageSize, this.filtered.length) - ); + public constructor() { + super(); - public setKindFilter(kind: JobKind | "all"): void { - this.kindFilter = kind; - } + this.informationService = this.ctx.getService(InformationService); - public goToPage(page: number): void { - this.page = Math.min(Math.max(1, page), this.pageCount); + this.watch(() => this.search, () => this.page = 1); + this.watch(() => this.kindFilter, () => this.page = 1); + this.watch(() => this.statusFilter, () => this.page = 1); } - public prevPage(): void { - this.goToPage(this.page - 1); + protected async mounted(): Promise { + await this.onRefreshBtn(); } - public nextPage(): void { - this.goToPage(this.page + 1); + public onPageBtn(page: number): void { + this.page = Math.min(Math.max(1, page), this.pageCount); } - public openError(job: AuditJob): void { - this.copied = false; - this.errorJob = job; + public onFirstPageBtn(): void { + this.onPageBtn(1); } - public closeError(): void { - this.errorJob = null; + public onPrevPageBtn(): void { + this.onPageBtn(this.page - 1); } - public async copyError(): Promise { - if (!this.errorJob?.error) { - return; - } + public onNextPageBtn(): void { + this.onPageBtn(this.page + 1); + } - await navigator.clipboard.writeText(this.errorJob.error); - this.copied = true; + public onLastPageBtn(): void { + this.onPageBtn(this.pageCount); } - public async refresh(): Promise { + public async onRefreshBtn(): Promise { if (this.refreshing) { return; } - // No data layer yet — simulate a short reload so the UI feels responsive. this.refreshing = true; - await new Promise((resolve): void => { - setTimeout(resolve, 600); - }); - this.refreshing = false; - } - public statusLabel(status: JobStatus): string { - switch (status) { - case JobStatus.Pending: - return "Pending"; - case JobStatus.Running: - return "Running"; - case JobStatus.Completed: - return "Completed"; - case JobStatus.Failed: - return "Failed"; - default: - return "Unknown"; - } - } - - public statusBadgeClass(status: JobStatus): string { - switch (status) { - case JobStatus.Pending: - return "badge-soft badge-neutral"; - case JobStatus.Running: - return "badge-soft badge-info"; - case JobStatus.Completed: - return "badge-soft badge-success"; - case JobStatus.Failed: - return "badge-soft badge-error"; - default: - return "badge-soft"; - } - } - - public formatDateTime(iso: string | null): string { - if (!iso) { - return "—"; - } + const items: AuditModel[] = await this.informationService.getAudits(); + this.items = items.sort((a, b) => !b.started_at ? -1 : a.started_at > b.started_at ? -1 : 1); - const date: Date = new Date(iso); - return date.toLocaleString(undefined, { - month: "short", - day: "2-digit", - hour: "2-digit", - minute: "2-digit" - }); - } - - public duration(job: AuditJob): string { - if (!job.completed) { - return job.status === JobStatus.Running ? "running…" : "—"; - } - - const ms: number = new Date(job.completed).getTime() - new Date(job.started).getTime(); - if (ms < 0) { - return "—"; - } - - const seconds: number = Math.round(ms / 1000); - if (seconds < 60) { - return `${seconds}s`; - } - - const minutes: number = Math.floor(seconds / 60); - return `${minutes}m ${seconds % 60}s`; + this.refreshing = false; } } diff --git a/dashboard/src/controls/AuditPanel.vue b/dashboard/src/controls/AuditPanel.vue index acccd3d..7dddb70 100644 --- a/dashboard/src/controls/AuditPanel.vue +++ b/dashboard/src/controls/AuditPanel.vue @@ -1,25 +1,24 @@ diff --git a/dashboard/src/controls/AuditPanelRow.model.ts b/dashboard/src/controls/AuditPanelRow.model.ts new file mode 100644 index 0000000..19ecbe1 --- /dev/null +++ b/dashboard/src/controls/AuditPanelRow.model.ts @@ -0,0 +1,97 @@ +import {UserControl} from "vue-mvvm"; +import {DialogService} from "vue-mvvm/dialog"; +import {DateTime, type Duration} from "luxon"; + +import {AuditErrorDialogModel} from "@controls/AuditErrorDialog.model"; + +import {AuditKind, type AuditModel, AuditStatus} from "@models/audit.model"; + +export class AuditPanelRowModel extends UserControl { + private readonly dialogService: DialogService; + + private readonly model: AuditModel; + + public readonly kind: string = this.computed(() => { + switch (this.model.kind) { + case AuditKind.Series: + return "Series"; + case AuditKind.Provider: + return "Provider"; + default: + return "N/A"; + } + }); + + public readonly expires: string | null = this.computed(() => { + if (!this.model.expires) { + return null; + } + + return DateTime.fromISO(this.model.expires).toLocaleString(DateTime.DATE_SHORT); + }); + + public readonly statusBadgeClasses: string = this.computed(() => { + switch (this.model.status) { + case AuditStatus.Queued: + return "badge-neutral"; + case AuditStatus.Processing: + return "badge-info"; + case AuditStatus.Completed: + return "badge-success"; + case AuditStatus.Failed: + return "badge-error"; + default: + return ""; + } + }); + + public readonly statusLabel: string = this.computed(() => { + switch (this.model.status) { + case AuditStatus.Queued: + return "Queued"; + case AuditStatus.Processing: + return "Running"; + case AuditStatus.Completed: + return "Completed"; + case AuditStatus.Failed: + return "Failed"; + default: + return "N/A"; + } + }); + + public readonly started: string | null = this.computed(() => DateTime.fromISO(this.model.started_at).toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)); + + public readonly completed: string | null = this.computed(() => { + if (!this.model.finished_at) { + return null; + } + + return DateTime.fromISO(this.model.started_at).toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS); + }); + + public duration: string | null = this.computed(() => { + if (!this.model.finished_at) { + return null; + } + + const start: DateTime = DateTime.fromISO(this.model.started_at); + const finished: DateTime = DateTime.fromISO(this.model.finished_at); + + const duration: Duration = finished.diff(start); + return duration.toFormat("m'm' s's'"); + }); + + public constructor(model: AuditModel) { + super(); + + this.dialogService = this.ctx.getService(DialogService); + + this.model = model; + } + + public async onErrorBtn(): Promise { + const dialog: AuditErrorDialogModel = this.dialogService.initDialog(AuditErrorDialogModel, this.model); + await dialog.openDialog(); + } +} \ No newline at end of file diff --git a/dashboard/src/controls/AuditPanelRow.vue b/dashboard/src/controls/AuditPanelRow.vue new file mode 100644 index 0000000..cead5c5 --- /dev/null +++ b/dashboard/src/controls/AuditPanelRow.vue @@ -0,0 +1,77 @@ + + + + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideChevronsLeft.vue b/dashboard/src/icons/LucideChevronsLeft.vue new file mode 100644 index 0000000..05a8f24 --- /dev/null +++ b/dashboard/src/icons/LucideChevronsLeft.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/icons/LucideChevronsRight.vue b/dashboard/src/icons/LucideChevronsRight.vue new file mode 100644 index 0000000..7e51052 --- /dev/null +++ b/dashboard/src/icons/LucideChevronsRight.vue @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/dashboard/src/models/audit.model.ts b/dashboard/src/models/audit.model.ts new file mode 100644 index 0000000..22014f0 --- /dev/null +++ b/dashboard/src/models/audit.model.ts @@ -0,0 +1,23 @@ +export enum AuditKind { + Series, + Provider +} + +export enum AuditStatus { + Queued, + Processing, + Completed, + Failed +} + +export interface AuditModel { + job_id: number; + kind: AuditKind; + job_name: string; + provider: string; + status: AuditStatus; + started_at: string; + finished_at: string | null; + error: string | null; + expires: string | null; +} \ No newline at end of file diff --git a/dashboard/src/services/api.service.ts b/dashboard/src/services/api.service.ts new file mode 100644 index 0000000..ea0ac95 --- /dev/null +++ b/dashboard/src/services/api.service.ts @@ -0,0 +1,78 @@ +import {ApiService, type PathParameter} from "@contracts/api.service"; + +import type {ServiceDeclaration} from "@services/shared"; + +import * as http from "@utils/http"; + +import * as AppEnv from "@AppEnv"; + +class ApiServiceImpl implements ApiService { + public static HEADERS: [string, string][] = [["Content-Type", "application/json"]]; + + public async get(def: PathParameter): Promise { + const url: string = this.buildURL(def); + return await http.get(url, ApiServiceImpl.HEADERS).json(); + } + + public async post(def: PathParameter, body: Body): Promise { + let data: string | undefined; + + if (body == null) { + data = undefined; + } else if (typeof body == "object") { + data = JSON.stringify(body); + } else { + data = body; + } + + const url: string = this.buildURL(def); + return await http.post(url, data, ApiServiceImpl.HEADERS).json(); + } + + public async put(def: PathParameter, body: Body): Promise { + let data: string; + + if (typeof body == "object") { + data = JSON.stringify(body); + } else { + data = body; + } + + const url: string = this.buildURL(def); + return await http.put(url, data, ApiServiceImpl.HEADERS).json(); + } + + public async delete(def: PathParameter): Promise { + const url: string = this.buildURL(def); + return await http.delete$(url, ApiServiceImpl.HEADERS).json(); + } + + protected buildURL(def: PathParameter, baseOverride: string | null = null): string { + if (Array.isArray(def)) { + const base: string = baseOverride ?? this.getApiUrl(); + const path: string = def.join("/"); + return `${base}/${path}`; + } + + const base: string = this.buildURL(def.path); + const query: string = Object.entries(def.query).map(([key, value]) => Array.isArray(value) + ? value.map(value => `${key}=${value}`).join("&") + : `${key}=${value}` + ).join("&"); + + return `${base}?${query}`; + } + + protected getApiUrl(): string { + if (AppEnv.isDev) { + return "http://localhost:5000"; + } + + return window.location.origin; + } +} + +export default { + key: ApiService, + ctor: ApiServiceImpl +} satisfies ServiceDeclaration; \ No newline at end of file diff --git a/dashboard/src/services/information.service.ts b/dashboard/src/services/information.service.ts new file mode 100644 index 0000000..ab23c2f --- /dev/null +++ b/dashboard/src/services/information.service.ts @@ -0,0 +1,26 @@ +import type {ReadableGlobalContext} from "vue-mvvm"; + +import {InformationService} from "@contracts/information.service"; +import {ApiService} from "@contracts/api.service"; + +import type {ServiceDeclaration} from "@services/shared"; + +import type {AuditModel} from "@models/audit.model"; + +class InformationServiceImpl implements InformationService { + private readonly apiService: ApiService; + + public constructor(ctx: ReadableGlobalContext) { + this.apiService = ctx.getService(ApiService); + } + + public async getAudits(): Promise { + return await this.apiService.get(["api", "information", "audits"]); + } +} + +export default { + key: InformationService, + ctor: InformationServiceImpl +} satisfies ServiceDeclaration; + diff --git a/dashboard/src/services/profile.service.ts b/dashboard/src/services/profile.service.ts index 03e1228..f3c4dd3 100644 --- a/dashboard/src/services/profile.service.ts +++ b/dashboard/src/services/profile.service.ts @@ -1,9 +1,34 @@ +import type {ReadableGlobalContext} from "vue-mvvm"; + import {ProfileService} from "@contracts/profile.service"; +import {ApiService} from "@contracts/api.service"; + import type {ServiceDeclaration} from "@services/shared"; +import {HTTPError} from "@utils/http"; + class ProfileServiceImpl implements ProfileService { - public async login(_username: string, _password: string): Promise { - return false; + private readonly apiService: ApiService; + + public constructor(ctx: ReadableGlobalContext) { + this.apiService = ctx.getService(ApiService); + } + + public async login(username: string, password: string): Promise { + try { + await this.apiService.post(["api", "credentials", "login"], { + "uuid": username, + "password": password + }); + + return true; + } catch (e) { + if (HTTPError.isHttpError(e) && e.status == 401) { + return false; + } + + throw e; + } } } diff --git a/dashboard/src/services/shared.ts b/dashboard/src/services/shared.ts index 2aaa8fc..c30f9e1 100644 --- a/dashboard/src/services/shared.ts +++ b/dashboard/src/services/shared.ts @@ -1,5 +1,9 @@ import type {ReadableGlobalContext, ServiceKey} from "vue-mvvm"; +export class ApiServiceBase { + +} + export interface ServiceDeclaration { key: ServiceKey; ctor: new (ctx: ReadableGlobalContext) => T; diff --git a/dashboard/src/utils/http.ts b/dashboard/src/utils/http.ts new file mode 100644 index 0000000..15a4108 --- /dev/null +++ b/dashboard/src/utils/http.ts @@ -0,0 +1,133 @@ +export class HTTPError extends Error { + public readonly response: Response; + public readonly status: number; + public readonly statusText: string; + public readonly url: string; + public readonly body: any; + + private constructor( + message: string, + response: Response + ) { + super(message); + this.name = "HTTPError"; + + this.response = response; + this.status = response.status; + this.statusText = response.statusText; + this.url = response.url; + } + + public static async create(response: Response): Promise { + let message: string = `HTTP ${response.status}: ${response.statusText} (${response.url})`; + + try { + let body: string; + const contentType: string | null = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + body = JSON.stringify(await response.json(), null, 4); + } else { + body = await response.text(); + } + + message += "\n\n" + body; + } catch { + // If body parsing fails, we skip appending it + } + + + return new HTTPError(message, response); + } + + public [Symbol.toPrimitive](hint: string): string | number { + if (hint === 'number') return this.status; + return this.message; + } + + public toString(): string { + return this.message; + } + + public static isHttpError(obj: unknown): obj is HTTPError { + return ( + typeof obj == "object" && + obj instanceof Error && + obj.name == "HTTPError" + ); + } +} + +class HTTPResponse { + private readonly promise: Promise; + + public constructor(promise: Promise) { + this.promise = promise; + } + + public async text(): Promise { + return (await this.getResponse()).text(); + } + + public async json(): Promise { + return (await this.getResponse()).json(); + } + + public async arrayBuffer(): Promise { + return (await this.getResponse()).arrayBuffer(); + } + + public async uint8Array(): Promise { + return new Uint8Array(await this.arrayBuffer()); + } + + public async blob(): Promise { + return (await this.getResponse()).blob(); + } + + public async wait(): Promise { + await this.getResponse(); + } + + private async getResponse(): Promise { + const response: Response = await this.promise; + if (!response.ok) { + throw await HTTPError.create(response); + } + + return response; + } +} + +export function get(url: string, headers: [string, string][] = []): HTTPResponse { + return new HTTPResponse(fetch(url, { + method: "GET", + headers: headers, + credentials: "include" + })); +} + +export function post(url: string, body: RequestInit["body"], headers: [string, string][] = []): HTTPResponse { + return new HTTPResponse(fetch(url, { + method: "POST", + body: body, + headers: headers, + credentials: "include" + })); +} + +export function put(url: string, body: RequestInit["body"], headers: [string, string][] = []): HTTPResponse { + return new HTTPResponse(fetch(url, { + method: "PUT", + body: body, + headers: headers, + credentials: "include" + })); +} + +export function delete$(url: string, headers: [string, string][] = []): HTTPResponse { + return new HTTPResponse(fetch(url, { + method: "DELETE", + headers: headers, + credentials: "include" + })); +} diff --git a/dashboard/src/views/LoginView.vue b/dashboard/src/views/LoginView.vue index 2d52459..6271cdc 100644 --- a/dashboard/src/views/LoginView.vue +++ b/dashboard/src/views/LoginView.vue @@ -22,7 +22,7 @@ vm.onFocus.subscribe((index: number) => {