From 12628c9233f9bcd0151ed9a8c2a30aebf4b7e9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Thu, 23 Jul 2026 21:51:53 +0800 Subject: [PATCH 01/14] feat(hub): migrate database admin pages to SPA (database-admin) - scaffold hub/database-admin as SvelteKit 2 + Svelte 5 static SPA with aurora/glass visual style (paths.base='/database') - add lib/{api,session,org}.ts + Aurora.svelte component - add routes: root redirect, /admin login page, /dashboard (OWNER/ADMIN only) - backend: replace server-rendered HTML routes with /database/config JSON endpoint - add hub/src/database/static.ts to serve SPA under /database/* - wire registerDatabaseSpa into plugin.ts - exempt /database/* from silo rate-limit (same treatment as /admin/*) - add database:dev + database:build npm scripts; update deploy scripts - update hub/src/database/README.md Co-Authored-By: Claude Opus 4.8 --- hub/database-admin/.gitignore | 23 + hub/database-admin/.npmrc | 1 + hub/database-admin/.prettierignore | 4 + hub/database-admin/.prettierrc.json | 9 + hub/database-admin/package-lock.json | 2660 +++++++++++++++++ hub/database-admin/package.json | 33 + hub/database-admin/src/app.d.ts | 13 + hub/database-admin/src/app.html | 29 + hub/database-admin/src/lib/api.ts | 76 + .../src/lib/components/Aurora.svelte | 6 + hub/database-admin/src/lib/org.ts | 7 + hub/database-admin/src/lib/session.ts | 58 + hub/database-admin/src/routes/+layout.svelte | 7 + hub/database-admin/src/routes/+page.svelte | 30 + .../src/routes/admin/+page.svelte | 78 + hub/database-admin/src/routes/app.css | 96 + .../src/routes/dashboard/+page.svelte | 160 + hub/database-admin/static/favicon.svg | 5 + hub/database-admin/static/robots.txt | 3 + hub/database-admin/svelte.config.js | 22 + hub/database-admin/tsconfig.json | 20 + hub/database-admin/vite.config.ts | 17 + hub/deploy/deploy_fleet_release.sh | 5 +- hub/deploy/deploy_platform.sh | 7 +- hub/package.json | 6 +- hub/src/database/README.md | 45 +- hub/src/database/plugin.ts | 5 + hub/src/database/routes/databaseRoutes.ts | 253 +- hub/src/database/static.ts | 98 + hub/src/deployment/siloRateLimit.ts | 17 +- 30 files changed, 3546 insertions(+), 247 deletions(-) create mode 100644 hub/database-admin/.gitignore create mode 100644 hub/database-admin/.npmrc create mode 100644 hub/database-admin/.prettierignore create mode 100644 hub/database-admin/.prettierrc.json create mode 100644 hub/database-admin/package-lock.json create mode 100644 hub/database-admin/package.json create mode 100644 hub/database-admin/src/app.d.ts create mode 100644 hub/database-admin/src/app.html create mode 100644 hub/database-admin/src/lib/api.ts create mode 100644 hub/database-admin/src/lib/components/Aurora.svelte create mode 100644 hub/database-admin/src/lib/org.ts create mode 100644 hub/database-admin/src/lib/session.ts create mode 100644 hub/database-admin/src/routes/+layout.svelte create mode 100644 hub/database-admin/src/routes/+page.svelte create mode 100644 hub/database-admin/src/routes/admin/+page.svelte create mode 100644 hub/database-admin/src/routes/app.css create mode 100644 hub/database-admin/src/routes/dashboard/+page.svelte create mode 100644 hub/database-admin/static/favicon.svg create mode 100644 hub/database-admin/static/robots.txt create mode 100644 hub/database-admin/svelte.config.js create mode 100644 hub/database-admin/tsconfig.json create mode 100644 hub/database-admin/vite.config.ts create mode 100644 hub/src/database/static.ts diff --git a/hub/database-admin/.gitignore b/hub/database-admin/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/hub/database-admin/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/hub/database-admin/.npmrc b/hub/database-admin/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/hub/database-admin/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/hub/database-admin/.prettierignore b/hub/database-admin/.prettierignore new file mode 100644 index 0000000..491d6b0 --- /dev/null +++ b/hub/database-admin/.prettierignore @@ -0,0 +1,4 @@ +build +.svelte-kit +node_modules +package-lock.json diff --git a/hub/database-admin/.prettierrc.json b/hub/database-admin/.prettierrc.json new file mode 100644 index 0000000..5b31130 --- /dev/null +++ b/hub/database-admin/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 120, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/hub/database-admin/package-lock.json b/hub/database-admin/package-lock.json new file mode 100644 index 0000000..8f513b3 --- /dev/null +++ b/hub/database-admin/package-lock.json @@ -0,0 +1,2660 @@ +{ + "name": "database-admin", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "database-admin", + "version": "0.0.1", + "devDependencies": { + "@skeletonlabs/skeleton": "^4.15.2", + "@skeletonlabs/skeleton-svelte": "^4.15.2", + "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.63.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@tailwindcss/vite": "^4.3.2", + "bits-ui": "^2.18.1", + "prettier": "^3.9.5", + "prettier-plugin-svelte": "^4.1.1", + "svelte": "^5.56.1", + "svelte-check": "^4.6.0", + "tailwindcss": "^4.3.2", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@internationalized/date": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", + "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@skeletonlabs/skeleton": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton/-/skeleton-4.15.2.tgz", + "integrity": "sha512-5O23Py76nw56aoieV2b2T7MJ6xS0DwDjUULpwLnCXxXOnmzADEoWoQxb/ABbqg04IMOygtRzK3HO22I1+kFsog==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": "^4.0.0" + } + }, + "node_modules/@skeletonlabs/skeleton-common": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton-common/-/skeleton-common-4.15.2.tgz", + "integrity": "sha512-y7KZn++Av8UHdoeaaguQ7zIS1HlK7Y5jyPDnHy65wJ60iS97fMtQV0kGhqVoYCWhor+xrjbAFjWtaPOPPDEMYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@skeletonlabs/skeleton-svelte": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton-svelte/-/skeleton-svelte-4.15.2.tgz", + "integrity": "sha512-vZkRhR701EOHVXVKh/NFRSY8D9LPBvmdNFdtfgqO7TEg77sypJUvERl2dxErTLY1QjVklVpjsHRCTaSLn+1Ntg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@internationalized/date": "3.12.0", + "@skeletonlabs/skeleton-common": "4.15.2", + "@zag-js/accordion": "1.39.1", + "@zag-js/avatar": "1.39.1", + "@zag-js/carousel": "1.39.1", + "@zag-js/collapsible": "1.39.1", + "@zag-js/collection": "1.39.1", + "@zag-js/combobox": "1.39.1", + "@zag-js/date-picker": "1.39.1", + "@zag-js/dialog": "1.39.1", + "@zag-js/file-upload": "1.39.1", + "@zag-js/floating-panel": "1.39.1", + "@zag-js/listbox": "1.39.1", + "@zag-js/menu": "1.39.1", + "@zag-js/pagination": "1.39.1", + "@zag-js/popover": "1.39.1", + "@zag-js/progress": "1.39.1", + "@zag-js/radio-group": "1.39.1", + "@zag-js/rating-group": "1.39.1", + "@zag-js/slider": "1.39.1", + "@zag-js/steps": "1.39.1", + "@zag-js/svelte": "1.39.1", + "@zag-js/switch": "1.39.1", + "@zag-js/tabs": "1.39.1", + "@zag-js/tags-input": "1.39.1", + "@zag-js/toast": "1.39.1", + "@zag-js/toggle-group": "1.39.1", + "@zag-js/tooltip": "1.39.1", + "@zag-js/tree-view": "1.39.1" + }, + "peerDependencies": { + "svelte": "^5.29.0" + } + }, + "node_modules/@skeletonlabs/skeleton-svelte/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz", + "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.70.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", + "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", + "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "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==", + "dev": true, + "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/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zag-js/accordion": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.39.1.tgz", + "integrity": "sha512-GA3m7gRTm3weSe1eMlHIsTNztcjZ6joIaRgxxKil7q/UX0xIVVGDy0aCr6oo7FAuoMiOOBVurYXILpFZ30nOXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/anatomy": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/anatomy/-/anatomy-1.39.1.tgz", + "integrity": "sha512-p2iFAs2pVQgv5iCDAftA7g9Z/fUYXW94dRIGk415TSbkp/YDENydm/JtRoNctp302UIx4Eeuc5QBR+7h5kuISA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zag-js/aria-hidden": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/aria-hidden/-/aria-hidden-1.39.1.tgz", + "integrity": "sha512-wiwcz3N086qBMEU3VKfHhcvGm6Jm1PIcDXys/jEqiKPtHoYZhDip0n0cPOoasss/A1oS39QFVdk3WpLXGu3Izw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/auto-resize": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/auto-resize/-/auto-resize-1.39.1.tgz", + "integrity": "sha512-ditIo9mW7fapq+4yx3/8hMpMZlWaoOy66EOzUz8dSVqnxnTWAjnTICu/9zFh8pkWerlzGTtDOJPP1oZ8S/rgVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/avatar": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/avatar/-/avatar-1.39.1.tgz", + "integrity": "sha512-LWrgJ0bebnXPSL+uehA9z6BlCD/MZEOQBJqH/F2QQFSAAZXUUDKtzVDmc+UtwjDsHXqqTghi+v2atQJHNMcJ2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/carousel": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/carousel/-/carousel-1.39.1.tgz", + "integrity": "sha512-5z5z3IldUgZ/R+KZLNQDoJFNTXzYd28YOmgfWH61Vvyv+RarX8kwZW8ajW/fNiqcWXyhW3/VMU0lArrfjbQVtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/scroll-snap": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/collapsible": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/collapsible/-/collapsible-1.39.1.tgz", + "integrity": "sha512-Zgccg/t7M8i0JVwZPPgW7XB7kGhTO475hsmwkF/8CYLqBBckVDHUARp2we24hENCm/98eez6R0eDEmE+tldFWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/collection": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/collection/-/collection-1.39.1.tgz", + "integrity": "sha512-fyOyKmP7MRo0/U8mBmB7KgHRXHhXP27LCcasy3x+qTAQtuEfYG1EPhKuj07oBWlX/2qfcKYn2R3YopHcqFcCiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/combobox": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/combobox/-/combobox-1.39.1.tgz", + "integrity": "sha512-fmStpG+k4xrxCzqUX0ssnOMeoSietWm5ir3qmEZcagzNqNycAXMvOELAIeyXi87Kut6aDGhxLOV7o395HVXl/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/collection": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/live-region": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/core": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/core/-/core-1.39.1.tgz", + "integrity": "sha512-Yp0r49QLYXe2j7fgyAiilH4umXFydCnr5hcRDwJU+sxvUAlq00JQIJIEK2pT6k8cJiNNsFEV5WkOX7jsqpAX2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/date-picker": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-picker/-/date-picker-1.39.1.tgz", + "integrity": "sha512-t9q1H0aZQJkbzKTR2Bn5vMwaoFoirxekiSxw8ju0F0vr4Kg4BJ9yueOQm5I2wALKnJbZu4Ua5MgzlrDF3CQt3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/date-utils": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/live-region": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + }, + "peerDependencies": { + "@internationalized/date": ">=3.0.0" + } + }, + "node_modules/@zag-js/date-utils": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-utils/-/date-utils-1.39.1.tgz", + "integrity": "sha512-i4SvBhru2Yz/zsHT0XvyFhf4a+pAKYkWXeVfU0RvF2S6mPTfgaMFF9ZNPq5Sy8K31EtAa6AVXcybYaYnibn1FA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@internationalized/date": ">=3.0.0" + } + }, + "node_modules/@zag-js/dialog": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/dialog/-/dialog-1.39.1.tgz", + "integrity": "sha512-q+HTmfuRDRZthln9mb7i52wdltQOZlw3+nw3a2uygEe9xuEtHBwUz31XJzkn2UWQqhAt7cC39OwykhNLKrfkqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/aria-hidden": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-trap": "1.39.1", + "@zag-js/remove-scroll": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/dismissable": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/dismissable/-/dismissable-1.39.1.tgz", + "integrity": "sha512-7/soy93Ersd5qedhSL/+CDcZ9gNTQV0ooDcqKtM8b4IxwD4rgWwGsewJY+tbKmOqaZobwa0YcWV2+YGgI23ESw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1", + "@zag-js/interact-outside": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/dom-query": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-1.39.1.tgz", + "integrity": "sha512-k01aXeUWLyJfB61CODaXj4PLhYmVpnVMFrC+3nk/XCn1MW7my8L/8KVg0m4W8n+X9MhpaLWsZDmK/dwED/3qSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/types": "1.39.1" + } + }, + "node_modules/@zag-js/file-upload": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-upload/-/file-upload-1.39.1.tgz", + "integrity": "sha512-cErPOnPwPyneUXpelsfm75DKn0/4SI8aqQnlbrqo522PEqAQyDfDdBsqebGgKWG3F0A++kKFp9LO9A5zCrw5gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/file-utils": "1.39.1", + "@zag-js/i18n-utils": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/file-utils": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-utils/-/file-utils-1.39.1.tgz", + "integrity": "sha512-ll/W5o74SMmoAS+l7PkmmGjPj4PLCSG/cwQh1Y/+LpaSev0YiR3Nk2OzRIIPtm3NivYVxKGawaCOf1RvT/82LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/i18n-utils": "1.39.1" + } + }, + "node_modules/@zag-js/floating-panel": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/floating-panel/-/floating-panel-1.39.1.tgz", + "integrity": "sha512-IfPbf3pwJGqBWHec/rPzpdPjfMCLed59LlEophvRy49FEdksv8eN6nr9DXl2wWZEoQhH99scXfLMbtEZsPsFWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/rect-utils": "1.39.1", + "@zag-js/store": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/focus-trap": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-trap/-/focus-trap-1.39.1.tgz", + "integrity": "sha512-2ZzVefHMotvtxUo/gP4R45Szw/EPaPkTKEHaug6/il62SPDbkFODF+5r1zXyLbLuwCHq0apvQasg/ONLihwlXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/focus-visible": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-1.39.1.tgz", + "integrity": "sha512-iEuTOYHE8HRn/7ULC9c9BTTWo0C0MJRCbYVxbh/d7v8qAuq4CS76pdfceNo3KeWbb968T+yiG6q0AjiHsr8IOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/i18n-utils": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/i18n-utils/-/i18n-utils-1.39.1.tgz", + "integrity": "sha512-TKRLQQlHgJ4cxsHo3tZPtbFjGu9m1UPtfezRGFKq7A8czhdqRhaCpaWF849cd6dI7x6rWvvTan858gOFpyANnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/interact-outside": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/interact-outside/-/interact-outside-1.39.1.tgz", + "integrity": "sha512-LnSbA+txMsFmzNPn84QKH01x2yJv4At/eKHn6rT2PyxXkJQIh8PvCTS3zVz4Syw11cmhcXt2eRwhzx8yImV92w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/listbox": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/listbox/-/listbox-1.39.1.tgz", + "integrity": "sha512-Mz0UpdXobdTQTyjM+Avgi7pDVB2dKyaUHqw3TloeleQL3VwTqClclkwHXtLYYE+oXa0zOet37wI9mzfaYx9iZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/collection": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/live-region": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/live-region/-/live-region-1.39.1.tgz", + "integrity": "sha512-E7YNd0QGzJ2n1ZhnI2smv+klwifsNRf9QaDCx7quVJCVYywpupsBK4R25KN75S1z8XaK+jAy6HYKj8DIhYjYeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zag-js/menu": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/menu/-/menu-1.39.1.tgz", + "integrity": "sha512-bRDGLGkiGhzNtORBXkbBQV/xp2zEkwpYIepfWCaUoFwKUmx7GGnShTBFxJyq0u2D4IkS9GOwcqm20EhMv6V+TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/rect-utils": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/pagination": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/pagination/-/pagination-1.39.1.tgz", + "integrity": "sha512-3Q1B9/g3ajhvXjuGffJ7otyXcXK5+uhdbE5A9CZa4bsW3pf25L9Cp+ZAjdXQMDc8T4jhZJAKFmDJfQgtr1oEIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/popover": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/popover/-/popover-1.39.1.tgz", + "integrity": "sha512-aO3ExO/O7Sa3ovdozFI6SujhNOpYdCca4bImnAiovDL8DY8zN3UNQebu35IQvw9/aRsx9VKSJL1AqzJJUImFRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/aria-hidden": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-trap": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/remove-scroll": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/popper": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/popper/-/popper-1.39.1.tgz", + "integrity": "sha512-h0UMY2dXJNfM3OvMQ9t9LzlmwvpCgjloz2IvU1txY3r32UIy7ve1H70zkKagLtLRxFTuWmhumYUPULPo/6a1DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6", + "@zag-js/dom-query": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/progress": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/progress/-/progress-1.39.1.tgz", + "integrity": "sha512-1IHyOw8DqPs3YH149Oj7W9a5oEfY5pc9GAVOPGbzYxVK/W8d/NIjVxa565I3J5cDJ0s6z3FrMSXMWUwr1ML4tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/radio-group": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/radio-group/-/radio-group-1.39.1.tgz", + "integrity": "sha512-+sC9xcAyY/GbY+8HpKlbPgSyOxBLUSB18s6fe6K1wdmyom4PM0nmhLouuxisbFZYHOyfQwAOMo+ainRENB2hzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/rating-group": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/rating-group/-/rating-group-1.39.1.tgz", + "integrity": "sha512-IfdxWmM+3zpztx/HcE3bWob72sZNb1+BzK4tSySLVyjeqs8OzLDzrCbKqt10DmibnNOvpbjbq4eX4P5hV9YN7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/rect-utils": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/rect-utils/-/rect-utils-1.39.1.tgz", + "integrity": "sha512-5gJ0PzeUme76xTWG+4XythWgmGgDKV4XAxEUaB3KKDtXgjDHwtu7PwKLIzFtlaaSf/U23PY+RNVBVCYg1GmZog==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zag-js/remove-scroll": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/remove-scroll/-/remove-scroll-1.39.1.tgz", + "integrity": "sha512-uZfPR3Gl9sQFo+tJ7kbuwsBhw+RIZwWFnMDgrz5LIwSNGN6hsyC4HGOxe29clkWQ2X2AjqqmEMETwgX7Jg+wxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/scroll-snap": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/scroll-snap/-/scroll-snap-1.39.1.tgz", + "integrity": "sha512-AzCc8MAAVqkiK5Y0cJZ24OIBZDQrUmEexACMuR6M5yZmlcEbS0EA/d6Wq+LSR1JMVTD4B+UwcMj1D3vJQ90ZTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "1.39.1" + } + }, + "node_modules/@zag-js/slider": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/slider/-/slider-1.39.1.tgz", + "integrity": "sha512-OEA9R7Ly5cw+6ANofnMpuHH3rAo8gZEnxy7iEwePu11pq2RCnt8DSj2V+uqU+dTq15Uup1LSzRgJfTnAC4Z85A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/steps": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/steps/-/steps-1.39.1.tgz", + "integrity": "sha512-DC6swMpwITTB0DyCSxlpWyPNSUN9ul9jz4N6aAyQ0L1IK/noF/YYTZRAcXNSRzN4iutO/2mFGGbwGq/oVf+gPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/store": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/store/-/store-1.39.1.tgz", + "integrity": "sha512-zFpwP4lhiBVD9987rwAfZNVa2/f/xx4mhbCE1EEw31zxLAozY2jONeJ3UzPP05VbzKlRHBcvkaXAJQQGegTwFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "proxy-compare": "3.0.1" + } + }, + "node_modules/@zag-js/svelte": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/svelte/-/svelte-1.39.1.tgz", + "integrity": "sha512-ZOyZjyvjePZdrkNTy5fa92ijeeID9e+3LRGziKGIII3JSEvwfkG/Buf8W84N8VHFxi0G0GcKmgVCDgKHyGQYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/core": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + }, + "peerDependencies": { + "svelte": ">=5" + } + }, + "node_modules/@zag-js/switch": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/switch/-/switch-1.39.1.tgz", + "integrity": "sha512-ikeQ42c0vyyPLeyW9U0dvcqTV1Ekpx5jZ050R905HGJ2GeWE0uBGuHbMpTG5U6Pwb0a+TMzqAr+jMsquVTCwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/tabs": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/tabs/-/tabs-1.39.1.tgz", + "integrity": "sha512-P2RThO1gX9SFsNqrAGPsXJxrjn5YqP6MFs9mdExU+tzzZyVjJQADkAmh98C0eEaCb6HKLpJZ/17hrnLDhm1Tig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/tags-input": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/tags-input/-/tags-input-1.39.1.tgz", + "integrity": "sha512-tc0+bd9FiUJwa+wY2hSVVGHLIBC3C3rOZX/4zjchRMs1xgl92c1/tYbytXny7ABB8ZMHveG7MtgDppVF4VkwBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/auto-resize": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/interact-outside": "1.39.1", + "@zag-js/live-region": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/toast": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/toast/-/toast-1.39.1.tgz", + "integrity": "sha512-K7ndEfBTKDds10iQKCQUmin74s6V4BEIypAIyQxs18gQB9TCn5+wff886JAzecIKPY97PDQHDKjYR71yzRC7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dismissable": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/toggle-group": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/toggle-group/-/toggle-group-1.39.1.tgz", + "integrity": "sha512-KS4Bo17foMKXVBhQjocRf4GQxMV4pMXclTo14IWjldaHs2HIrNJ0Ar0Ri+vo47BBKBNsXs4HuNvfbMdQj94wEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/tooltip": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/tooltip/-/tooltip-1.39.1.tgz", + "integrity": "sha512-IsxFj7l8kPciwIyYJWlmQ7mhXocbjXxLj3m9z099slYOF7lApA33/ndY32w9ptrI4/nUh2nldzw6eRfSpVnuOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/focus-visible": "1.39.1", + "@zag-js/popper": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/tree-view": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/tree-view/-/tree-view-1.39.1.tgz", + "integrity": "sha512-sm6qUZjO0OaqBqO5s55KU+l5p1wXfUVScoen7BYVoFBuROH7qAZJi8YMclGvnnlyV506i8Hk0qqWnLg0F38jCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zag-js/anatomy": "1.39.1", + "@zag-js/collection": "1.39.1", + "@zag-js/core": "1.39.1", + "@zag-js/dom-query": "1.39.1", + "@zag-js/types": "1.39.1", + "@zag-js/utils": "1.39.1" + } + }, + "node_modules/@zag-js/types": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/types/-/types-1.39.1.tgz", + "integrity": "sha512-w3vVpgxmdJvMDvv19DXTtFI6kJL6TXw//U0Z1BAc3rnDA9orcB9Ryw4uMNvIzFA607CgssyJcWDaQ/M3yAcbJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "3.2.3" + } + }, + "node_modules/@zag-js/utils": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/@zag-js/utils/-/utils-1.39.1.tgz", + "integrity": "sha512-9k741cH7L655Ua3tedTkuMblcXVXVgCLTB9svp9oTjA7oatpOpYF4z43kgAQVjyThNXMJ7AvtO4C80ajQLTScg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bits-ui": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", + "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": 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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "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/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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.1.tgz", + "integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^5.0.0" + } + }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "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/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svelte": { + "version": "5.56.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", + "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "dev": true, + "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/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/hub/database-admin/package.json b/hub/database-admin/package.json new file mode 100644 index 0000000..dea271a --- /dev/null +++ b/hub/database-admin/package.json @@ -0,0 +1,33 @@ +{ + "name": "database-admin", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "devDependencies": { + "@skeletonlabs/skeleton": "^4.15.2", + "@skeletonlabs/skeleton-svelte": "^4.15.2", + "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.63.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@tailwindcss/vite": "^4.3.2", + "bits-ui": "^2.18.1", + "prettier": "^3.9.5", + "prettier-plugin-svelte": "^4.1.1", + "svelte": "^5.56.1", + "svelte-check": "^4.6.0", + "tailwindcss": "^4.3.2", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } +} diff --git a/hub/database-admin/src/app.d.ts b/hub/database-admin/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/hub/database-admin/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/hub/database-admin/src/app.html b/hub/database-admin/src/app.html new file mode 100644 index 0000000..827d63d --- /dev/null +++ b/hub/database-admin/src/app.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + Database Admin + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/hub/database-admin/src/lib/api.ts b/hub/database-admin/src/lib/api.ts new file mode 100644 index 0000000..50c433f --- /dev/null +++ b/hub/database-admin/src/lib/api.ts @@ -0,0 +1,76 @@ +/** + * Thin API client for the database-admin backend. Same-origin cookie auth, + * reusing the platform session (`cph_session`) and the admin plane's /api/me. + */ + +export class ApiError extends Error { + code: string; + status: number; + constructor(code: string, message: string, status: number) { + super(message); + this.name = 'ApiError'; + this.code = code; + this.status = status; + } +} + +async function request(method: string, url: string, body?: unknown): Promise { + const init: RequestInit = { + method, + credentials: 'same-origin', + headers: body !== undefined ? { 'content-type': 'application/json' } : undefined, + body: body !== undefined ? JSON.stringify(body) : undefined, + }; + const res = await fetch(url, init); + const text = await res.text(); + let data: unknown = null; + if (text !== '') { + try { + data = JSON.parse(text); + } catch { + data = text; + } + } + if (!res.ok) { + const err = (data as { error?: { code?: string; message?: string } } | null)?.error; + throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status); + } + return data; +} + +const get = (u: string) => request('GET', u); +const post = (u: string, b?: unknown) => request('POST', u, b); + +// --- Types --- + +export interface OrgMembership { + id: string; + slug: string; + name: string; + status: string; + role: 'OWNER' | 'ADMIN' | 'MEMBER'; +} + +export interface MeResponse { + user: { + id: string; + feishuOpenId: string; + displayName: string; + avatarUrl: string | null; + }; + organizations: OrgMembership[]; +} + +/** Unauthenticated bootstrap the login page needs: which org to OAuth against + dev toggle. */ +export interface DatabaseConfig { + siloOrganizationSlug: string; + devLoginEnabled: boolean; +} + +// --- API --- + +export const api = { + me: () => get('/api/me') as Promise, + logout: () => post('/auth/logout'), + databaseConfig: () => get('/database/config') as Promise, +}; diff --git a/hub/database-admin/src/lib/components/Aurora.svelte b/hub/database-admin/src/lib/components/Aurora.svelte new file mode 100644 index 0000000..27c39b4 --- /dev/null +++ b/hub/database-admin/src/lib/components/Aurora.svelte @@ -0,0 +1,6 @@ + +
+
+
+
+
diff --git a/hub/database-admin/src/lib/org.ts b/hub/database-admin/src/lib/org.ts new file mode 100644 index 0000000..3973154 --- /dev/null +++ b/hub/database-admin/src/lib/org.ts @@ -0,0 +1,7 @@ +import type { OrgMembership } from './api'; + +export function isOrgAdmin(org: OrgMembership | null | undefined): boolean { + if (!org) return false; + const role = String(org.role ?? '').toUpperCase(); + return role === 'OWNER' || role === 'ADMIN'; +} diff --git a/hub/database-admin/src/lib/session.ts b/hub/database-admin/src/lib/session.ts new file mode 100644 index 0000000..a62ce30 --- /dev/null +++ b/hub/database-admin/src/lib/session.ts @@ -0,0 +1,58 @@ +import { writable } from 'svelte/store'; +import { goto } from '$app/navigation'; +import { base } from '$app/paths'; +import { api, type MeResponse } from './api'; + +interface SessionState { + loading: boolean; + me: MeResponse | null; + error: string | null; +} + +export const session = writable({ + loading: true, + me: null, + error: null, +}); + +export async function loadSession(): Promise { + session.update((s) => ({ ...s, loading: true, error: null })); + try { + const me = await api.me(); + session.set({ loading: false, me, error: null }); + } catch (err) { + const status = (err as { status?: number }).status; + if (status === 401) { + session.set({ loading: false, me: null, error: null }); + void redirectToLogin(); + return; + } + session.set({ + loading: false, + me: null, + error: err instanceof Error ? err.message : String(err), + }); + } +} + +/** + * Send the browser to the login page (`/database/admin`). Unlike admin-web we + * don't jump straight to Feishu OAuth: the login page reads /database/config + * to build the org-scoped link and to show the dev bypass when enabled. + */ +export async function redirectToLogin(): Promise { + const loginPath = `${base}/admin`; + if (window.location.pathname === loginPath) return; + await goto(loginPath, { replaceState: true }); +} + +/** Build the org-scoped Feishu OAuth entry point (a backend route, not under base). */ +export function feishuLoginHref(orgSlug: string, returnTo: string = `${base}/dashboard`): string { + return `/auth/feishu/${encodeURIComponent(orgSlug)}?returnTo=${encodeURIComponent(returnTo)}`; +} + +export async function logout(): Promise { + await api.logout(); + session.set({ loading: false, me: null, error: null }); + await redirectToLogin(); +} diff --git a/hub/database-admin/src/routes/+layout.svelte b/hub/database-admin/src/routes/+layout.svelte new file mode 100644 index 0000000..5f7ae2b --- /dev/null +++ b/hub/database-admin/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/hub/database-admin/src/routes/+page.svelte b/hub/database-admin/src/routes/+page.svelte new file mode 100644 index 0000000..83c0c1f --- /dev/null +++ b/hub/database-admin/src/routes/+page.svelte @@ -0,0 +1,30 @@ + + +
+
+ + + + +

正在进入…

+
+
diff --git a/hub/database-admin/src/routes/admin/+page.svelte b/hub/database-admin/src/routes/admin/+page.svelte new file mode 100644 index 0000000..65e7732 --- /dev/null +++ b/hub/database-admin/src/routes/admin/+page.svelte @@ -0,0 +1,78 @@ + + + + +
+
+
+
+ D +
+

Database Admin

+

使用飞书登录以管理数据库

+
+ + {#if error} +
+ 无法连接后端:{error} +
+ {/if} + + + + + + 使用飞书登录 + + + {#if config?.devLoginEnabled} +
+
+
+ 开发模式 +
+
+ + 一键登录管理员 + +

仅开发环境可见 · 跳过飞书 OAuth

+ {/if} +
+
diff --git a/hub/database-admin/src/routes/app.css b/hub/database-admin/src/routes/app.css new file mode 100644 index 0000000..c29cc79 --- /dev/null +++ b/hub/database-admin/src/routes/app.css @@ -0,0 +1,96 @@ +@import 'tailwindcss'; + +@source './**/*.{html,js,svelte,ts}'; +@source '../lib/**/*.{html,js,svelte,ts}'; + +/* + * Design system for the database-admin SPA. Migrated verbatim (in spirit) from + * the previous server-rendered pages in hub/src/database/routes/databaseRoutes.ts: + * a light aurora / glassmorphism look with violet→cyan gradients and soft rise-in + * entrances. Distinct from admin-web's flat industrial theme on purpose. + */ + +@theme { + --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', sans-serif; +} + +@keyframes aurora { + 0% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(6%, -8%) scale(1.15); + } + 66% { + transform: translate(-8%, 6%) scale(0.9); + } + 100% { + transform: translate(0, 0) scale(1); + } +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +@layer base { + :root { + font-family: var(--font-sans); + } + html { + height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +@layer components { + .aurora { + filter: blur(80px); + animation: aurora 18s ease-in-out infinite; + } + .rise { + animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; + } + .rise-1 { + animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; + } + .rise-2 { + animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both; + } + .rise-3 { + animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both; + } + .grad-text { + background: linear-gradient(120deg, #7c3aed, #0891b2, #4f46e5); + background-size: 200% auto; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: shimmer 6s linear infinite; + } + .glass { + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + } + .glow-btn { + box-shadow: 0 12px 32px -10px rgba(124, 58, 237, 0.45); + } +} diff --git a/hub/database-admin/src/routes/dashboard/+page.svelte b/hub/database-admin/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..de75911 --- /dev/null +++ b/hub/database-admin/src/routes/dashboard/+page.svelte @@ -0,0 +1,160 @@ + + + + +{#if $session.loading} +
+
+ + + + +

加载中…

+
+
+{:else if $session.error} +
+
+

无法连接后端

+

{$session.error}

+ +
+
+{:else if !allowed} +
+
+

无权访问

+

数据库管理台仅向组织所有者与管理员开放。

+ +
+
+{:else} +
+ + + + +
+
+
+

概览

+

欢迎回来,这里是数据库管理台

+
+ +
+ +
+
+ {#each stats as s, i} +
+
+

{s.label}

+

{s.value}

+
+ {/each} +
+ +
+ 内容区占位 · 后续数据端点挂在 /database/* 并加 guard +
+
+
+
+{/if} diff --git a/hub/database-admin/static/favicon.svg b/hub/database-admin/static/favicon.svg new file mode 100644 index 0000000..43bc66d --- /dev/null +++ b/hub/database-admin/static/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/hub/database-admin/static/robots.txt b/hub/database-admin/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/hub/database-admin/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/hub/database-admin/svelte.config.js b/hub/database-admin/svelte.config.js new file mode 100644 index 0000000..9c60538 --- /dev/null +++ b/hub/database-admin/svelte.config.js @@ -0,0 +1,22 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: false, + }), + // This SPA is served under /database by the Hub. admin-web owns the + // root asset paths (/_app, /favicon.svg); a base path moves this app's + // assets to /database/_app/* so the two builds never collide. + paths: { base: '/database' }, + }, +}; + +export default config; diff --git a/hub/database-admin/tsconfig.json b/hub/database-admin/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/hub/database-admin/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/hub/database-admin/vite.config.ts b/hub/database-admin/vite.config.ts new file mode 100644 index 0000000..4839de8 --- /dev/null +++ b/hub/database-admin/vite.config.ts @@ -0,0 +1,17 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { + proxy: { + '/api': 'http://127.0.0.1:8788', + '/auth': 'http://127.0.0.1:8788', + // Backend owns the /database/* HTTP surface (login page, dashboard, + // data routes). Proxy it in dev so those paths hit the real server + // instead of the SPA fallback. + '/database': 'http://127.0.0.1:8788', + }, + }, +}); diff --git a/hub/deploy/deploy_fleet_release.sh b/hub/deploy/deploy_fleet_release.sh index a078b8f..53d2148 100755 --- a/hub/deploy/deploy_fleet_release.sh +++ b/hub/deploy/deploy_fleet_release.sh @@ -82,10 +82,11 @@ REMOTE rsync -az --delete \ --exclude node_modules --exclude dist --exclude .env \ --exclude admin-web/node_modules --exclude admin-web/build --exclude admin-web/.svelte-kit \ + --exclude database-admin/node_modules --exclude database-admin/build --exclude database-admin/.svelte-kit \ -e "ssh ${SSH_OPTS[*]}" \ "$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/" - echo "[fleet] npm ci + build (tsc + admin-web SPA)" + echo "[fleet] npm ci + build (tsc + admin-web & database-admin SPAs)" ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" bash -s <`, -回调由 `src/admin/routes/authRoutes.ts` 处理并种下 session cookie。 +- `GET /database/config` —— 免鉴权。返回 `{ siloOrganizationSlug, devLoginEnabled }`, + 给 SPA 登录页拼飞书链接、决定是否显示 dev 按钮用。不含任何敏感数据。 +- `GET /database/dev-login` —— 仅开发。见下。 +- `GET /database`、`GET /database/*` —— SPA shell / 客户端路由 fallback + (`static.ts` 的 `registerDatabaseSpa`);资产在 `/database/_app/*`。 + +SPA 页面(`database-admin`,`paths.base='/database'`): + +- `/database/admin` —— 飞书登录页。按钮指向 `/auth/feishu/`(slug 来自 + `/database/config`),回调由 `src/admin/routes/authRoutes.ts` 处理并种 session cookie。 +- `/database/dashboard` —— 后台壳。未登录跳登录页;**登录但非 OWNER/ADMIN 显示无权提示**。 + +> **注册顺序要点**:concrete 路由(`/database/config`、`/database/dev-login`)必须在 +> `registerDatabaseSpa` 的 `/database/*` fallback 之前注册(已在 `plugin.ts` 保证), +> 否则通配会 shadow 它们。 ## 开发模式:用环境变量开启一键登录 @@ -29,7 +43,8 @@ HUB_DEV_LOGIN_BYPASS="true" 开启后: -- `/database/admin` 登录页显示「⚡ 一键登录管理员」按钮 +- `/database/config` 返回 `devLoginEnabled: true`,SPA 登录页据此显示 + 「⚡ 一键登录管理员」按钮 - 后端注册 `/database/dev-login` 端点:按钮就是打它,它签发一个和飞书 OAuth 回调完全一样的 session,然后跳到 `/database/dashboard` @@ -57,12 +72,22 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 | 文件 | 职责 | |------|------| -| `plugin.ts` | 模块对外入口,`hub.ts` 调 `registerDatabasePlugin()` | -| `routes/databaseRoutes.ts` | 路由 + 页面渲染,**你主要在这里加内容** | +| `plugin.ts` | 模块对外入口,`hub.ts` 调 `registerDatabasePlugin()`;先挂 concrete 路由再挂 SPA | +| `routes/databaseRoutes.ts` | 后端 JSON / redirect 路由(`/database/config`、`/database/dev-login`),**数据端点加在这里** | +| `static.ts` | `registerDatabaseSpa`:托管 `database-admin/build` 的 SPA + `/database/*` fallback | -新增一类端点时:要么直接往 `databaseRoutes.ts` 加 `app.get("/database/...")`, +新增一类**数据**端点时:要么直接往 `databaseRoutes.ts` 加 `app.get("/database/...")`, 要么新建 `routes/xxxRoutes.ts` 并在 `databaseRoutes.ts` 里 `registerXxxRoutes(app, {...})` -注册一次。 +注册一次。**页面**则加在 `database-admin/src/routes/` 下(SvelteKit 路由)。 + +## SPA 构建与托管 + +- 前端在 `hub/database-admin/`,`npm run build`(或 hub 根的 `npm run database:build`) + 产出到 `database-admin/build/`。hub 的 `npm run build` 会把两个 SPA 一起带出来。 +- `static.ts` 默认从 `../../database-admin/build` 读产物;可用 `CPH_DATABASE_UI_DIR` + 覆盖。产物缺失时降级:只 warn,不挂 SPA,`/database/config` 与 `/database/dev-login` 仍可用。 +- 本地开发:hub 根 `npm run database:dev` 起 Vite,它把 `/api`、`/auth`、`/database` + 代理到 `127.0.0.1:8788`。 ## 约定(与 admin 面一致) diff --git a/hub/src/database/plugin.ts b/hub/src/database/plugin.ts index 4d17c16..6d9561c 100644 --- a/hub/src/database/plugin.ts +++ b/hub/src/database/plugin.ts @@ -16,6 +16,7 @@ import type { FastifyInstance } from "fastify"; import type { PrismaClient } from "@prisma/client"; import { registerDatabaseRoutes } from "./routes/databaseRoutes.js"; +import { registerDatabaseSpa } from "./static.js"; export interface DatabasePluginConfig { readonly prisma: PrismaClient; @@ -42,4 +43,8 @@ export async function registerDatabasePlugin( siloOrganizationSlug: config.siloOrganizationSlug, allowDevLoginBypass, }); + + // SPA shell + fallback, registered after the concrete /database/* routes above + // so the /database/* wildcard does not shadow them. + await registerDatabaseSpa(app); } diff --git a/hub/src/database/routes/databaseRoutes.ts b/hub/src/database/routes/databaseRoutes.ts index 5f1ef6f..4cd82d1 100644 --- a/hub/src/database/routes/databaseRoutes.ts +++ b/hub/src/database/routes/databaseRoutes.ts @@ -5,23 +5,30 @@ * Handlers use ABSOLUTE paths (no Fastify prefix) so every route greps as the * literal string it serves. * - * /database/admin — Feishu-only login page (+ dev one-click button) - * /database/dashboard — sidebar + content admin shell, session-gated + * The login page and dashboard are now served by the `database-admin` SvelteKit + * SPA (see ../static.ts / registerDatabaseSpa). This file keeps only the + * concrete JSON/redirect routes the SPA depends on: + * + * /database/config — unauthenticated bootstrap: silo org slug + dev toggle * /database/dev-login — DEV ONLY bypass, registered only when the flag is on * - * The dev bypass (button + /database/dev-login) is self-contained here and - * gated by allowDevLoginBypass (computed in ./plugin.ts from HUB_DEV_LOGIN_BYPASS - * + NODE_ENV). Production requires real Feishu OAuth. + * The dev bypass (/database/dev-login) is self-contained here and gated by + * allowDevLoginBypass (computed in ./plugin.ts from HUB_DEV_LOGIN_BYPASS + + * NODE_ENV). Production requires real Feishu OAuth. + * + * NOTE: concrete routes here MUST be registered before the SPA fallback + * (registerDatabaseSpa serves /database and /database/*), or the wildcard would + * shadow them. */ import type { FastifyInstance } from "fastify"; import type { PrismaClient } from "@prisma/client"; -import { SESSION_COOKIE_NAME, signSession, verifySession } from "../../admin/auth/session.js"; +import { SESSION_COOKIE_NAME, signSession } from "../../admin/auth/session.js"; export interface DatabaseRouteConfig { readonly prisma: PrismaClient; /** HMAC secret for the signed session cookie — reused from the admin plane. */ readonly sessionSecret: string; - /** Silo Organization slug — builds the org-scoped Feishu login link. */ + /** Silo Organization slug — the SPA builds the org-scoped Feishu login link from it. */ readonly siloOrganizationSlug: string; /** DEV ONLY. Enables the one-click button and the /database/dev-login route. */ readonly allowDevLoginBypass: boolean; @@ -31,18 +38,14 @@ export async function registerDatabaseRoutes( app: FastifyInstance, config: DatabaseRouteConfig, ): Promise { - app.get("/database/admin", async (request, reply) => { - // Already signed in → straight to the dashboard. - if ((await resolveUser(request.cookies[SESSION_COOKIE_NAME], config)) !== null) { - return reply.redirect("/database/dashboard"); - } - return reply.type("text/html").send(renderLoginPage(config)); - }); - - app.get("/database/dashboard", async (request, reply) => { - const user = await resolveUser(request.cookies[SESSION_COOKIE_NAME], config); - if (user === null) return reply.redirect("/database/admin"); - return reply.type("text/html").send(renderDashboard(user.displayName)); + // Unauthenticated bootstrap for the static SPA login page. Exposes only what + // the page needs to build the Feishu login link and toggle the dev button — + // no secrets, no user data. + app.get("/database/config", async () => { + return { + siloOrganizationSlug: config.siloOrganizationSlug, + devLoginEnabled: config.allowDevLoginBypass, + }; }); // DEV ONLY bypass — self-contained here, registered only when the flag is on @@ -96,212 +99,8 @@ export async function registerDatabaseRoutes( }); } - // Add more /database/* routes here. Guard data routes with requireSession / - // requireOrgRole (../../admin/auth/guards.js) and scope every query to the - // caller's org (ADR-0020). Access the DB via config.prisma. -} - -/** Verify the session cookie and load the user, or null if not signed in. */ -async function resolveUser( - rawCookie: string | undefined, - config: DatabaseRouteConfig, -): Promise<{ displayName: string } | null> { - if (rawCookie === undefined || rawCookie === "") return null; - const session = verifySession(rawCookie, config.sessionSecret); - if (session === null) return null; - const user = await config.prisma.user.findUnique({ - where: { id: session.userId }, - select: { displayName: true }, - }); - return user; -} - -/** - * Shared document head: Tailwind CDN + a small design system (fonts, keyframes - * for the aurora background, fade-in, shimmer). Both pages import it so the - * look stays consistent. - */ -function pageHead(title: string): string { - return ` - - - ${title} - - - - -`; -} - -/** The animated aurora background blobs, shared by both pages (soft pastels on light). */ -const AURORA = ` -
-
-
-
-
`; - -function renderLoginPage(config: DatabaseRouteConfig): string { - const feishuHref = `/auth/feishu/${encodeURIComponent(config.siloOrganizationSlug)}`; - const devButton = config.allowDevLoginBypass - ? ` -
-
-
开发模式
-
- - 一键登录管理员 - -

仅开发环境可见 · 跳过飞书 OAuth

` - : ""; - - return ` - -${pageHead("Database Admin · 登录")} - - ${AURORA} -
-
-
D
-

Database Admin

-

使用飞书登录以管理数据库

-
- - - - 使用飞书登录 - - ${devButton} -
- -`; -} - -/** Sidebar nav items. `active` marks the current page. `href` "#" = placeholder. */ -const NAV_ITEMS: ReadonlyArray<{ label: string; icon: string; href: string; active: boolean }> = [ - { label: "概览", icon: "M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z", href: "/database/dashboard", active: true }, - { label: "数据表", icon: "M4 5h16v4H4V5Zm0 6h16v4H4v-4Zm0 6h16v2H4v-2Z", href: "#", active: false }, - { label: "查询", icon: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z", href: "#", active: false }, - { label: "设置", icon: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-3 2 1-2 3-2-1a7 7 0 0 1-2 1l-1 2h-4l-1-2a7 7 0 0 1-2-1l-2 1-2-3 2-1a7 7 0 0 1 0-2l-2-1 2-3 2 1a7 7 0 0 1 2-1l1-2h4l1 2a7 7 0 0 1 2 1l2-1 2 3-2 1a7 7 0 0 1 0 2Z", href: "#", active: false }, -]; - -function renderDashboard(displayName: string): string { - const nav = NAV_ITEMS.map((item) => { - const cls = item.active - ? "group flex items-center gap-3 rounded-xl bg-gradient-to-r from-violet-500 to-indigo-500 px-3 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-300/50" - : "group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"; - return ` - - ${item.label} - `; - }).join("\n "); - - const stats = [ - { label: "数据表", value: "—", accent: "from-violet-200/60 to-transparent" }, - { label: "记录数", value: "—", accent: "from-cyan-200/60 to-transparent" }, - { label: "最近查询", value: "—", accent: "from-indigo-200/60 to-transparent" }, - ].map((s, i) => ` -
-
-

${s.label}

-

${s.value}

-
`).join(""); - - const initial = escapeHtml(displayName.slice(0, 1) || "U"); - - return ` - -${pageHead("Database Admin · 概览")} - - ${AURORA} -
- - - - -
-
-
-

概览

-

欢迎回来,这里是数据库管理台

-
- -
- -
-
- ${stats} -
- -
- 内容区占位 · 在 src/database/routes/databaseRoutes.ts 里继续搭建 -
-
-
-
- - - -`; -} - -function escapeHtml(value: string): string { - return value - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """); + // Add more /database/* JSON routes here. Guard data routes with requireSession + // / requireOrgRole (../../admin/auth/guards.js) and scope every query to the + // caller's org (ADR-0020). Access the DB via config.prisma. Register concrete + // routes before registerDatabaseSpa's /database/* fallback (done in ./plugin.ts). } diff --git a/hub/src/database/static.ts b/hub/src/database/static.ts new file mode 100644 index 0000000..a5d01e0 --- /dev/null +++ b/hub/src/database/static.ts @@ -0,0 +1,98 @@ +/** + * Serves the database-admin SPA (built by SvelteKit via `database-admin/build/`) + * and the SPA index fallback for client-side routes under `/database/*`. + * + * The SvelteKit project lives in `hub/database-admin/` and is built with + * `paths.base = '/database'`, so its assets are emitted under `/database/_app/*` + * (not the root `/_app/*` that admin-web owns — that keeps the two SPAs from + * colliding). On disk the files still live at `build/_app/*`; this handler maps + * the `/database`-prefixed URLs back to those files. + * + * Run `npm run build` in database-admin/ to produce the static output. In + * development, `npm run dev` there proxies `/api`, `/auth`, and `/database` to + * the Hub. Override the UI directory with `CPH_DATABASE_UI_DIR` if needed. + * + * Mirrors src/admin/static.ts. + */ +import { readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, extname, join, resolve as resolvePath } from "node:path"; +import type { FastifyInstance } from "fastify"; + +const MIME: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".png": "image/png", + ".jpg": "image/jpeg", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".json": "application/json; charset=utf-8", + ".txt": "text/plain; charset=utf-8", +}; + +function resolveUiDir(): string { + const override = process.env["CPH_DATABASE_UI_DIR"]; + if (override && override.trim() !== "") return resolvePath(override); + const here = dirname(fileURLToPath(import.meta.url)); + return resolvePath(join(here, "..", "..", "database-admin", "build")); +} + +export async function registerDatabaseSpa(app: FastifyInstance): Promise { + const uiDir = resolveUiDir(); + if (!existsSync(join(uiDir, "index.html"))) { + app.log.warn( + { uiDir }, + "database-admin/build not found; /database SPA shell disabled. Run `npm run build` in database-admin/ to enable. /database/config and /database/dev-login remain functional.", + ); + return; + } + const indexHtml = await readFile(join(uiDir, "index.html"), "utf8"); + + // SvelteKit build assets. base='/database' emits them at /database/_app/*, + // but on disk they're under build/_app/*. + app.get("/database/_app/*", async (request, reply) => { + const rel = (request.params as { "*": string })["*"]; + const safe = rel.split("/").filter((p) => p !== ".." && p !== "").join("/"); + try { + const buf = await readFile(join(uiDir, "_app", safe)); + const mime = MIME[extname(safe)] ?? "application/octet-stream"; + return reply.type(mime).send(buf); + } catch { + return reply.status(404).send({ error: { code: "not_found", message: "asset not found" } }); + } + }); + + // Top-level static files emitted under the base path (favicon.svg, robots.txt). + app.get("/database/favicon.svg", async (_request, reply) => { + try { + const buf = await readFile(join(uiDir, "favicon.svg")); + return reply.type("image/svg+xml").send(buf); + } catch { + return reply.status(404).send(); + } + }); + app.get("/database/robots.txt", async (_request, reply) => { + try { + const buf = await readFile(join(uiDir, "robots.txt")); + return reply.type("text/plain; charset=utf-8").send(buf); + } catch { + return reply.status(404).send(); + } + }); + + // SPA client-side route fallback. Concrete /database/* routes (/database/config, + // /database/dev-login, and the asset routes above) are more specific, so + // Fastify's router matches them before this wildcard. Everything else under + // /database serves index.html so SvelteKit's client router can resolve the view. + app.get("/database", async (_request, reply) => { + return reply.type("text/html; charset=utf-8").send(indexHtml); + }); + app.get("/database/*", async (_request, reply) => { + return reply.type("text/html; charset=utf-8").send(indexHtml); + }); +} diff --git a/hub/src/deployment/siloRateLimit.ts b/hub/src/deployment/siloRateLimit.ts index c17b056..62a5c40 100644 --- a/hub/src/deployment/siloRateLimit.ts +++ b/hub/src/deployment/siloRateLimit.ts @@ -2,8 +2,8 @@ * Silo-wide HTTP request rate limit (ADR-0022 `requestRate`). * * Counts dynamic traffic only: APIs, auth, and other application handlers. - * Static SPA assets and the org-admin HTML shell are exempt so a single page - * load (dozens of `/_app/*` chunks + favicon) does not exhaust the minute budget. + * Static SPA assets and the admin HTML shells are exempt so a single page load + * (dozens of `/_app/*` chunks + favicon) does not exhaust the minute budget. */ /** Paths that must not consume the silo HTTP request-rate budget. */ @@ -12,12 +12,21 @@ export function isSiloHttpRateLimitExempt(url: string): boolean { if (path === "/api/healthz") return true; - // SvelteKit build output and top-level static files (see admin/static.ts). + // admin-web SvelteKit build output at the root, and top-level static files + // (see admin/static.ts). if (path === "/_app" || path.startsWith("/_app/")) return true; if (path === "/favicon.ico" || path === "/favicon.svg" || path === "/robots.txt") return true; - // SPA index shell for client-side routes (not an API). + // database-admin SvelteKit build output, served under /database (base path; + // see database/static.ts). + if (path === "/database/_app" || path.startsWith("/database/_app/")) return true; + if (path === "/database/favicon.svg" || path === "/database/robots.txt") return true; + + // SPA index shells for client-side routes (not APIs). The /database/* shell is + // blanket-exempt like /admin/* since client routes are unknowable up front; + // this also covers the once-per-load /database/config bootstrap. if (path === "/admin" || path.startsWith("/admin/")) return true; + if (path === "/database" || path.startsWith("/database/")) return true; return false; } From 192cd43245a97c6adea35d7f42db4f5c953ac006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Thu, 23 Jul 2026 22:31:26 +0800 Subject: [PATCH 02/14] feat(hub): add global nestable member group hierarchy Global, unlimited-depth member groups managed by the platform super admin (requirement 3.1-3.3). Stores membership + nesting only, never permission data; exposes user -> ancestor-closed group set. - MemberGroup: soft delete via archivedAt; parentId FK RESTRICT. Deleting a group cascade-soft-deletes its whole subtree as an application operation, not a DB cascade. - MemberGroupMembership: user<->group many-to-many, revokedAt soft delete, user/group indexed for resolution hot path. - MemberGroupClosure: transitive closure (depth-0 self rows) for one-join ancestor/descendant resolution; maintained on create/reparent with a cycle guard. Permission side (GROUP principal, FOLDER resource, grant inheritance) is deferred. This principal is deliberately not org-scoped and will need ADR-0028 to supersede the ADR-0020 cross-org invariant before the GROUP principal ships. Co-Authored-By: Claude Opus 4.8 --- .../migration.sql | 70 +++++++++++++++++++ hub/prisma/schema.prisma | 62 ++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 hub/prisma/migrations/20260723120000_member_group_hierarchy/migration.sql diff --git a/hub/prisma/migrations/20260723120000_member_group_hierarchy/migration.sql b/hub/prisma/migrations/20260723120000_member_group_hierarchy/migration.sql new file mode 100644 index 0000000..b17f6ec --- /dev/null +++ b/hub/prisma/migrations/20260723120000_member_group_hierarchy/migration.sql @@ -0,0 +1,70 @@ +-- Global, unlimited-depth member group hierarchy (requirement 3.1-3.3). +-- Managed only by the platform super administrator; deliberately NOT +-- org-scoped. Stores membership + nesting only, never permission data. +-- Deletion is soft (archivedAt / revokedAt markers); a group delete +-- cascade-soft-deletes its whole subtree as an application operation. +-- The permission side (GROUP principal, FOLDER resource, grant inheritance) +-- is intentionally deferred to a later migration. + +-- CreateTable +CREATE TABLE "MemberGroup" ( + "id" TEXT NOT NULL, + "parentId" TEXT, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "archivedAt" TIMESTAMP(3), + + CONSTRAINT "MemberGroup_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberGroupMembership" ( + "id" TEXT NOT NULL, + "groupId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "revokedAt" TIMESTAMP(3), + + CONSTRAINT "MemberGroupMembership_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MemberGroupClosure" ( + "ancestorId" TEXT NOT NULL, + "descendantId" TEXT NOT NULL, + "depth" INTEGER NOT NULL, + + CONSTRAINT "MemberGroupClosure_pkey" PRIMARY KEY ("ancestorId", "descendantId") +); + +-- CreateIndex +CREATE INDEX "MemberGroup_parentId_archivedAt_idx" ON "MemberGroup"("parentId", "archivedAt"); + +-- CreateIndex +CREATE INDEX "MemberGroupMembership_userId_revokedAt_idx" ON "MemberGroupMembership"("userId", "revokedAt"); + +-- CreateIndex +CREATE INDEX "MemberGroupMembership_groupId_revokedAt_idx" ON "MemberGroupMembership"("groupId", "revokedAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "MemberGroupMembership_groupId_userId_revokedAt_key" ON "MemberGroupMembership"("groupId", "userId", "revokedAt"); + +-- CreateIndex +CREATE INDEX "MemberGroupClosure_descendantId_idx" ON "MemberGroupClosure"("descendantId"); + +-- AddForeignKey +ALTER TABLE "MemberGroup" ADD CONSTRAINT "MemberGroup_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "MemberGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberGroupMembership" ADD CONSTRAINT "MemberGroupMembership_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "MemberGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberGroupMembership" ADD CONSTRAINT "MemberGroupMembership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberGroupClosure" ADD CONSTRAINT "MemberGroupClosure_ancestorId_fkey" FOREIGN KEY ("ancestorId") REFERENCES "MemberGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MemberGroupClosure" ADD CONSTRAINT "MemberGroupClosure_descendantId_fkey" FOREIGN KEY ("descendantId") REFERENCES "MemberGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/hub/prisma/schema.prisma b/hub/prisma/schema.prisma index 9e619ea..cf7c2bf 100644 --- a/hub/prisma/schema.prisma +++ b/hub/prisma/schema.prisma @@ -193,6 +193,7 @@ model User { heldLocks ProjectAgentLock[] @relation("lockHolder") feishuBindings ProjectGroupBinding[] @relation("bindingCreator") teamMemberships TeamMembership[] + memberGroupMemberships MemberGroupMembership[] externalPrincipalMemberships ExternalPrincipalMembership[] permissionGrants PermissionGrant[] @relation("grantCreator") roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator") @@ -393,6 +394,67 @@ model TeamExternalBinding { @@index([teamId, revokedAt]) } +// --- Member groups (global, nestable authorization principal) ------------ + +/// Global, unlimited-depth member group. Managed only by the platform super administrator (ADR-0023); +/// Deletion is soft: `archivedAt` is a marker. +/// Deleting a group cascade-soft-deletes its whole subtree — an application +/// operation (walk the subtree, stamp archivedAt), not a DB constraint. +model MemberGroup { + id String @id @default(cuid()) + parentId String? + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + archivedAt DateTime? + + parent MemberGroup? @relation("groupTree", fields: [parentId], references: [id], onDelete: Restrict) + children MemberGroup[] @relation("groupTree") + memberships MemberGroupMembership[] + asAncestor MemberGroupClosure[] @relation("ancestor") + asDescendant MemberGroupClosure[] @relation("descendant") + + @@index([parentId, archivedAt]) +} + +/// User↔group many-to-many; a user may belong to multiple groups. Soft delete +/// via `revokedAt` (same pattern as TeamMembership) allows re-adding a removed +/// member. `userId, revokedAt` index is the resolution hot path: fetch a user's direct groups. +model MemberGroupMembership { + id String @id @default(cuid()) + groupId String + userId String + createdAt DateTime @default(now()) + revokedAt DateTime? + + group MemberGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([groupId, userId, revokedAt]) + @@index([userId, revokedAt]) + @@index([groupId, revokedAt]) +} + +/// Transitive closure of the MemberGroup tree. Every group has a depth=0 +/// self row. Turns ancestor/descendant resolution into one indexed join +/// instead of a recursive CTE; maintained only on create / reparent (rare +/// super-admin ops, so the write cost is amortized against hot reads). On soft +/// delete the closure rows are retained; resolution filters by +/// MemberGroup.archivedAt. Reparent must reject a new parent inside the moved +/// subtree (cycle guard). +model MemberGroupClosure { + ancestorId String + descendantId String + depth Int + + ancestor MemberGroup @relation("ancestor", fields: [ancestorId], references: [id], onDelete: Cascade) + descendant MemberGroup @relation("descendant", fields: [descendantId], references: [id], onDelete: Cascade) + + @@id([ancestorId, descendantId]) + @@index([descendantId]) +} + /// Locally synchronized Feishu external principal membership. model ExternalDirectoryConnection { id String @id @default(cuid()) From 2f2ece1a3a1c0632f7172b730ab41d312f551f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 16:39:38 +0800 Subject: [PATCH 03/14] fix(database): expose submitCreate on window.__lib in library browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新建根目录/子节点弹窗的「创建」按钮 onclick 调 window.__lib.submitCreate, 但该函数虽已定义却漏挂到 __lib 导出表,导致点击报 "submitCreate is not a function"。补挂即可。 Co-Authored-By: Claude Opus 4.8 --- hub/src/database/routes/libraryBrowser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/src/database/routes/libraryBrowser.ts b/hub/src/database/routes/libraryBrowser.ts index bc61435..0499f48 100644 --- a/hub/src/database/routes/libraryBrowser.ts +++ b/hub/src/database/routes/libraryBrowser.ts @@ -574,6 +574,7 @@ export function renderLibraryBrowser(): string { ok("已收回"); renderTab(); } catch (e) { fail(e); } }, + submitCreate, newFileDialog, submitNewFile, openFile, closePreview, saveFile, deleteFile, showHistory, acceptLatest() { openFile(currentFile.path); toast("已载入最新内容,请在此基础上合并", "info"); }, submitExport, closeModal, From 11a7ec800413192cd37a3383c3674a93896af919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 18:06:18 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat(database):=20=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=88=90=E5=91=98=E7=BB=84(MemberGroup)=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=B8=8E=E5=B5=8C=E5=A5=97=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 4 + ...-member-group-management-and-resolution.md | 153 ++++ hub/.gitignore | 1 + hub/filelib-web/src/lib/BrowserShell.svelte | 32 +- hub/filelib-web/src/lib/GroupAdmin.svelte | 280 +++++++ hub/filelib-web/src/lib/types.ts | 15 + hub/src/database/README.md | 11 +- hub/src/database/filelib/audit.ts | 9 +- hub/src/database/filelib/groupResolver.ts | 14 +- .../database/filelib/memberGroupResolver.ts | 35 + .../database/filelib/memberGroupService.ts | 607 +++++++++++++++ hub/src/database/routes/adminPanels.ts | 688 +++++++++++++++--- hub/src/database/routes/databaseRoutes.ts | 15 +- hub/src/database/routes/filelibRoutes.ts | 25 +- hub/src/database/routes/memberGroupRoutes.ts | 196 +++++ hub/src/database/routes/uiTheme.ts | 17 + hub/test/integration/member-groups.test.ts | 311 ++++++++ 17 files changed, 2289 insertions(+), 124 deletions(-) create mode 100644 docs/adr/0028-member-group-management-and-resolution.md create mode 100644 hub/filelib-web/src/lib/GroupAdmin.svelte create mode 100644 hub/src/database/filelib/memberGroupResolver.ts create mode 100644 hub/src/database/filelib/memberGroupService.ts create mode 100644 hub/src/database/routes/memberGroupRoutes.ts create mode 100644 hub/test/integration/member-groups.test.ts diff --git a/CONTEXT.md b/CONTEXT.md index 0bac2d5..0cba565 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -99,3 +99,7 @@ _Avoid_: Cost budget, unlimited run **Emergency Workload Brake**: An audited Platform Administrator control that prevents new agent work for one Organization or the whole platform and may explicitly stop active work during an incident. _Avoid_: Organization deletion, service restart + +**Member Group**: +A global, unlimited-depth, nestable authorization principal managed by the website administrator; a file-library grant on a group applies to that group and its whole descendant subtree, and a user's effective permission collects every group they belong to plus those groups' ancestors (ADR-0028). It stores no folder/project permission itself — only the user→group membership. Global: not owned by any Organization. +_Avoid_: Team (the org-scoped flat grouping), Feishu department diff --git a/docs/adr/0028-member-group-management-and-resolution.md b/docs/adr/0028-member-group-management-and-resolution.md new file mode 100644 index 0000000..17dae92 --- /dev/null +++ b/docs/adr/0028-member-group-management-and-resolution.md @@ -0,0 +1,153 @@ +# ADR 0028: Member Group Management And Resolution + +## Status + +Accepted. + +## Context + +ADR-0020 fixed `Organization` as the tenant root and ADR-0019 pinned the +principal-set permission model. The file library (《文件库-接口契约.md》) computes +effective permission over two principal kinds — `USER` and `GROUP` — and consumes +the group side through a single read-only port, `GroupResolver` +(`resolveMemberGroupIds(userId) → groupIds[]`, contract C2/G2). + +The contract's v0.1 proposal framed the Group system as a *separate HTTP service* +owned by another team, consumed read-only. In practice the schema now carries the +group tables directly in the hub database (`MemberGroup`, `MemberGroupMembership`, +`MemberGroupClosure` — a global, unlimited-depth, closure-backed hierarchy), and +the product requirement is to build **group management in the backend admin**, not +to integrate a foreign service. Until this ADR, nothing read or wrote those tables: +the live `GroupResolver` was a transitional implementation reading flat hub `Team` +membership, and the admin "Group 管理" panel actually managed `Team`. + +This ADR settles the semantics needed to make the `MemberGroup` tables the real, +in-hub group system. + +## Decision + +### Group system is in-hub, not a foreign service + +`MemberGroup` is the platform's global member-group principal. It lives in the hub +database and is managed through the `/database` backend. The contract's "separate +service" framing was an unfrozen v0.1 proposal; the implementation aligns to the +tables that were actually built. The `GroupResolver` port stays — an external +`HUB_GROUP_SERVICE_URL` HTTP implementation remains a supported override — but the +default implementation reads the in-hub `MemberGroup` closure. + +### Authority: website administrator only + +Group create/delete and member add/remove are restricted to the **website +administrator**, defined (consistently with the rest of the file library, D19/C4 +adaptation) as an `OWNER`/`ADMIN` of the silo Organization (`isWebsiteAdmin` in +`filelib/guards.ts`). ADR-0023's `PlatformIdentity` is the future "true" platform +control plane; the file library uniformly uses org OWNER/ADMIN today and this +feature stays consistent with that. Reading groups for the authorization selector +(`/groups/search`) is **not** admin-gated — picking a group to grant is a Manage +holder's ability, not an administrator's. + +### Resolution semantics (the crux) + +`resolveMemberGroupIds(user)` returns the user's **active direct groups ∪ the +active ancestors of those groups**, deduplicated (the closure's depth-0 self row +makes each direct group its own ancestor). This is the single query the permission +engine relies on; equivalently: a grant placed on group G applies to members of G +and of every descendant of G (requirement 3.2 — permission flows down the tree, so +resolution collects up the tree). It is computed **live, never cached** (contract +D4/G4): a membership change is visible on the very next protected request. + +MemberGroup is global (no `organizationId`), so resolution is not org-scoped. + +### Soft delete via `archivedAt`, cascading the subtree + +Delete is soft: `MemberGroup.archivedAt` is a tag. Deleting a group +cascade-soft-deletes its **whole subtree** (walk `MemberGroupClosure` where +`ancestorId = G`, stamp `archivedAt` on each active descendant) — an application +operation, not a DB constraint. Closure and membership rows are **retained**; +resolution and listing filter by `archivedAt`, so an archived group and everything +under it stop contributing to permission at once. + +### Closure maintenance + +The closure is maintained on **create**: insert `(G, G, 0)`, then for a parent `P` +insert `(a.ancestorId, G, a.depth + 1)` for every `a` in +`closure where descendantId = P`. v1 does **not** support reparenting a group +(moving it under a new parent). The schema reserves reparent (closure rebuild plus +the cycle guard "reject a new parent inside the moved subtree"); it is a follow-on. + +### Rename and description edits are in scope; reparent stays out + +A group's `name` and `description` are mutable by the website administrator +(`PATCH /database/api/groups/:id`, audited as `group.update`). This is deliberately +separated from reparent: renaming touches **no** closure row and cannot create a +cycle, so it carries none of the invariant risk that keeps reparent out of v1. The +endpoint therefore **rejects** a `parentId` field outright rather than ignoring it, +so a future reparent cannot arrive silently through this route. Passing an empty +`description` clears it; omitting a field leaves it unchanged. + +### Restore is deliberately asymmetric with delete + +Archived groups stay visible to the administrator (`GET +/database/api/groups?includeArchived=1` returns them carrying `archivedAt`; the +console tags and greys them) and can be restored (`POST +/database/api/groups/:id/restore`, audited as `group.restore`). + +Restore is **not** the mirror image of delete. Delete cascades down the whole +subtree; restore un-archives **the group plus every archived ancestor of it, and +nothing below it**: + +- Restoring the ancestor chain is **mandatory**, not a convenience. An active group + whose parent is archived has no path in the tree, and the `depth` derivation + (closure row count) presumes "an active group's ancestors are active" — the + invariant that cascade-delete establishes. Restoring a node alone would break it. +- The subtree is deliberately **left archived**. A group's descendants may have been + archived for reasons of their own, and one click should not silently re-grant + permission across a whole historical branch. Descendants remain visible in their + archived state and are each restored explicitly. + +Restore takes effect immediately, like every other membership change (D4/G4): the +group resumes contributing permission on the next resolution. + +An archived group is **readable but not writable**. Its membership rows are never +revoked by archiving, so `listMembers` succeeds on an archived group — the console +must be able to show *who was in it* before deciding whether to restore it. Every +mutation, by contrast, still requires an active group (`requireActiveGroup` → 404): +rename, child creation, and member add/remove all reject. The group is inert for +permission purposes and frozen for editing, but not hidden and not forgotten. + +### Member picker reads global users, admin-only + +`GET /database/api/users/search` backs the "add member" picker: it matches `User` +by display name or Feishu open id and is gated to the website administrator, the +same authority that may add members. It widens no existing capability — adding a +member already accepts **any** global user (`resolveUser` does not require an org +membership), so the endpoint only replaces blind id entry with search. It is +deliberately **not** opened to the non-admin authorization-selector audience that +`/groups/search` serves: choosing a group to grant is a Manage-holder action, +whereas enumerating people is not. `excludeGroupId` filters out the target group's +active members so the picker cannot surface a candidate that must 409. + +### Audit is written in-hub + +The contract (C3 §6.3) originally deferred group actions to the foreign Group +service's own audit. With the group system in-hub, group mutations are audited +through the existing file-library sink (`filelib/audit.ts`, same-transaction +`AuditEntry`) under the silo Organization — `MemberGroup` has no `organizationId`, +so the audit row is attributed to the silo org. New actions: `group.create`, +`group.update`, `group.delete`, `group.restore`, `group.member_add`, +`group.member_remove`; new audit object type `group`. + +## Consequences + +- The default `GroupResolver` becomes the in-hub `MemberGroup` closure reader. + `createTeamGroupResolver` is retained but deprecated (no longer wired); existing + flat-Team group grants no longer resolve for the file library. +- Group grants take effect in real time through the existing `effectiveRole` + reducer (P6) with no change to the permission algebra — only the set of group ids + fed to it changes. +- v1 omits reparent; the closure invariants above must hold whenever reparent is + added later (rebuild descendants' ancestor rows, reject cycles). +- Group management is an admin-only surface; the authorization selector is not. +- Numeric limits (max depth, max members) and a hard-delete/restore path remain + follow-on operational decisions; they must not weaken the archived-filter, + admin-authority, or live-resolution invariants fixed here. diff --git a/hub/.gitignore b/hub/.gitignore index 4e09b8a..9830097 100644 --- a/hub/.gitignore +++ b/hub/.gitignore @@ -8,6 +8,7 @@ dist/ .dev-keyring.json .dev-workspaces/ .dev-skills/ +.filelib-repos/ admin-web/node_modules/ admin-web/build/ admin-web/.svelte-kit/ diff --git a/hub/filelib-web/src/lib/BrowserShell.svelte b/hub/filelib-web/src/lib/BrowserShell.svelte index 87d841f..e992058 100644 --- a/hub/filelib-web/src/lib/BrowserShell.svelte +++ b/hub/filelib-web/src/lib/BrowserShell.svelte @@ -7,8 +7,12 @@ import TreeNode from "./TreeNode.svelte"; import NodeDetailPanel from "./NodeDetailPanel.svelte"; import FileEditor from "./FileEditor.svelte"; + import GroupAdmin from "./GroupAdmin.svelte"; import Modal from "./Modal.svelte"; + // 视图切换(仅管理员可见 Group 管理入口;非管理员恒为 library)。 + let view = $state<"library" | "groups">("library"); + let roots = $state(null); let treeError = $state(null); let showCreateRoot = $state(false); @@ -62,7 +66,31 @@ const initial = $derived(($me?.userId ?? "U").slice(0, 1).toUpperCase()); -
+
+ {#if $me?.isWebsiteAdmin} + +
+ + +
+ {/if} + +
+ {#if $me?.isWebsiteAdmin && view === "groups"} + + {:else}
{#if showCreateRoot} diff --git a/hub/filelib-web/src/lib/GroupAdmin.svelte b/hub/filelib-web/src/lib/GroupAdmin.svelte new file mode 100644 index 0000000..8eaea46 --- /dev/null +++ b/hub/filelib-web/src/lib/GroupAdmin.svelte @@ -0,0 +1,280 @@ + + +
+ + + + +
+ {#if selected === null} +
从左侧选择一个 Group 查看成员
+ {:else} +
{selected.name}
+ {#if selected.description} +

{selected.description}

+ {:else} +
+ {/if} + +
+
+ + { if (e.key === "Enter") void addMember(); }} + /> +
+ +
+ +
+
+ 成员 {members === null ? "" : `(${members.length})`} +
+ {#if members === null} +
加载中…
+ {:else if membersError} +
{membersError}
+ {:else if members.length === 0} +
暂无成员
+ {:else} + {#each members as m (m.userId)} +
+ {m.displayName} + {m.userId} + +
+ {/each} + {/if} +
+ {/if} +
+
+ +{#if showCreate} + (showCreate = false)}> +
+ + +
+
+ + +
+
+ + +
+
+{/if} diff --git a/hub/filelib-web/src/lib/types.ts b/hub/filelib-web/src/lib/types.ts index 60bfc35..22e5179 100644 --- a/hub/filelib-web/src/lib/types.ts +++ b/hub/filelib-web/src/lib/types.ts @@ -83,3 +83,18 @@ export interface GroupSearchResult { readonly name: string; readonly breadcrumb: string; } + +/** 成员组(ADR-0028);后端返回扁平列表,前端按 parentId/depth 拼树。 */ +export interface MemberGroupNode { + readonly id: string; + readonly parentId: string | null; + readonly name: string; + readonly description: string | null; + readonly depth: number; + readonly memberCount: number; +} + +export interface MemberGroupMember { + readonly userId: string; + readonly displayName: string; +} diff --git a/hub/src/database/README.md b/hub/src/database/README.md index 6c29dc3..8b927d4 100644 --- a/hub/src/database/README.md +++ b/hub/src/database/README.md @@ -74,8 +74,10 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 |------|------| | `plugin.ts` | 模块对外入口,`hub.ts` 调 `registerDatabasePlugin()` | | `routes/databaseRoutes.ts` | 登录页/dashboard + 各子路由装配点 | -| `routes/filelibRoutes.ts` | 文件库 树/授权/搜索 API | +| `routes/filelibRoutes.ts` | 文件库 树/授权 API | | `routes/fileRoutes.ts` | 文件库 文件内容/导出 API | +| `routes/memberGroupRoutes.ts` | 成员组管理 API + `/groups/search` + `/users/search`(ADR-0028) | +| `routes/adminPanels.ts` | dashboard「用户管理」(org 成员)/「Group 管理」(MemberGroup 嵌套树)面板 | | `routes/libraryPage.ts` | `/database/library` 文件库浏览页 | | `filelib/` | 文件库领域层(见下) | @@ -98,7 +100,9 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 | `filelib/fileService.ts` | 文件路径安全 + 版本化读写(先 git 后审计的顺序铁律) | | `filelib/exportService.ts` | 导出 job 状态机(D10 异步)+ ExportAdapter port | | `filelib/versionStore.ts` | 契约 C1 port + 内存实现(版本团队 npm 包到位后替换) | -| `filelib/groupResolver.ts` | 契约 C2 port + Team 过渡实现 | +| `filelib/groupResolver.ts` | 契约 C2 port(+ 已弃用的 Team 过渡实现,ADR-0028) | +| `filelib/memberGroupResolver.ts` | **默认** C2 实现:读 in-hub MemberGroup 闭包(ADR-0028) | +| `filelib/memberGroupService.ts` | 成员组 CRUD(含改名)+ 成员增删 + 闭包维护 + 搜索(ADR-0028) | | `filelib/groupResolverHttp.ts` | C2 HTTP 实现(HUB_GROUP_SERVICE_URL 启用;失败 → 503) | | `filelib/audit.ts` | 审计动作词表(C3 §6.3)+ 同事务写入 | | `filelib/guards.ts` | session → FileLibActor;网站管理员 = org OWNER/ADMIN(D19) | @@ -107,7 +111,8 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 环境变量: - `HUB_FILELIB_STORAGE_ROOT` — 项目 git 仓库根目录(默认 `./.filelib-repos`) -- `HUB_GROUP_SERVICE_URL` — Group 团队服务地址(C2);未配置时读 hub Team(扁平) +- `HUB_GROUP_SERVICE_URL` — 外部 Group 服务地址(C2);**未配置时读 in-hub + MemberGroup 闭包**(ADR-0028 起的默认;此前是扁平 hub Team) > ⚠️ 开发期注意:当前 VersionStore 是**进程内存**实现,**服务重启后仓库全失**, > 此前创建的项目再访问文件会报 `repo_not_found`(需重建项目)。版本团队的 diff --git a/hub/src/database/filelib/audit.ts b/hub/src/database/filelib/audit.ts index f58a92c..fe01d09 100644 --- a/hub/src/database/filelib/audit.ts +++ b/hub/src/database/filelib/audit.ts @@ -32,9 +32,16 @@ export const FILE_LIB_AUDIT_ACTIONS = { fileConflictDetected: "file.conflict_detected", exportRun: "export.run", adminForceAdjust: "admin.force_adjust", + // ADR-0028:成员组内置进 hub,组动作在本地审计(契约 C3 §6.3 原委托外部 Group 服务)。 + groupCreate: "group.create", + groupUpdate: "group.update", + groupDelete: "group.delete", + groupRestore: "group.restore", + groupMemberAdd: "group.member_add", + groupMemberRemove: "group.member_remove", } as const; -export type FileLibAuditObjectType = "folder" | "project" | "file" | "grant" | "export_job"; +export type FileLibAuditObjectType = "folder" | "project" | "file" | "grant" | "export_job" | "group"; export interface FileLibAuditEntry { readonly action: string; diff --git a/hub/src/database/filelib/groupResolver.ts b/hub/src/database/filelib/groupResolver.ts index a3aa546..2134476 100644 --- a/hub/src/database/filelib/groupResolver.ts +++ b/hub/src/database/filelib/groupResolver.ts @@ -2,8 +2,10 @@ * GroupResolver port(契约 C2)。 * * 权限计算只依赖这一个查询:"用户 → 所属 Group(含全部祖先)"。 - * Group 系统(需求系统二:全局、无限嵌套)由别的团队交付;调用方只依赖此 - * port,真身到位后替换实现,不换调用点。 + * ADR-0028 起,默认实现是 in-hub 的 MemberGroup 闭包读取器 + * (`createMemberGroupResolver`,见 memberGroupResolver.ts); + * `HUB_GROUP_SERVICE_URL` 配置后切外部 HTTP 实现(groupResolverHttp.ts)。 + * 调用方只依赖此 port,不换调用点。 */ import type { PrismaClient } from "@prisma/client"; @@ -13,9 +15,11 @@ export interface GroupResolver { } /** - * 过渡实现:读 hub 既有 Team(org 内、扁平无嵌套 → "祖先即自身")。 - * 需求 3.2 的祖先递归语义在嵌套 Group 落地前无从谈起;此实现保证权限引擎 - * 的 Group 通路今天就是真的,而不是 mock。 + * @deprecated ADR-0028:成员组已内置为 in-hub MemberGroup,默认 resolver 改为 + * `createMemberGroupResolver`。此扁平 Team 过渡实现不再接线,保留仅为历史参照 + * (以及潜在的迁移对照),新代码不要使用。 + * + * 旧过渡实现:读 hub 既有 Team(org 内、扁平无嵌套 → "祖先即自身")。 */ export function createTeamGroupResolver( prisma: PrismaClient, diff --git a/hub/src/database/filelib/memberGroupResolver.ts b/hub/src/database/filelib/memberGroupResolver.ts new file mode 100644 index 0000000..60c2730 --- /dev/null +++ b/hub/src/database/filelib/memberGroupResolver.ts @@ -0,0 +1,35 @@ +/** + * 默认 GroupResolver 实现:读 in-hub MemberGroup 闭包(ADR-0028)。 + * + * resolveMemberGroupIds(user) = 用户**活跃直接组 ∪ 这些组的活跃祖先**,去重 + * (闭包 depth0 自身行令每个直接组也是自己的祖先)。等价于:授权放在组 G 上, + * G 及其全部子孙的成员都命中(需求 3.2 权限沿树向下 → 解析沿树向上收集)。 + * + * 实时、不缓存(契约 D4/G4):成员变更在下一次受保护请求即可见。 + * MemberGroup 全局(无 organizationId),解析不做 org scope。 + * 两条 Prisma 查询,不用裸 SQL(与 treeService 风格一致)。 + */ + +import type { PrismaClient } from "@prisma/client"; +import type { GroupResolver } from "./groupResolver.js"; + +export function createMemberGroupResolver(prisma: PrismaClient): GroupResolver { + return { + async resolveMemberGroupIds(userId) { + // 1) 活跃直接组:成员未撤销 + 组未归档。 + const direct = await prisma.memberGroupMembership.findMany({ + where: { userId, revokedAt: null, group: { archivedAt: null } }, + select: { groupId: true }, + }); + if (direct.length === 0) return []; + const directIds = direct.map((m) => m.groupId); + + // 2) 经闭包取活跃祖先(含 depth0 自身);祖先组须未归档。 + const ancestors = await prisma.memberGroupClosure.findMany({ + where: { descendantId: { in: directIds }, ancestor: { archivedAt: null } }, + select: { ancestorId: true }, + }); + return [...new Set(ancestors.map((a) => a.ancestorId))]; + }, + }; +} diff --git a/hub/src/database/filelib/memberGroupService.ts b/hub/src/database/filelib/memberGroupService.ts new file mode 100644 index 0000000..8035d8d --- /dev/null +++ b/hub/src/database/filelib/memberGroupService.ts @@ -0,0 +1,607 @@ +/** + * 成员组(MemberGroup)管理服务(ADR-0028)。 + * + * 语义锚定: + * - 全局主体:MemberGroup 无 organizationId,不做租户 scope;审计行挂 silo org + * (deps.organizationId)—— MemberGroup 无 orgId,审计沿用文件库 sink(决策4)。 + * - 权限门禁:创建/删除/成员增删仅网站管理员(silo org OWNER/ADMIN); + * 搜索(授权选择器)不限管理员 —— 选组授权是 Manage 持有者的能力(决策2)。 + * - 软删除:archivedAt 打标;删组级联软删整棵子树(闭包 ancestorId=G); + * 闭包/成员行保留,list/解析按 archivedAt 过滤(决策4)。 + * - 闭包维护:仅 create —— 插 (G,G,0),再对 parent P 插 + * (a.ancestorId, G, a.depth+1) for a in closure where descendantId=P。 + * v1 不支持 reparent(决策5)。 + * + * 与 hub Team 不同:成员是全局用户,不要求 org membership;按 userId 或 + * User.feishuOpenId(全局 @unique)解析。 + */ + +import type { PrismaClient, Prisma } from "@prisma/client"; +import { FileLibError } from "./model.js"; +import type { FileLibActor } from "./treeService.js"; +import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js"; + +export interface MemberGroupServiceDeps { + readonly prisma: PrismaClient; + /** silo org id —— 仅用于审计归属(MemberGroup 全局无 orgId,决策4)。 */ + readonly organizationId: string; +} + +export interface MemberGroupDto { + readonly id: string; + readonly parentId: string | null; + readonly name: string; + readonly description: string | null; + /** 到根的边数(根 = 0);由闭包行数推导。 */ + readonly depth: number; + readonly memberCount: number; + /** 软删标记(决策4)。null = 活跃;非 null = 已归档,不贡献任何权限。 */ + readonly archivedAt: Date | null; +} + +export interface MemberGroupMemberDto { + readonly userId: string; + readonly displayName: string; + readonly feishuOpenId: string; + readonly avatarUrl: string | null; + /** 加入本组时间(membership.createdAt),用于成员表排序/展示。 */ + readonly joinedAt: Date; +} + +/** 成员选择器候选(加成员弹窗搜索用)。 */ +export interface UserSearchResult { + readonly userId: string; + readonly displayName: string; + readonly feishuOpenId: string; + readonly avatarUrl: string | null; +} + +export interface MemberGroupSearchResult { + readonly id: string; + readonly name: string; + /** 祖先链(根在前,自身在末),用 " / " 连接;无祖先时即自身名。 */ + readonly breadcrumb: string; +} + +export interface CreateMemberGroupInput { + readonly name: string; + readonly description?: string | undefined; + readonly parentId?: string | null | undefined; +} + +export interface AddMemberInput { + readonly userId?: string | undefined; + readonly feishuOpenId?: string | undefined; +} + +/** 改名/改描述(决策6)。字段缺省 = 不动;description 传空串 = 清空。 */ +export interface UpdateMemberGroupInput { + readonly name?: string | undefined; + readonly description?: string | undefined; +} + +type Tx = Prisma.TransactionClient; + +/* ---------------------------------------------------------------- 内部工具 */ + +/** 管理门禁:非网站管理员一律 403(决策2)。 */ +function requireAdmin(actor: FileLibActor): void { + if (!actor.isWebsiteAdmin) { + throw new FileLibError(403, "forbidden", "group management requires website administrator"); + } +} + +/** 组名校验(Group 域与节点域分开:轻量 trim/非空/长度,不套用节点命名规则)。 */ +function normalizeGroupName(raw: string): string { + const name = raw.trim(); + if (name === "") throw new FileLibError(400, "invalid_request", "group name must not be empty"); + if (name.length > 100) throw new FileLibError(400, "invalid_request", "group name too long (max 100)"); + return name; +} + +async function requireActiveGroup( + client: PrismaClient | Tx, + groupId: string, +): Promise<{ readonly id: string; readonly name: string }> { + const group = await client.memberGroup.findFirst({ + where: { id: groupId, archivedAt: null }, + select: { id: true, name: true }, + }); + if (group === null) throw new FileLibError(404, "group_not_found", "group not found"); + return group; +} + +/** 全局用户解析:按 userId,或 User.feishuOpenId(全局 @unique)。不要求 org 成员。 */ +async function resolveUser( + tx: Tx, + input: AddMemberInput, +): Promise<{ + readonly id: string; + readonly displayName: string; + readonly feishuOpenId: string; + readonly avatarUrl: string | null; +}> { + const select = { id: true, displayName: true, feishuOpenId: true, avatarUrl: true } as const; + if (input.userId !== undefined && input.userId !== "") { + const user = await tx.user.findUnique({ where: { id: input.userId }, select }); + if (user === null) throw new FileLibError(404, "user_not_found", `user not found: ${input.userId}`); + return user; + } + if (input.feishuOpenId !== undefined && input.feishuOpenId !== "") { + const user = await tx.user.findUnique({ + where: { feishuOpenId: input.feishuOpenId }, + select, + }); + if (user === null) throw new FileLibError(404, "user_not_found", `user not found: ${input.feishuOpenId}`); + return user; + } + throw new FileLibError(400, "invalid_request", "userId or feishuOpenId is required"); +} + +/* ---------------------------------------------------------------- 公共操作 */ + +/** + * 创建成员组(建根 / 建子)。仅网站管理员。事务内维护闭包。 + * parentId 给定时校验其活跃存在;闭包:插自身 depth0 + 继承 parent 的祖先。 + */ +export async function createMemberGroup( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + input: CreateMemberGroupInput, +): Promise { + requireAdmin(actor); + const name = normalizeGroupName(input.name); + const description = input.description?.trim() || null; + const parentId = input.parentId ?? null; + + return deps.prisma.$transaction(async (tx) => { + let parentClosure: { ancestorId: string; depth: number }[] = []; + if (parentId !== null) { + const parent = await tx.memberGroup.findFirst({ + where: { id: parentId, archivedAt: null }, + select: { id: true }, + }); + if (parent === null) throw new FileLibError(404, "group_not_found", "parent group not found"); + parentClosure = await tx.memberGroupClosure.findMany({ + where: { descendantId: parentId }, + select: { ancestorId: true, depth: true }, + }); + } + + const group = await tx.memberGroup.create({ + data: { name, parentId, ...(description !== null ? { description } : {}) }, + select: { id: true, parentId: true, name: true, description: true }, + }); + + // 闭包维护:自身 depth0,再继承 parent 的每个祖先(depth+1)。 + await tx.memberGroupClosure.create({ + data: { ancestorId: group.id, descendantId: group.id, depth: 0 }, + }); + if (parentClosure.length > 0) { + await tx.memberGroupClosure.createMany({ + data: parentClosure.map((a) => ({ + ancestorId: a.ancestorId, + descendantId: group.id, + depth: a.depth + 1, + })), + }); + } + // parent 的闭包行数 = parent.depth + 1 = 新组 depth(闭包不变量)。 + const depth = parentClosure.length; + + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupCreate, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { name, parentId }, + }); + + return { + id: group.id, + parentId: group.parentId, + name: group.name, + description: group.description, + depth, + memberCount: 0, + archivedAt: null, + }; + }); +} + +/** + * 改名 / 改描述(决策6)。仅网站管理员。**不动 parentId** —— reparent 仍属 v1 + * 范围外(决策5),闭包无需维护。字段缺省即不动;description 传 "" 清空。 + */ +export async function updateMemberGroup( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, + input: UpdateMemberGroupInput, +): Promise { + requireAdmin(actor); + if (input.name === undefined && input.description === undefined) { + throw new FileLibError(400, "invalid_request", "name or description is required"); + } + const name = input.name === undefined ? undefined : normalizeGroupName(input.name); + + return deps.prisma.$transaction(async (tx) => { + await requireActiveGroup(tx, groupId); + const group = await tx.memberGroup.update({ + where: { id: groupId }, + data: { + ...(name !== undefined ? { name } : {}), + ...(input.description !== undefined + ? { description: input.description.trim() || null } + : {}), + }, + select: { id: true, parentId: true, name: true, description: true }, + }); + + // depth 由闭包行数推导(与 listMemberGroups 同一不变量);update 不改闭包。 + const closureCount = await tx.memberGroupClosure.count({ where: { descendantId: groupId } }); + const memberCount = await tx.memberGroupMembership.count({ + where: { groupId, revokedAt: null }, + }); + + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupUpdate, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { + ...(name !== undefined ? { name } : {}), + ...(input.description !== undefined ? { description: group.description } : {}), + }, + }); + + return { + id: group.id, + parentId: group.parentId, + name: group.name, + description: group.description, + depth: closureCount - 1, + memberCount, + archivedAt: null, // requireActiveGroup 已保证是活跃组 + }; + }); +} + +/** + * 软删除成员组:级联软删整棵子树(闭包 ancestorId=G 的全部活跃 descendant)。 + * 闭包/成员行保留;list/解析按 archivedAt 过滤,整支立即停止贡献权限。 + */ +export async function deleteMemberGroup( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, +): Promise<{ readonly archivedCount: number }> { + requireAdmin(actor); + return deps.prisma.$transaction(async (tx) => { + const group = await requireActiveGroup(tx, groupId); + const subtree = await tx.memberGroupClosure.findMany({ + where: { ancestorId: groupId }, + select: { descendantId: true }, + }); + const ids = subtree.map((r) => r.descendantId); + const now = new Date(); + const result = await tx.memberGroup.updateMany({ + where: { id: { in: ids }, archivedAt: null }, + data: { archivedAt: now }, + }); + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupDelete, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { name: group.name, archivedCount: result.count }, + }); + return { archivedCount: result.count }; + }); +} + +/** + * 恢复(取消归档)。仅网站管理员。**与删除不对称**(决策7): + * - 删除级联整棵子树;恢复只恢复「该组 + 其全部已归档祖先」,**不动子树**。 + * - 恢复祖先链是必须的:活跃组的祖先必须活跃,否则该组在树上无路径、 + * depth 推导(闭包行数)与"祖先必活跃"的前提脱节。 + * - 子树保持归档、仍可见(带标记),由管理员逐个决定是否恢复 —— 避免一次 + * 恢复意外把整支历史组全部重新授权。 + * 恢复即刻恢复该组贡献的权限(实时解析,不缓存)。 + */ +export async function restoreMemberGroup( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, +): Promise<{ readonly restoredCount: number }> { + requireAdmin(actor); + return deps.prisma.$transaction(async (tx) => { + const group = await tx.memberGroup.findUnique({ + where: { id: groupId }, + select: { id: true, name: true, archivedAt: true }, + }); + if (group === null) throw new FileLibError(404, "group_not_found", "group not found"); + if (group.archivedAt === null) { + throw new FileLibError(409, "not_archived", "group is not archived"); + } + + // 自身 + 祖先(闭包 descendantId=G 含 depth0 自身),只挑已归档的解标。 + const chain = await tx.memberGroupClosure.findMany({ + where: { descendantId: groupId }, + select: { ancestorId: true }, + }); + const ids = chain.map((r) => r.ancestorId); + const result = await tx.memberGroup.updateMany({ + where: { id: { in: ids }, archivedAt: { not: null } }, + data: { archivedAt: null }, + }); + + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupRestore, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { name: group.name, restoredCount: result.count }, + }); + return { restoredCount: result.count }; + }); +} + +/** + * 组扁平列表(前端自行按 parentId/depth 拼树);仅网站管理员。 + * includeArchived=true 时连已归档组一并返回(带 archivedAt 标记),供后台展示/恢复; + * 默认只返回活跃组 —— 权限相关的调用方一律走默认。 + */ +export async function listMemberGroups( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + includeArchived = false, +): Promise { + requireAdmin(actor); + const groups = await deps.prisma.memberGroup.findMany({ + where: includeArchived ? {} : { archivedAt: null }, + orderBy: { name: "asc" }, + select: { id: true, parentId: true, name: true, description: true, archivedAt: true }, + }); + if (groups.length === 0) return []; + const ids = groups.map((g) => g.id); + + // depth:每个组的闭包行数(自身 + 祖先)- 1。级联软删保证活跃组的祖先必活跃。 + const closure = await deps.prisma.memberGroupClosure.findMany({ + where: { descendantId: { in: ids } }, + select: { descendantId: true }, + }); + const closureCount = new Map(); + for (const row of closure) { + closureCount.set(row.descendantId, (closureCount.get(row.descendantId) ?? 0) + 1); + } + + const counts = await deps.prisma.memberGroupMembership.groupBy({ + by: ["groupId"], + where: { groupId: { in: ids }, revokedAt: null }, + _count: { _all: true }, + }); + const countByGroup = new Map(counts.map((c) => [c.groupId, c._count._all])); + + return groups.map((g) => ({ + id: g.id, + parentId: g.parentId, + name: g.name, + description: g.description, + depth: (closureCount.get(g.id) ?? 1) - 1, + memberCount: countByGroup.get(g.id) ?? 0, + archivedAt: g.archivedAt, + })); +} + +/** + * 组成员列表(仅网站管理员)。**已归档组也可读**(决策7):软删是打标,成员行仍在, + * 后台需要看得见「这个组曾经有谁」。写操作(add/remove)仍要求活跃组 —— 可读不可改。 + */ +export async function listMembers( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, +): Promise { + requireAdmin(actor); + const exists = await deps.prisma.memberGroup.findUnique({ + where: { id: groupId }, + select: { id: true }, + }); + if (exists === null) throw new FileLibError(404, "group_not_found", "group not found"); + const rows = await deps.prisma.memberGroupMembership.findMany({ + where: { groupId, revokedAt: null }, + select: { + createdAt: true, + user: { select: { id: true, displayName: true, feishuOpenId: true, avatarUrl: true } }, + }, + orderBy: { createdAt: "asc" }, + }); + return rows.map((r) => ({ + userId: r.user.id, + displayName: r.user.displayName, + feishuOpenId: r.user.feishuOpenId, + avatarUrl: r.user.avatarUrl, + joinedAt: r.createdAt, + })); +} + +/** 加成员(userId 或 feishuOpenId 解析);已是活跃成员 → 409。仅网站管理员。 */ +export async function addMember( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, + input: AddMemberInput, +): Promise { + requireAdmin(actor); + return deps.prisma.$transaction(async (tx) => { + const group = await requireActiveGroup(tx, groupId); + const user = await resolveUser(tx, input); + const existing = await tx.memberGroupMembership.findFirst({ + where: { groupId: group.id, userId: user.id, revokedAt: null }, + select: { id: true }, + }); + if (existing !== null) { + throw new FileLibError(409, "already_member", "user is already a member of this group"); + } + const created = await tx.memberGroupMembership.create({ + data: { groupId: group.id, userId: user.id }, + select: { createdAt: true }, + }); + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupMemberAdd, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { userId: user.id }, + }); + return { + userId: user.id, + displayName: user.displayName, + feishuOpenId: user.feishuOpenId, + avatarUrl: user.avatarUrl, + joinedAt: created.createdAt, + }; + }); +} + +/** 移成员(软删 revokedAt);不在组 → 404。仅网站管理员。 */ +export async function removeMember( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + groupId: string, + userId: string, +): Promise { + requireAdmin(actor); + await deps.prisma.$transaction(async (tx) => { + const group = await requireActiveGroup(tx, groupId); + const membership = await tx.memberGroupMembership.findFirst({ + where: { groupId: group.id, userId, revokedAt: null }, + select: { id: true }, + }); + if (membership === null) throw new FileLibError(404, "member_not_found", "group member not found"); + await tx.memberGroupMembership.update({ + where: { id: membership.id }, + data: { revokedAt: new Date() }, + }); + await writeFileLibAudit(tx, { + action: FILE_LIB_AUDIT_ACTIONS.groupMemberRemove, + actorUserId: actor.userId, + organizationId: deps.organizationId, + objectType: "group", + objectId: group.id, + objectPath: group.id, + detail: { userId }, + }); + }); +} + +/** + * 成员选择器:按显示名/openId 搜全局用户。**仅网站管理员**(与加成员同权,决策2) + * —— 加成员本就能指定任意全局用户(resolveUser 不要求 org 成员),故此端点不扩大 + * 已有能力面,只是把"盲敲 id"变成"搜索选择"。 + * excludeGroupId 给定时,过滤掉该组的活跃成员(避免选中必然 409 的人)。 + */ +export async function searchUsers( + deps: MemberGroupServiceDeps, + actor: FileLibActor, + q: string, + excludeGroupId?: string, + limit = 20, +): Promise { + requireAdmin(actor); + const keyword = q.trim(); + + let excludeIds: string[] = []; + if (excludeGroupId !== undefined && excludeGroupId !== "") { + const rows = await deps.prisma.memberGroupMembership.findMany({ + where: { groupId: excludeGroupId, revokedAt: null }, + select: { userId: true }, + }); + excludeIds = rows.map((r) => r.userId); + } + + const users = await deps.prisma.user.findMany({ + where: { + ...(excludeIds.length > 0 ? { id: { notIn: excludeIds } } : {}), + ...(keyword === "" + ? {} + : { + OR: [ + { displayName: { contains: keyword, mode: "insensitive" as const } }, + { feishuOpenId: { contains: keyword, mode: "insensitive" as const } }, + ], + }), + }, + take: limit, + orderBy: { displayName: "asc" }, + select: { id: true, displayName: true, feishuOpenId: true, avatarUrl: true }, + }); + + return users.map((u) => ({ + userId: u.id, + displayName: u.displayName, + feishuOpenId: u.feishuOpenId, + avatarUrl: u.avatarUrl, + })); +} + +/** + * 授权选择器搜索(契约 C2 /groups/search)。**不限管理员**(决策2)。 + * 活跃组按名过滤,breadcrumb 由活跃祖先链按 depth 排序拼成。 + */ +export async function searchMemberGroups( + deps: MemberGroupServiceDeps, + q: string, + limit = 20, +): Promise { + const keyword = q.trim(); + const groups = await deps.prisma.memberGroup.findMany({ + where: { + archivedAt: null, + ...(keyword === "" ? {} : { name: { contains: keyword, mode: "insensitive" as const } }), + }, + take: limit, + orderBy: { name: "asc" }, + select: { id: true, name: true }, + }); + if (groups.length === 0) return []; + const ids = groups.map((g) => g.id); + + // 祖先链(仅活跃祖先);depth 越大越靠根。 + const closure = await deps.prisma.memberGroupClosure.findMany({ + where: { descendantId: { in: ids }, ancestor: { archivedAt: null } }, + select: { descendantId: true, ancestorId: true, depth: true }, + }); + const ancestorIds = [...new Set(closure.map((c) => c.ancestorId))]; + const names = await deps.prisma.memberGroup.findMany({ + where: { id: { in: ancestorIds } }, + select: { id: true, name: true }, + }); + const nameById = new Map(names.map((n) => [n.id, n.name])); + const chainByGroup = new Map(); + for (const row of closure) { + const arr = chainByGroup.get(row.descendantId) ?? []; + arr.push({ ancestorId: row.ancestorId, depth: row.depth }); + chainByGroup.set(row.descendantId, arr); + } + + return groups.map((g) => { + const chain = (chainByGroup.get(g.id) ?? []).slice().sort((a, b) => b.depth - a.depth); + const breadcrumb = chain + .map((c) => nameById.get(c.ancestorId) ?? "") + .filter((s) => s !== "") + .join(" / "); + return { id: g.id, name: g.name, breadcrumb: breadcrumb || g.name }; + }); +} diff --git a/hub/src/database/routes/adminPanels.ts b/hub/src/database/routes/adminPanels.ts index 6a54981..6196d96 100644 --- a/hub/src/database/routes/adminPanels.ts +++ b/hub/src/database/routes/adminPanels.ts @@ -1,9 +1,11 @@ /** - * 管理员后台「用户管理」「Group 管理」面板(复用 hub 已有 org 管理 API)。 + * 管理员后台「用户管理」「Group 管理」面板。 * - * 用户管理 = org 成员(/api/org/:orgSlug/members); - * Group 管理 = Team(当前 Group 的过渡实现,/api/org/:orgSlug/teams), - * 真 Group 系统落地后此面板改接新 API 即可,文件库授权侧不动。 + * 用户管理 = org 成员(/api/org/:orgSlug/members)。 + * Group 管理 = **MemberGroup**(全局可无限嵌套,ADR-0028),走 /database/api/groups/*。 + * 已从旧的扁平 hub Team 迁过来 —— Team 无父子字段,建不出子组。此面板与 + * filelib-web 的 GroupAdmin.svelte 消费同一套 API,语义一致。 + * 交互:嵌套树(展开/折叠)+ 右键菜单(建子组/建根组/重命名/删除)。 */ function apiBase(orgSlug: string): string { @@ -105,35 +107,110 @@ export function renderUsersPanel(orgSlug: string): string { /* ---------------------------------------------------------------- Group 管理 */ -export function renderGroupsPanel(orgSlug: string): string { +/** + * MemberGroup 嵌套树管理(ADR-0028)。无 orgSlug 参数 —— MemberGroup 是全局主体, + * 不归属任何 Organization,端点也不带 org 段。 + */ +/** 内联 SVG 图标表(24x24 stroke 风格,与 dashboard 侧栏一致)。 */ +const GROUP_ICONS: Record = { + // Group 节点 = 人的集合。**不用文件夹图标** —— Group 不是目录, + // 与文件库的 FOLDER/PROJECT 是两套体系,图标上也不应混淆。两人剪影。 + group: "M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19M10 11.5a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM20 19v-1.5a3.5 3.5 0 0 0-2.6-3.38M15.4 5.22a3.25 3.25 0 0 1 0 6.06", + users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm14 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75", + user: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z", + plus: "M12 5v14M5 12h14", + pencil: "M17 3a2.8 2.8 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3Z", + trash: "M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14", + search: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z", + chevron: "m9 18 6-6-6-6", + layers: "m12 2 9 5-9 5-9-5 9-5Zm9 11-9 5-9-5m18 5-9 5-9-5", + clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Zm0-14v6l4 2", + minus: "M5 12h14", + // 已归档(软删)标记用;与"删除"区分 —— 数据仍在,只是打了 archivedAt。 + archive: "M3 8h18v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Zm1-5h16l1 5H3l1-5Zm5 9h6", + restore: "M3 12a9 9 0 1 0 3-6.7M3 4v4.5h4.5", +}; + +/** `icon("users", 16)` → 内联 svg 串。 */ +function icon(name: keyof typeof GROUP_ICONS | string, size = 16): string { + const d = GROUP_ICONS[name] ?? ""; + return ``; +} + +export function renderGroupsPanel(): string { return ` -
-
-
-
新建 Group
-
-
-
-
+
+ +
+
+ ${icon("layers", 17)} +
Group 树
+
-
-
Group 列表
-
+
+ ${icon("search", 13)} +
+ +
+
-
-
从左侧选择一个 Group 查看成员
+ + +
+
+
+ ${icon("users", 40)} + 从左侧选择一个 Group 查看成员 +
+
+ `; } diff --git a/hub/src/database/routes/databaseRoutes.ts b/hub/src/database/routes/databaseRoutes.ts index c5b3f46..597fef7 100644 --- a/hub/src/database/routes/databaseRoutes.ts +++ b/hub/src/database/routes/databaseRoutes.ts @@ -26,11 +26,12 @@ import path from "node:path"; import { SESSION_COOKIE_NAME, signSession, verifySession } from "../../admin/auth/session.js"; import { registerFileLibRoutes } from "./filelibRoutes.js"; import { registerFileRoutes } from "./fileRoutes.js"; +import { registerMemberGroupRoutes } from "./memberGroupRoutes.js"; import { registerTeacherApp } from "./teacherApp.js"; import { renderLibraryBrowser } from "./libraryBrowser.js"; import { renderGroupsPanel, renderUsersPanel } from "./adminPanels.js"; import { createInMemoryVersionStore } from "../filelib/versionStore.js"; -import { createTeamGroupResolver } from "../filelib/groupResolver.js"; +import { createMemberGroupResolver } from "../filelib/memberGroupResolver.js"; import { createHttpGroupResolver } from "../filelib/groupResolverHttp.js"; import { createManifestStubAdapter } from "../filelib/exportService.js"; import { FILE_LIB_AUDIT_ACTIONS } from "../filelib/audit.js"; @@ -122,7 +123,8 @@ export async function registerDatabaseRoutes( // 文件库(独立模块,《文件库-接口契约.md》):API + 浏览页 + 老师端 /app。 // 依赖装配:VersionStore 当前为内存+快照实现(版本团队 npm 包到位后替换); - // GroupResolver 默认读 hub Team,HUB_GROUP_SERVICE_URL 配置后切 HTTP(C2); + // GroupResolver 默认读 in-hub MemberGroup 闭包(ADR-0028), + // HUB_GROUP_SERVICE_URL 配置后切 HTTP(C2); // 导出适配器当前为 manifest stub(OPEN-6,真导出工具到位后替换)。 const siloOrg = await config.prisma.organization.findUnique({ where: { slug: config.siloOrganizationSlug }, @@ -141,13 +143,14 @@ export async function registerDatabaseRoutes( organizationId: siloOrg.id, storageRoot, groupResolver: groupServiceUrl === undefined || groupServiceUrl.trim() === "" - ? createTeamGroupResolver(config.prisma, siloOrg.id) + ? createMemberGroupResolver(config.prisma) : createHttpGroupResolver({ baseUrl: groupServiceUrl }), versionStore, exportAdapters: [createManifestStubAdapter(versionStore)], }; await registerFileLibRoutes(app, filelibDeps); await registerFileRoutes(app, filelibDeps); + await registerMemberGroupRoutes(app, filelibDeps); await registerTeacherApp(app, { prisma: config.prisma, sessionSecret: config.sessionSecret, @@ -339,7 +342,6 @@ ${pageHead("Database Admin")}
${initial}
-

已登录

${escapeHtml(displayName)}

@@ -368,9 +370,8 @@ ${pageHead("Database Admin")} ${renderUsersPanel(orgSlug)} -
{#if showCreateRoot} (showCreateRoot = false)}> -
- - +
+ +
-
- -
-
- - +
+ +
- - + +
{/if} diff --git a/hub/filelib-web/src/lib/LoginView.svelte b/hub/filelib-web/src/lib/LoginView.svelte index a5cf7f2..8b950f2 100644 --- a/hub/filelib-web/src/lib/LoginView.svelte +++ b/hub/filelib-web/src/lib/LoginView.svelte @@ -1,18 +1,13 @@ + +
+ {@render children()} +
+ + diff --git a/hub/filelib-web/src/routes/+layout.ts b/hub/filelib-web/src/routes/+layout.ts new file mode 100644 index 0000000..adc15e4 --- /dev/null +++ b/hub/filelib-web/src/routes/+layout.ts @@ -0,0 +1,7 @@ +/** + * 纯 SPA:关掉 SSR 与预渲染,构建产物只有一个 fallback index.html + * (adapter-static + fallback,见 svelte.config.js),由 hub 后端在 + * /app 与 /database/* 两个前缀下原样送出。 + */ +export const ssr = false; +export const prerender = false; diff --git a/hub/filelib-web/src/routes/+page.svelte b/hub/filelib-web/src/routes/+page.svelte new file mode 100644 index 0000000..389dc3a --- /dev/null +++ b/hub/filelib-web/src/routes/+page.svelte @@ -0,0 +1,11 @@ + + +
跳转中…
diff --git a/hub/filelib-web/src/routes/app/+page.svelte b/hub/filelib-web/src/routes/app/+page.svelte new file mode 100644 index 0000000..26fd7c1 --- /dev/null +++ b/hub/filelib-web/src/routes/app/+page.svelte @@ -0,0 +1,22 @@ + + +文件库 + +{#if !$authChecked} +
加载中…
+{:else if $me} +
+ +
+{:else} + +{/if} diff --git a/hub/filelib-web/src/routes/database/+page.svelte b/hub/filelib-web/src/routes/database/+page.svelte new file mode 100644 index 0000000..eaa5117 --- /dev/null +++ b/hub/filelib-web/src/routes/database/+page.svelte @@ -0,0 +1,11 @@ + + +
跳转中…
diff --git a/hub/filelib-web/src/routes/database/admin/+page.svelte b/hub/filelib-web/src/routes/database/admin/+page.svelte new file mode 100644 index 0000000..d7b2c8e --- /dev/null +++ b/hub/filelib-web/src/routes/database/admin/+page.svelte @@ -0,0 +1,63 @@ + + +Database Admin · 登录 + +
+
+
Database Admin
+

使用飞书登录以管理数据库

+ + {#if !$authChecked} +

加载中…

+ {:else if info} + 使用飞书登录 + + {#if info.devLoginEnabled} +
+ 开发模式 + +
+ ⚡ 一键登录管理员 +

仅开发环境可见 · 跳过飞书 OAuth

+ {/if} + {:else if loadFailed} +

无法加载登录配置,请稍后重试

+ {:else} +

加载中…

+ {/if} +
+
diff --git a/hub/filelib-web/src/routes/database/dashboard/+layout.svelte b/hub/filelib-web/src/routes/database/dashboard/+layout.svelte new file mode 100644 index 0000000..655f099 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/+layout.svelte @@ -0,0 +1,99 @@ + + +Database Admin + +{#if !$authChecked} +
加载中…
+{:else if $me === null} +
跳转到登录页…
+{:else if !$me.isWebsiteAdmin} +
+
+

无权访问管理后台

+

+ 当前账号不是本组织的所有者或管理员。普通老师请到文件库使用。 +

+ 前往文件库 + +
+
+{:else} +
+ + +
+ {@render children()} +
+
+{/if} diff --git a/hub/filelib-web/src/routes/database/dashboard/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/+page.svelte new file mode 100644 index 0000000..2654946 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/+page.svelte @@ -0,0 +1,67 @@ + + +
+

概览

+

文件库实时数据

+ + {#if error} +
{error}
+ {:else} +
+ {#each cards as card (card.label)} +
+

{card.label}

+

{card.value ?? "—"}

+
+ {/each} +
+ +
+

最近活动

+ {#if stats === null} +
加载中…
+ {:else if stats.recent.length === 0} +
暂无文件库活动 · 到「文件库」里创建第一个文件夹吧
+ {:else} + {#each stats.recent as row (row.action + row.when + row.label)} +
+ {row.action} + {row.label} + {row.actor} · {fmtWhen(row.when)} +
+ {/each} + {/if} +
+ {/if} +
diff --git a/hub/filelib-web/src/routes/database/dashboard/groups/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/groups/+page.svelte new file mode 100644 index 0000000..5f6de69 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/groups/+page.svelte @@ -0,0 +1,10 @@ + + +
+ +
diff --git a/hub/filelib-web/src/routes/database/dashboard/library/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/library/+page.svelte new file mode 100644 index 0000000..e3c6512 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/library/+page.svelte @@ -0,0 +1,8 @@ + + +
+ +
diff --git a/hub/filelib-web/src/routes/database/dashboard/search/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/search/+page.svelte new file mode 100644 index 0000000..6154d90 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/search/+page.svelte @@ -0,0 +1,4 @@ +
+

查询

+

查询功能建设中

+
diff --git a/hub/filelib-web/src/routes/database/dashboard/settings/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/settings/+page.svelte new file mode 100644 index 0000000..67201bb --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/settings/+page.svelte @@ -0,0 +1,4 @@ +
+

设置

+

设置功能建设中

+
diff --git a/hub/filelib-web/src/routes/database/dashboard/users/+page.svelte b/hub/filelib-web/src/routes/database/dashboard/users/+page.svelte new file mode 100644 index 0000000..fbaede7 --- /dev/null +++ b/hub/filelib-web/src/routes/database/dashboard/users/+page.svelte @@ -0,0 +1,168 @@ + + +
+

用户管理

+ +
+
+
添加成员
+
+ + + + +
+
+ +
+
成员列表
+ + {#if error} +
{error}
+ {:else if members === null} +
加载中…
+ {:else if members.length === 0} +
暂无成员
+ {:else} + + + + + + + + + + + {#each members as m (m.userId)} + + + + + + + {/each} + +
成员userId角色
{m.displayName || m.userId}{m.userId} + + + +
+ {/if} +
+
+
From d159e372d2550006427eff7500512b1d980fee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:17:59 +0800 Subject: [PATCH 08/14] =?UTF-8?q?refactor(database)!:=20=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=B8=B2=E6=9F=93=E4=BB=BB=E4=BD=95=20HTML,?= =?UTF-8?q?=E5=8F=AA=E5=87=BA=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删掉约 1770 行服务端模板拼接:renderDashboard / renderLoginPage (databaseRoutes)、adminPanels、libraryBrowser、uiTheme,以及 libraryPage —— 后者迁移前已是无人引用的死代码。 新增两个端点承接原先在 page handler 里 inline 算的东西: GET /database/config 免鉴权 bootstrap(org slug + dev 开关); 注册位置刻意早于 silo org 的提前返回, org 未就绪时登录页仍要能渲染。 GET /database/api/stats 概览统计,要求 silo org OWNER/ADMIN —— 它聚合的是 org 级计数与审计流,不是 单节点权限视图。 静态托管收敛到 static.ts:一份 filelib-web 构建产物挂 /app 与 /database 两个前缀,资源路由只注册一次。并发症是路由顺序成了硬约束 —— 具体页面路由必须先于 SPA 通配注册,否则重演 /database/dashboard 盖住 SPA 的老 bug(ADR-0029)。 /database/library 改为 302 到 /database/dashboard/library。 BREAKING: 部署需先构建 filelib-web,否则 static.ts 的 existsSync 守卫会让 /app 与 /database 全部 404。 --- hub/src/database/README.md | 43 +- hub/src/database/routes/adminPanels.ts | 747 -------------------- hub/src/database/routes/databaseRoutes.ts | 250 +------ hub/src/database/routes/libraryBrowser.ts | 597 ---------------- hub/src/database/routes/libraryPage.ts | 801 ---------------------- hub/src/database/routes/teacherApp.ts | 33 +- hub/src/database/routes/uiTheme.ts | 155 ----- hub/src/database/static.ts | 113 ++- 8 files changed, 109 insertions(+), 2630 deletions(-) delete mode 100644 hub/src/database/routes/adminPanels.ts delete mode 100644 hub/src/database/routes/libraryBrowser.ts delete mode 100644 hub/src/database/routes/libraryPage.ts delete mode 100644 hub/src/database/routes/uiTheme.ts diff --git a/hub/src/database/README.md b/hub/src/database/README.md index 8b927d4..9d18952 100644 --- a/hub/src/database/README.md +++ b/hub/src/database/README.md @@ -3,26 +3,35 @@ `/database/*` HTTP 面。代码写在这个目录里,`hub.ts` 通过 `plugin.ts` 挂载它, 所以服务器启动时能正确识别这些路由。 -**前后端分离**:页面已迁到独立的 SvelteKit 静态 SPA `hub/database-admin/` -(与 `hub/admin-web/` 同一套框架)。本目录的后端只保留三件事:鉴权透传、一个 -免鉴权配置端点、以及把 SPA 构建产物托管出去。页面全部由 SPA 客户端渲染。 +**前后端分离**:页面全部在 SvelteKit 静态 SPA `hub/filelib-web/`(与 `hub/admin-web/` +同一套框架)。**老师端 `/app` 与管理后台 `/database` 共用这一份工程和这一份构建产物** —— +两个挂载前缀,一个 SPA。本目录的后端只保留三件事:鉴权透传、JSON 数据端点、 +以及把构建产物托管出去。服务端不渲染任何 HTML。 后端路由: -- `GET /database/config` —— 免鉴权。返回 `{ siloOrganizationSlug, devLoginEnabled }`, +- `GET /database/config` —— 免鉴权。返回 `{ orgSlug, devLoginEnabled }`, 给 SPA 登录页拼飞书链接、决定是否显示 dev 按钮用。不含任何敏感数据。 + (`/database/api/login-info` 是同形状的既有端点,由 `routes/teacherApp.ts` 注册。) +- `GET /database/api/stats` —— 概览页统计。需登录 **且** 是 silo org OWNER/ADMIN。 - `GET /database/dev-login` —— 仅开发。见下。 -- `GET /database`、`GET /database/*` —— SPA shell / 客户端路由 fallback - (`static.ts` 的 `registerDatabaseSpa`);资产在 `/database/_app/*`。 +- `GET /database`、`GET /database/*`、`GET /app`、`GET /app/*` —— SPA shell / + 客户端路由 fallback(`static.ts` 的 `registerDatabaseSpa`)。 +- `GET /_filelib/*` —— 构建产物资源。SvelteKit 的 `appDir` 改名为 `_filelib`, + 以避开 `admin-web` 在根上注册的 `/_app/*`(同名会让 Fastify 启动即抛重复路由)。 -SPA 页面(`database-admin`,`paths.base='/database'`): +SPA 页面(`filelib-web`,真 URL 路由、无 hash): -- `/database/admin` —— 飞书登录页。按钮指向 `/auth/feishu/`(slug 来自 - `/database/config`),回调由 `src/admin/routes/authRoutes.ts` 处理并种 session cookie。 -- `/database/dashboard` —— 后台壳。未登录跳登录页;**登录但非 OWNER/ADMIN 显示无权提示**。 +- `/app` —— 老师端文件库。未登录显示登录卡片。 +- `/database/admin` —— 管理员飞书登录页。按钮指向 `/auth/feishu/`, + 回调由 `src/admin/routes/authRoutes.ts` 处理并种 session cookie。 +- `/database/dashboard` —— 后台外壳(侧栏 + 权限门)。未登录跳登录页; + **登录但非 OWNER/ADMIN 显示无权提示**。六个 tab 都是子路由: + `/database/dashboard`(概览)、`/library`、`/users`、`/groups`、`/search`、`/settings`。 -> **注册顺序要点**:concrete 路由(`/database/config`、`/database/dev-login`)必须在 -> `registerDatabaseSpa` 的 `/database/*` fallback 之前注册(已在 `plugin.ts` 保证), +> **注册顺序要点**:concrete 路由(`/database/config`、`/database/api/*`、 +> `/database/dev-login`、`/app/dev-login-teacher`)必须在 `registerDatabaseSpa` 的 +> `/database/*`、`/app/*` fallback 之前注册(已在 `plugin.ts` 保证), > 否则通配会 shadow 它们。 ## 开发模式:用环境变量开启一键登录 @@ -73,17 +82,17 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 | 文件 | 职责 | |------|------| | `plugin.ts` | 模块对外入口,`hub.ts` 调 `registerDatabasePlugin()` | -| `routes/databaseRoutes.ts` | 登录页/dashboard + 各子路由装配点 | +| `routes/databaseRoutes.ts` | `/database/config`、`/database/api/stats`、dev 旁路 + 各子路由装配点 | | `routes/filelibRoutes.ts` | 文件库 树/授权 API | | `routes/fileRoutes.ts` | 文件库 文件内容/导出 API | | `routes/memberGroupRoutes.ts` | 成员组管理 API + `/groups/search` + `/users/search`(ADR-0028) | -| `routes/adminPanels.ts` | dashboard「用户管理」(org 成员)/「Group 管理」(MemberGroup 嵌套树)面板 | -| `routes/libraryPage.ts` | `/database/library` 文件库浏览页 | +| `routes/teacherApp.ts` | `/database/api/login-info` + 老师端 DEV 一键登录 | +| `static.ts` | filelib-web 构建产物托管:`/_filelib/*` 资源 + `/app`、`/database` 两个 SPA 回退 | | `filelib/` | 文件库领域层(见下) | -新增一类**数据**端点时:要么直接往 `databaseRoutes.ts` 加 `app.get("/database/...")`, +新增一类**数据**端点时:要么直接往 `databaseRoutes.ts` 加 `app.get("/database/api/...")`, 要么新建 `routes/xxxRoutes.ts` 并在 `databaseRoutes.ts` 里 `registerXxxRoutes(app, {...})` -注册一次。 +注册一次。**不要在后端拼 HTML** —— 页面一律加在 `hub/filelib-web/src/routes/` 下。 ## 文件库(filelib/) diff --git a/hub/src/database/routes/adminPanels.ts b/hub/src/database/routes/adminPanels.ts deleted file mode 100644 index 6196d96..0000000 --- a/hub/src/database/routes/adminPanels.ts +++ /dev/null @@ -1,747 +0,0 @@ -/** - * 管理员后台「用户管理」「Group 管理」面板。 - * - * 用户管理 = org 成员(/api/org/:orgSlug/members)。 - * Group 管理 = **MemberGroup**(全局可无限嵌套,ADR-0028),走 /database/api/groups/*。 - * 已从旧的扁平 hub Team 迁过来 —— Team 无父子字段,建不出子组。此面板与 - * filelib-web 的 GroupAdmin.svelte 消费同一套 API,语义一致。 - * 交互:嵌套树(展开/折叠)+ 右键菜单(建子组/建根组/重命名/删除)。 - */ - -function apiBase(orgSlug: string): string { - return `/api/org/${encodeURIComponent(orgSlug)}`; -} - -/* ---------------------------------------------------------------- 用户管理 */ - -export function renderUsersPanel(orgSlug: string): string { - return ` -
-
-
添加成员
-
- - - - -
-
-
-
成员列表
- - - -
成员userId角色
-
-
-`; -} - -/* ---------------------------------------------------------------- Group 管理 */ - -/** - * MemberGroup 嵌套树管理(ADR-0028)。无 orgSlug 参数 —— MemberGroup 是全局主体, - * 不归属任何 Organization,端点也不带 org 段。 - */ -/** 内联 SVG 图标表(24x24 stroke 风格,与 dashboard 侧栏一致)。 */ -const GROUP_ICONS: Record = { - // Group 节点 = 人的集合。**不用文件夹图标** —— Group 不是目录, - // 与文件库的 FOLDER/PROJECT 是两套体系,图标上也不应混淆。两人剪影。 - group: "M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19M10 11.5a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM20 19v-1.5a3.5 3.5 0 0 0-2.6-3.38M15.4 5.22a3.25 3.25 0 0 1 0 6.06", - users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm14 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75", - user: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z", - plus: "M12 5v14M5 12h14", - pencil: "M17 3a2.8 2.8 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3Z", - trash: "M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14", - search: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z", - chevron: "m9 18 6-6-6-6", - layers: "m12 2 9 5-9 5-9-5 9-5Zm9 11-9 5-9-5m18 5-9 5-9-5", - clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Zm0-14v6l4 2", - minus: "M5 12h14", - // 已归档(软删)标记用;与"删除"区分 —— 数据仍在,只是打了 archivedAt。 - archive: "M3 8h18v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Zm1-5h16l1 5H3l1-5Zm5 9h6", - restore: "M3 12a9 9 0 1 0 3-6.7M3 4v4.5h4.5", -}; - -/** `icon("users", 16)` → 内联 svg 串。 */ -function icon(name: keyof typeof GROUP_ICONS | string, size = 16): string { - const d = GROUP_ICONS[name] ?? ""; - return ``; -} - -export function renderGroupsPanel(): string { - return ` -
- -
-
- ${icon("layers", 17)} -
Group 树
- -
-
- ${icon("search", 13)} - -
- -
-
-
- - -
-
-
- ${icon("users", 40)} - 从左侧选择一个 Group 查看成员 -
-
-
-
- -`; -} diff --git a/hub/src/database/routes/databaseRoutes.ts b/hub/src/database/routes/databaseRoutes.ts index 597fef7..9cfaf35 100644 --- a/hub/src/database/routes/databaseRoutes.ts +++ b/hub/src/database/routes/databaseRoutes.ts @@ -5,11 +5,12 @@ * Handlers use ABSOLUTE paths (no Fastify prefix) so every route greps as the * literal string it serves. * - * The login page and dashboard are now served by the `database-admin` SvelteKit - * SPA (see ../static.ts / registerDatabaseSpa). This file keeps only the - * concrete JSON/redirect routes the SPA depends on: + * 标准前后端分离:本文件**不渲染任何 HTML**。登录页与管理后台六个 tab 全部由 + * `hub/filelib-web` 这一个 SvelteKit SPA 提供(同一份产物挂 /app 与 /database, + * 见 ../static.ts / registerDatabaseSpa)。此处只留 SPA 依赖的 JSON/跳转端点: * * /database/config — unauthenticated bootstrap: silo org slug + dev toggle + * /database/api/stats — 概览页统计(需登录 + silo org OWNER/ADMIN) * /database/dev-login — DEV ONLY bypass, registered only when the flag is on * * The dev bypass (/database/dev-login) is self-contained here and gated by @@ -23,19 +24,17 @@ import type { FastifyInstance } from "fastify"; import type { PrismaClient } from "@prisma/client"; import path from "node:path"; -import { SESSION_COOKIE_NAME, signSession, verifySession } from "../../admin/auth/session.js"; +import { SESSION_COOKIE_NAME, signSession } from "../../admin/auth/session.js"; import { registerFileLibRoutes } from "./filelibRoutes.js"; import { registerFileRoutes } from "./fileRoutes.js"; import { registerMemberGroupRoutes } from "./memberGroupRoutes.js"; import { registerTeacherApp } from "./teacherApp.js"; -import { renderLibraryBrowser } from "./libraryBrowser.js"; -import { renderGroupsPanel, renderUsersPanel } from "./adminPanels.js"; import { createInMemoryVersionStore } from "../filelib/versionStore.js"; import { createMemberGroupResolver } from "../filelib/memberGroupResolver.js"; import { createHttpGroupResolver } from "../filelib/groupResolverHttp.js"; import { createManifestStubAdapter } from "../filelib/exportService.js"; import { FILE_LIB_AUDIT_ACTIONS } from "../filelib/audit.js"; -import { UI_HEAD_FONTS, UI_THEME_CSS } from "./uiTheme.js"; +import { actorOrNull, sendRouteError } from "../filelib/routeShared.js"; import type { FileLibRouteDeps } from "../filelib/routeShared.js"; export interface DatabaseRouteConfig { @@ -55,19 +54,30 @@ export async function registerDatabaseRoutes( // 文件库依赖在下方装配;概览页统计在请求时经此引用读取(请求一定晚于装配完成)。 let filelibDepsForStats: FileLibRouteDeps | null = null; - app.get("/database/admin", async (request, reply) => { - // Already signed in → straight to the dashboard. - if ((await resolveUser(request.cookies[SESSION_COOKIE_NAME], config)) !== null) { - return reply.redirect("/database/dashboard"); - } - return reply.type("text/html").send(renderLoginPage(config)); - }); + // 登录页/前端 bootstrap(公开;org slug 本就在 OAuth URL 中,不构成敏感信息)。 + // 与老师端 /database/api/login-info 同形状 —— 后者由 teacherApp.ts 注册, + // 两处并存是为了兼容既有前端调用点。 + app.get("/database/config", async () => ({ + orgSlug: config.siloOrganizationSlug, + devLoginEnabled: config.allowDevLoginBypass, + })); - app.get("/database/dashboard", async (request, reply) => { - const user = await resolveUser(request.cookies[SESSION_COOKIE_NAME], config); - if (user === null) return reply.redirect("/database/admin"); - const stats = await loadDashboardStats(config.prisma, filelibDepsForStats); - return reply.type("text/html").send(renderDashboard(user.displayName, stats, config.siloOrganizationSlug)); + // 概览页统计。登录 + silo org OWNER/ADMIN 才给 —— 它聚合的是全 org 口径的 + // 计数与审计流,不是某个节点的授权视图,所以不走 per-node 的 role 判定。 + app.get("/database/api/stats", async (request, reply) => { + if (filelibDepsForStats === null) { + return reply.status(503).send({ error: { code: "unavailable", message: "filelib not ready" } }); + } + const actor = await actorOrNull(request, reply, filelibDepsForStats); + if (actor === null) return reply; + if (!actor.isWebsiteAdmin) { + return reply.status(403).send({ error: { code: "forbidden", message: "requires organization OWNER/ADMIN" } }); + } + try { + return await loadDashboardStats(config.prisma, filelibDepsForStats); + } catch (error) { + return sendRouteError(reply, error); + } }); // DEV ONLY bypass — self-contained here, registered only when the flag is on @@ -121,7 +131,7 @@ export async function registerDatabaseRoutes( }); } - // 文件库(独立模块,《文件库-接口契约.md》):API + 浏览页 + 老师端 /app。 + // 文件库(独立模块,《文件库-接口契约.md》):API + 老师端 /app 静态托管。 // 依赖装配:VersionStore 当前为内存+快照实现(版本团队 npm 包到位后替换); // GroupResolver 默认读 in-hub MemberGroup 闭包(ADR-0028), // HUB_GROUP_SERVICE_URL 配置后切 HTTP(C2); @@ -158,9 +168,10 @@ export async function registerDatabaseRoutes( allowDevLoginBypass: config.allowDevLoginBypass, }); - // 独立文件库页已并入后台「文件库」tab(/database/dashboard#library),旧地址跳转保留兼容。 + // 独立文件库页已并入后台「文件库」tab;旧地址跳转保留兼容。 + // SPA 化后目标是真路由(不再是 #library 锚点)。 app.get("/database/library", async (_request, reply) => - reply.redirect("/database/dashboard#library"), + reply.redirect("/database/dashboard/library"), ); filelibDepsForStats = filelibDeps; @@ -184,9 +195,8 @@ interface DashboardStats { /** 概览页统计:org 范围内的文件夹/项目/授权(DB)+ 文件(版本库)+ 最近活动(AuditEntry)。 */ async function loadDashboardStats( prisma: PrismaClient, - deps: FileLibRouteDeps | null, -): Promise { - if (deps === null) return null; + deps: FileLibRouteDeps, +): Promise { const organizationId = deps.organizationId; const [folders, projects, grants] = await Promise.all([ prisma.fileLibNode.count({ where: { organizationId, kind: "FOLDER", deletedAt: null } }), @@ -234,193 +244,3 @@ async function loadDashboardStats( }); return { folders, projects, files, grants, recent }; } - -/** Verify the session cookie and load the user, or null if not signed in. */ -async function resolveUser( - rawCookie: string | undefined, - config: DatabaseRouteConfig, -): Promise<{ displayName: string } | null> { - if (rawCookie === undefined || rawCookie === "") return null; - const session = verifySession(rawCookie, config.sessionSecret); - if (session === null) return null; - const user = await config.prisma.user.findUnique({ - where: { id: session.userId }, - select: { displayName: true }, - }); - return user; -} - -/** 管理后台共享 head(全局 UI 主题,与老师端 /app 同源)。 */ -function pageHead(title: string): string { - return ` - - - ${title} - ${UI_HEAD_FONTS} - -`; -} - -function renderLoginPage(config: DatabaseRouteConfig): string { - const feishuHref = `/auth/feishu/${encodeURIComponent(config.siloOrganizationSlug)}`; - const devButton = config.allowDevLoginBypass - ? `
- 开发模式 - -
- ⚡ 一键登录管理员 -

仅开发环境可见 · 跳过飞书 OAuth

` - : ""; - - return ` - -${pageHead("Database Admin · 登录")} - -
-
-
Database Admin
-

使用飞书登录以管理数据库

- 使用飞书登录 - ${devButton} -
-
- -`; -} - -/** Sidebar nav items. `active` marks the current page. `href` "#" = placeholder. */ -const NAV_TABS: ReadonlyArray<{ id: string; label: string; icon: string }> = [ - { id: "overview", label: "概览", icon: "M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z" }, - { id: "library", label: "文件库", icon: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" }, - { id: "users", label: "用户管理", icon: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" }, - { id: "groups", label: "Group 管理", icon: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm14 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75M23 21v-2a4 4 0 0 0-3-3.87" }, - { id: "search", label: "查询", icon: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z" }, - { id: "settings", label: "设置", icon: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-3 2 1-2 3-2-1a7 7 0 0 1-2 1l-1 2h-4l-1-2a7 7 0 0 1-2-1l-2 1-2-3 2-1a7 7 0 0 1 0-2l-2-1 2-3 2 1a7 7 0 0 1 2 1l1-2h4l1 2a7 7 0 0 1 0 2l2-1 2 3-2 1a7 7 0 0 1 0 2Z" }, -]; - -function renderDashboard(displayName: string, stats: DashboardStats | null, orgSlug: string): string { - const nav = NAV_TABS.map((t) => ` - `).join("\n"); - - const cards = [ - { label: "文件夹", value: stats?.folders ?? "—" }, - { label: "项目", value: stats?.projects ?? "—" }, - { label: "文件", value: stats?.files ?? "—" }, - { label: "活跃授权", value: stats?.grants ?? "—" }, - ].map((s) => ` -
-

${s.label}

-

${s.value}

-
`).join(""); - - const recentRows = stats === null || stats.recent.length === 0 - ? `
暂无文件库活动 · 到「文件库」里创建第一个文件夹吧
` - : stats.recent.map((r) => ` -
- ${escapeHtml(r.action)} - ${escapeHtml(r.label)} - ${escapeHtml(r.actor)} · ${escapeHtml(r.when.toLocaleString("zh-CN"))} -
`).join(""); - - const initial = escapeHtml(displayName.slice(0, 1) || "U"); - - return ` - -${pageHead("Database Admin")} - -
- - -
-
-

概览

-

文件库实时数据

-
- ${cards} -
-
-

最近活动

- ${recentRows} -
-
- - - - - - - - - - -
-
- - - -`; -} - -function escapeHtml(value: string): string { - return value - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """); -} diff --git a/hub/src/database/routes/libraryBrowser.ts b/hub/src/database/routes/libraryBrowser.ts deleted file mode 100644 index 0499f48..0000000 --- a/hub/src/database/routes/libraryBrowser.ts +++ /dev/null @@ -1,597 +0,0 @@ -/** - * 管理员后台「文件库」页内浏览器区块(树 | 内容 | 预览三栏,含授权管理)。 - * - * renderLibraryBrowser() 返回可嵌入任意后台布局的 HTML+JS 片段: - * 以 #lib-root 为根、内部用 q() 作用域选择器,不与宿主页其他元素冲突。 - * 与独立页版浏览器的区别:去掉了自己的 wordmark/用户栏(由后台外壳提供), - * 保留管理员工具(根目录创建、授权面板、独立权限开关)。 - */ - -export function renderLibraryBrowser(): string { - return ` -
- -
-
- 文件库 - -
-
-
- - -
-
从左侧选择一个文件夹或项目
-
- - - - -
-
-
- - - -`; -} diff --git a/hub/src/database/routes/libraryPage.ts b/hub/src/database/routes/libraryPage.ts deleted file mode 100644 index 396f2d8..0000000 --- a/hub/src/database/routes/libraryPage.ts +++ /dev/null @@ -1,801 +0,0 @@ -/** - * /database/library —— 文件库浏览页(管理员后台的文件工具)。 - * - * 服务端渲染外壳 + 浏览器端 JS 调 /database/api/*(同源 session cookie)。 - * 设计令牌与全局 UI 主题(uiTheme.ts)一致。能力面全部由 API 的 404/403 表达(D8)。 - */ - -import type { FastifyInstance } from "fastify"; -import type { PrismaClient } from "@prisma/client"; -import { SESSION_COOKIE_NAME, verifySession } from "../../admin/auth/session.js"; - -export interface LibraryPageConfig { - readonly prisma: PrismaClient; - readonly sessionSecret: string; -} - -export async function registerLibraryPage( - app: FastifyInstance, - config: LibraryPageConfig, -): Promise { - app.get("/database/library", async (request, reply) => { - const raw = request.cookies[SESSION_COOKIE_NAME]; - const session = raw === undefined || raw === "" ? null : verifySession(raw, config.sessionSecret); - if (session === null) return reply.redirect("/database/admin"); - const user = await config.prisma.user.findUnique({ - where: { id: session.userId }, - select: { displayName: true }, - }); - if (user === null) return reply.redirect("/database/admin"); - return reply.type("text/html").send(renderLibraryPage(user.displayName)); - }); -} - -function renderLibraryPage(displayName: string): string { - const initial = displayName.slice(0, 1).replace(/[&<>"']/, "U"); - return ` - - - - - 文件库 - - - - - -
- -
-
从左侧选择一个文件夹或项目
-
-
- - -
- - - -`; -} diff --git a/hub/src/database/routes/teacherApp.ts b/hub/src/database/routes/teacherApp.ts index 665bf16..871f896 100644 --- a/hub/src/database/routes/teacherApp.ts +++ b/hub/src/database/routes/teacherApp.ts @@ -1,15 +1,12 @@ /** - * 老师端用户前端托管(标准前后端分离): - * GET /app/* — filelib-web(Svelte SPA)构建产物静态托管 + SPA 回退 - * GET /database/api/login-info — 登录页配置(org slug / dev 开关) - * GET /app/dev-login{,-teacher} — DEV ONLY 一键登录(管理员 / 普通老师) + * 老师端后端支撑(标准前后端分离): + * GET /database/api/login-info — 登录页配置(org slug / dev 开关) + * GET /app/dev-login-teacher — DEV ONLY 一键登录(普通老师) * - * 服务端不再渲染老师端页面;页面由独立前端工程 hub/filelib-web 产出。 + * 服务端不渲染任何页面。`/app/*` 的静态托管与 SPA 回退在 ../static.ts —— 那里 + * 与管理后台 `/database/*` 共用同一份 filelib-web 构建产物,资源路由只注册一次。 */ -import fs from "node:fs"; -import path from "node:path"; -import fastifyStatic from "@fastify/static"; import type { FastifyInstance } from "fastify"; import type { PrismaClient } from "@prisma/client"; import { SESSION_COOKIE_NAME, signSession } from "../../admin/auth/session.js"; @@ -33,26 +30,6 @@ export async function registerTeacherApp( devLoginEnabled: config.allowDevLoginBypass, })); - // 标准分离:静态托管 SPA 构建产物;非文件路径回退 index.html 交给前端。 - const distDir = path.resolve(import.meta.dirname, "../../../filelib-web/dist"); - if (fs.existsSync(path.join(distDir, "index.html"))) { - await app.register(fastifyStatic, { root: distDir, prefix: "/app/", decorateReply: true }); - app.setNotFoundHandler((request, reply) => { - if (request.url.startsWith("/app")) { - return reply.sendFile("index.html", distDir); - } - return reply.status(404).send({ error: { code: "not_found", message: "not found" } }); - }); - } else { - app.log.warn({ distDir }, "filelib-web dist not found; build it with `npm run build --prefix filelib-web`"); - app.get("/app", async (_request, reply) => - reply - .status(503) - .type("text/plain") - .send("filelib-web 未构建。请先运行 npm run build --prefix hub/filelib-web"), - ); - } - if (!config.allowDevLoginBypass) return; registerDevLogins(app, config); } diff --git a/hub/src/database/routes/uiTheme.ts b/hub/src/database/routes/uiTheme.ts deleted file mode 100644 index 03776b9..0000000 --- a/hub/src/database/routes/uiTheme.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * 全局共享 UI 主题(老师端 /app、管理员后台 /database、文件页 /database/library)。 - * - * 方向:高级简约、零视觉疲劳 —— 暖白底、发丝边框、近黑主按钮(唯一强调色)、 - * 无渐变、无彩色、无阴影(弹层仅一丝)、Inter 全界面、克制的动效。 - * 所有页面的设计令牌收敛于此,调色只改这里。 - */ - -export const UI_HEAD_FONTS = ` - - -`; - -export const UI_THEME_CSS = ` - :root { - --bg: #FCFCFB; - --panel: #FFFFFF; - --sidebar: #F7F7F5; - --text: #1A1A18; - --text-2: #6B6A66; - --text-3: #9C9B96; - --border: #ECECE8; - --border-soft: #F1F1EE; - --hover: #F4F4F1; - --selected: #EBEBE7; - --accent: #1A1A18; - --accent-hover: #333330; - --danger: #A13A33; - --guide: #E9E9E5; - --diff-add-bg: #F3F6F2; - --diff-add-text: #4A6741; - --diff-del-bg: #F8F2F1; - --diff-del-text: #A13A33; - --sans: 'Inter', -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; - --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - --shadow-pop: 0 4px 20px rgba(26,26,24,.07); - } - * { box-sizing: border-box; } - html, body { margin: 0; height: 100%; } - body { - font-family: var(--sans); - background: var(--bg); color: var(--text); - font-size: 14px; line-height: 1.65; - -webkit-font-smoothing: antialiased; - } - .hidden { display: none !important; } - h1, h2, h3 { margin: 0; } - a { color: var(--text); text-decoration: none; } - a:hover { text-decoration: underline; } - - /* 按钮:近黑实心(主)/ 发丝边幽灵(次)/ 文字型(危险) */ - .btn { - display: inline-flex; align-items: center; gap: 5px; - padding: 6px 14px; border-radius: 8px; - border: 1px solid var(--border); background: var(--panel); - color: var(--text); font-size: 12.5px; font-weight: 500; - cursor: pointer; transition: all 120ms ease; text-decoration: none; - } - .btn:hover { background: var(--hover); text-decoration: none; } - .btn-primary { - background: var(--accent); border-color: var(--accent); color: #fff; - } - .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } - .btn-danger { border-color: transparent; background: transparent; color: var(--danger); } - .btn-danger:hover { background: #A13A3312; } - - /* 面板与标签 */ - .panel { - background: var(--panel); border: 1px solid var(--border-soft); - border-radius: 10px; padding: 20px 22px; - } - .tag { - font-size: 10.5px; font-weight: 500; font-family: var(--mono); - padding: 2px 8px; border-radius: 999px; - border: 1px solid var(--border-soft); color: var(--text-3); background: var(--panel); - } - .tag-role { color: var(--text-2); border-color: var(--border); } - - /* 页签 */ - .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border-soft); margin-bottom: 18px; } - .tab { - padding: 8px 14px; font-size: 13px; color: var(--text-3); - border: none; background: none; cursor: pointer; - border-bottom: 2px solid transparent; margin-bottom: -1px; - transition: color 120ms ease; - } - .tab:hover { color: var(--text); } - .tab.active { color: var(--text); font-weight: 600; border-bottom-color: var(--accent); } - - /* 表单 */ - .input, .select, .textarea { - width: 100%; padding: 7px 11px; border-radius: 8px; - border: 1px solid var(--border); background: var(--panel); - font-size: 13px; color: var(--text); font-family: inherit; - outline: none; transition: border-color 120ms ease; - } - .input:focus, .select:focus, .textarea:focus { border-color: var(--accent); } - .textarea { font-family: var(--mono); font-size: 12.5px; line-height: 1.75; resize: vertical; } - .form-label { display: block; font-size: 11.5px; color: var(--text-3); margin-bottom: 4px; } - .form-row { margin-bottom: 12px; } - .inline-form { display: flex; gap: 8px; align-items: center; } - .inline-form .input { flex: 1; } - - /* 表格 */ - table.list { width: 100%; border-collapse: collapse; font-size: 13px; } - table.list th { text-align: left; font-size: 11.5px; font-weight: 500; color: var(--text-3); padding: 4px 0; } - table.list td { padding: 8px 0; border-top: 1px solid var(--border-soft); } - table.list tr:first-child td { border-top: none; } - - /* 弹层 */ - .modal-mask { - position: fixed; inset: 0; z-index: 40; - background: rgba(26,26,24,.3); - display: flex; align-items: center; justify-content: center; padding: 16px; - } - .modal-card { - width: 100%; max-width: 430px; background: var(--panel); - border: 1px solid var(--border-soft); border-radius: 14px; padding: 22px; - box-shadow: var(--shadow-pop); - } - .modal-title { font-size: 15px; font-weight: 600; margin-bottom: 14px; } - .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; } - .toast { - border-radius: 8px; padding: 8px 16px; font-size: 12.5px; color: #fff; - background: #333230; max-width: 330px; - } - .toast-err { background: #7E2C26; } - #toast-root { position: fixed; bottom: 18px; right: 18px; z-index: 50; display: flex; flex-direction: column; gap: 8px; } - - .quiet { color: var(--text-3); font-size: 12.5px; } - .divider { border-top: 1px solid var(--border-soft); margin: 14px 0; } - .section-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; } - .section-note { font-size: 11.5px; color: var(--text-3); margin-top: 6px; } - .file-path { font-family: var(--mono); font-size: 12.5px; color: var(--text); } - .file-meta { font-size: 11px; color: var(--text-3); font-family: var(--mono); } - .link-danger { color: var(--danger); font-size: 12.5px; background: none; border: none; cursor: pointer; padding: 0; } - .link-danger:hover { text-decoration: underline; } - - /* 开关(toggle)。用法: - 真正的 checkbox 藏在下面 —— 保留键盘可达与 :checked 语义,不做 div 假开关。 */ - .switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; } - .switch > input { position: absolute; opacity: 0; width: 0; height: 0; } - .switch > span { - position: relative; flex-shrink: 0; width: 30px; height: 17px; border-radius: 999px; - background: var(--border); transition: background .16s; - } - .switch > span::after { - content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; - border-radius: 50%; background: #fff; transition: transform .16s; - box-shadow: 0 1px 2px rgba(0,0,0,.25); - } - .switch > input:checked + span { background: var(--accent); } - .switch > input:checked + span::after { transform: translateX(13px); } - .switch > input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; } -`; diff --git a/hub/src/database/static.ts b/hub/src/database/static.ts index a5d01e0..9fcacb6 100644 --- a/hub/src/database/static.ts +++ b/hub/src/database/static.ts @@ -1,98 +1,71 @@ /** - * Serves the database-admin SPA (built by SvelteKit via `database-admin/build/`) - * and the SPA index fallback for client-side routes under `/database/*`. + * 前端静态托管:老师端 `/app/*` 与管理后台 `/database/*` 共用 `hub/filelib-web` + * 这**一份** SvelteKit 构建产物(adapter-static + fallback,见 filelib-web/svelte.config.js)。 * - * The SvelteKit project lives in `hub/database-admin/` and is built with - * `paths.base = '/database'`, so its assets are emitted under `/database/_app/*` - * (not the root `/_app/*` that admin-web owns — that keeps the two SPAs from - * colliding). On disk the files still live at `build/_app/*`; this handler maps - * the `/database`-prefixed URLs back to those files. + * 标准前后端分离:服务端不渲染任何页面 —— 两个前缀下都只把同一个 index.html + * 原样送出,由 SvelteKit 客户端路由决定显示哪个视图;数据一律走 /database/api/*。 * - * Run `npm run build` in database-admin/ to produce the static output. In - * development, `npm run dev` there proxies `/api`, `/auth`, and `/database` to - * the Hub. Override the UI directory with `CPH_DATABASE_UI_DIR` if needed. + * 三类路由: + * /_filelib/* — 构建产物资源(JS/CSS/字体)。SvelteKit 的 appDir 被改名为 + * `_filelib`,以避开 admin-web 在根上注册的 /_app/* + * (见 ../admin/static.ts)—— 同名会让 Fastify 启动即抛重复路由。 + * /app, /app/* — 老师端 SPA 回退 + * /database, /database/* — 管理后台 SPA 回退 * - * Mirrors src/admin/static.ts. + * 具体路由(/database/api/*、/database/dev-login、/app/dev-login-teacher 等)由 + * databaseRoutes.ts / teacherApp.ts 先注册;Fastify 按具体度匹配,通配不遮蔽它们。 + * + * Override the UI directory with `CPH_FILELIB_UI_DIR` if needed. */ import { readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { dirname, extname, join, resolve as resolvePath } from "node:path"; -import type { FastifyInstance } from "fastify"; - -const MIME: Record = { - ".html": "text/html; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".mjs": "text/javascript; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".png": "image/png", - ".jpg": "image/jpeg", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".json": "application/json; charset=utf-8", - ".txt": "text/plain; charset=utf-8", -}; +import { dirname, join, resolve as resolvePath } from "node:path"; +import fastifyStatic from "@fastify/static"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; function resolveUiDir(): string { - const override = process.env["CPH_DATABASE_UI_DIR"]; + const override = process.env["CPH_FILELIB_UI_DIR"]; if (override && override.trim() !== "") return resolvePath(override); const here = dirname(fileURLToPath(import.meta.url)); - return resolvePath(join(here, "..", "..", "database-admin", "build")); + return resolvePath(join(here, "..", "..", "filelib-web", "build")); } +/** 构建产物根目录下的顶层静态文件(SvelteKit 把 static/ 原样拷到这里)。 */ +const TOP_LEVEL_FILES = ["favicon.svg", "favicon.ico", "robots.txt"] as const; + export async function registerDatabaseSpa(app: FastifyInstance): Promise { const uiDir = resolveUiDir(); if (!existsSync(join(uiDir, "index.html"))) { app.log.warn( { uiDir }, - "database-admin/build not found; /database SPA shell disabled. Run `npm run build` in database-admin/ to enable. /database/config and /database/dev-login remain functional.", + "filelib-web/build not found; /app and /database SPA shells disabled. Run `npm run build --prefix filelib-web` to enable. JSON APIs remain fully functional.", ); return; } const indexHtml = await readFile(join(uiDir, "index.html"), "utf8"); - // SvelteKit build assets. base='/database' emits them at /database/_app/*, - // but on disk they're under build/_app/*. - app.get("/database/_app/*", async (request, reply) => { - const rel = (request.params as { "*": string })["*"]; - const safe = rel.split("/").filter((p) => p !== ".." && p !== "").join("/"); - try { - const buf = await readFile(join(uiDir, "_app", safe)); - const mime = MIME[extname(safe)] ?? "application/octet-stream"; - return reply.type(mime).send(buf); - } catch { - return reply.status(404).send({ error: { code: "not_found", message: "asset not found" } }); - } + // 构建资源。@fastify/static 负责 MIME、ETag/Last-Modified 与目录穿越防护。 + // 只注册一次 —— /app 与 /database 下的页面引用的都是这同一组绝对路径 + // (filelib-web 的 paths.relative=false 保证了这点)。 + await app.register(fastifyStatic, { + root: join(uiDir, "_filelib"), + prefix: "/_filelib/", + decorateReply: true, }); - // Top-level static files emitted under the base path (favicon.svg, robots.txt). - app.get("/database/favicon.svg", async (_request, reply) => { - try { - const buf = await readFile(join(uiDir, "favicon.svg")); - return reply.type("image/svg+xml").send(buf); - } catch { - return reply.status(404).send(); - } - }); - app.get("/database/robots.txt", async (_request, reply) => { - try { - const buf = await readFile(join(uiDir, "robots.txt")); - return reply.type("text/plain; charset=utf-8").send(buf); - } catch { - return reply.status(404).send(); - } - }); + for (const name of TOP_LEVEL_FILES) { + if (!existsSync(join(uiDir, name))) continue; + app.get(`/${name}`, async (_request, reply) => reply.sendFile(name, uiDir)); + } - // SPA client-side route fallback. Concrete /database/* routes (/database/config, - // /database/dev-login, and the asset routes above) are more specific, so - // Fastify's router matches them before this wildcard. Everything else under - // /database serves index.html so SvelteKit's client router can resolve the view. - app.get("/database", async (_request, reply) => { - return reply.type("text/html; charset=utf-8").send(indexHtml); - }); - app.get("/database/*", async (_request, reply) => { - return reply.type("text/html; charset=utf-8").send(indexHtml); - }); + // SPA 回退:两个前缀,同一份 index.html。处理器不读请求、不查库 —— 所以 + // 启动时读一次缓存在闭包里是安全的(每个请求发出的字节完全相同)。 + const sendIndex = async (_request: FastifyRequest, reply: FastifyReply): Promise => + reply.type("text/html; charset=utf-8").send(indexHtml); + + app.get("/app", sendIndex); + app.get("/app/*", sendIndex); + app.get("/database", sendIndex); + app.get("/database/*", sendIndex); } From a7f90f387d9cb0bfdc2d249be454c2385dd2a34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:18:15 +0800 Subject: [PATCH 09/14] =?UTF-8?q?chore(database-admin):=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E8=AF=A5=E5=89=8D=E7=AB=AF=E5=B7=A5=E7=A8=8B,?= =?UTF-8?q?=E5=B7=B2=E8=A2=AB=20filelib-web=20=E5=8F=96=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12628c9 引入它意在替换后端渲染的 /database 页面,但从未接通:具体 路由 /database/dashboard 比 SPA 通配 /database/* 更具体,服务端 handler 永远胜出,SPA 的 dashboard 不可达。工程头注释声称 SPA 已 接管 dashboard、且 /database/config 存在,两者当时都不成立。 hub 的 build 脚本也从未构建它,于是 static.ts 里的 existsSync 守卫 每次部署都失败,这个外壳实际长期处于禁用状态 —— 它没服务过一个请求。 与 filelib-web 合并而非并存的理由:两者共用文件库浏览器、会话层、 toast 宿主与设计令牌,拆开就要把这些全复制一遍(ADR-0029)。 内容可从 git 历史取回。 --- hub/database-admin/.gitignore | 23 - hub/database-admin/.npmrc | 1 - hub/database-admin/.prettierignore | 4 - hub/database-admin/.prettierrc.json | 9 - hub/database-admin/package-lock.json | 2660 ----------------- hub/database-admin/package.json | 33 - hub/database-admin/src/app.d.ts | 13 - hub/database-admin/src/app.html | 29 - hub/database-admin/src/lib/api.ts | 76 - .../src/lib/components/Aurora.svelte | 6 - hub/database-admin/src/lib/org.ts | 7 - hub/database-admin/src/lib/session.ts | 58 - hub/database-admin/src/routes/+layout.svelte | 7 - hub/database-admin/src/routes/+page.svelte | 30 - .../src/routes/admin/+page.svelte | 78 - hub/database-admin/src/routes/app.css | 96 - .../src/routes/dashboard/+page.svelte | 160 - hub/database-admin/static/favicon.svg | 5 - hub/database-admin/static/robots.txt | 3 - hub/database-admin/svelte.config.js | 22 - hub/database-admin/tsconfig.json | 20 - hub/database-admin/vite.config.ts | 17 - 22 files changed, 3357 deletions(-) delete mode 100644 hub/database-admin/.gitignore delete mode 100644 hub/database-admin/.npmrc delete mode 100644 hub/database-admin/.prettierignore delete mode 100644 hub/database-admin/.prettierrc.json delete mode 100644 hub/database-admin/package-lock.json delete mode 100644 hub/database-admin/package.json delete mode 100644 hub/database-admin/src/app.d.ts delete mode 100644 hub/database-admin/src/app.html delete mode 100644 hub/database-admin/src/lib/api.ts delete mode 100644 hub/database-admin/src/lib/components/Aurora.svelte delete mode 100644 hub/database-admin/src/lib/org.ts delete mode 100644 hub/database-admin/src/lib/session.ts delete mode 100644 hub/database-admin/src/routes/+layout.svelte delete mode 100644 hub/database-admin/src/routes/+page.svelte delete mode 100644 hub/database-admin/src/routes/admin/+page.svelte delete mode 100644 hub/database-admin/src/routes/app.css delete mode 100644 hub/database-admin/src/routes/dashboard/+page.svelte delete mode 100644 hub/database-admin/static/favicon.svg delete mode 100644 hub/database-admin/static/robots.txt delete mode 100644 hub/database-admin/svelte.config.js delete mode 100644 hub/database-admin/tsconfig.json delete mode 100644 hub/database-admin/vite.config.ts diff --git a/hub/database-admin/.gitignore b/hub/database-admin/.gitignore deleted file mode 100644 index 3b462cb..0000000 --- a/hub/database-admin/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/hub/database-admin/.npmrc b/hub/database-admin/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/hub/database-admin/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/hub/database-admin/.prettierignore b/hub/database-admin/.prettierignore deleted file mode 100644 index 491d6b0..0000000 --- a/hub/database-admin/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -build -.svelte-kit -node_modules -package-lock.json diff --git a/hub/database-admin/.prettierrc.json b/hub/database-admin/.prettierrc.json deleted file mode 100644 index 5b31130..0000000 --- a/hub/database-admin/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "semi": true, - "trailingComma": "all", - "printWidth": 120, - "plugins": ["prettier-plugin-svelte"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] -} diff --git a/hub/database-admin/package-lock.json b/hub/database-admin/package-lock.json deleted file mode 100644 index 8f513b3..0000000 --- a/hub/database-admin/package-lock.json +++ /dev/null @@ -1,2660 +0,0 @@ -{ - "name": "database-admin", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "database-admin", - "version": "0.0.1", - "devDependencies": { - "@skeletonlabs/skeleton": "^4.15.2", - "@skeletonlabs/skeleton-svelte": "^4.15.2", - "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.63.0", - "@sveltejs/vite-plugin-svelte": "^7.1.2", - "@tailwindcss/vite": "^4.3.2", - "bits-ui": "^2.18.1", - "prettier": "^3.9.5", - "prettier-plugin-svelte": "^4.1.1", - "svelte": "^5.56.1", - "svelte-check": "^4.6.0", - "tailwindcss": "^4.3.2", - "typescript": "^6.0.3", - "vite": "^8.0.16" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@internationalized/date": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", - "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@skeletonlabs/skeleton": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton/-/skeleton-4.15.2.tgz", - "integrity": "sha512-5O23Py76nw56aoieV2b2T7MJ6xS0DwDjUULpwLnCXxXOnmzADEoWoQxb/ABbqg04IMOygtRzK3HO22I1+kFsog==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": "^4.0.0" - } - }, - "node_modules/@skeletonlabs/skeleton-common": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton-common/-/skeleton-common-4.15.2.tgz", - "integrity": "sha512-y7KZn++Av8UHdoeaaguQ7zIS1HlK7Y5jyPDnHy65wJ60iS97fMtQV0kGhqVoYCWhor+xrjbAFjWtaPOPPDEMYA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@skeletonlabs/skeleton-svelte": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton-svelte/-/skeleton-svelte-4.15.2.tgz", - "integrity": "sha512-vZkRhR701EOHVXVKh/NFRSY8D9LPBvmdNFdtfgqO7TEg77sypJUvERl2dxErTLY1QjVklVpjsHRCTaSLn+1Ntg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@internationalized/date": "3.12.0", - "@skeletonlabs/skeleton-common": "4.15.2", - "@zag-js/accordion": "1.39.1", - "@zag-js/avatar": "1.39.1", - "@zag-js/carousel": "1.39.1", - "@zag-js/collapsible": "1.39.1", - "@zag-js/collection": "1.39.1", - "@zag-js/combobox": "1.39.1", - "@zag-js/date-picker": "1.39.1", - "@zag-js/dialog": "1.39.1", - "@zag-js/file-upload": "1.39.1", - "@zag-js/floating-panel": "1.39.1", - "@zag-js/listbox": "1.39.1", - "@zag-js/menu": "1.39.1", - "@zag-js/pagination": "1.39.1", - "@zag-js/popover": "1.39.1", - "@zag-js/progress": "1.39.1", - "@zag-js/radio-group": "1.39.1", - "@zag-js/rating-group": "1.39.1", - "@zag-js/slider": "1.39.1", - "@zag-js/steps": "1.39.1", - "@zag-js/svelte": "1.39.1", - "@zag-js/switch": "1.39.1", - "@zag-js/tabs": "1.39.1", - "@zag-js/tags-input": "1.39.1", - "@zag-js/toast": "1.39.1", - "@zag-js/toggle-group": "1.39.1", - "@zag-js/tooltip": "1.39.1", - "@zag-js/tree-view": "1.39.1" - }, - "peerDependencies": { - "svelte": "^5.29.0" - } - }, - "node_modules/@skeletonlabs/skeleton-svelte/node_modules/@internationalized/date": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", - "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", - "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz", - "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.70.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", - "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", - "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", - "cookie": "^0.6.0", - "devalue": "^5.8.1", - "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@sveltejs/load-config": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", - "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", - "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "deepmerge": "^4.3.1", - "magic-string": "^0.30.21", - "obug": "^2.1.0", - "vitefu": "^1.1.2" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "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==", - "dev": true, - "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/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@zag-js/accordion": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.39.1.tgz", - "integrity": "sha512-GA3m7gRTm3weSe1eMlHIsTNztcjZ6joIaRgxxKil7q/UX0xIVVGDy0aCr6oo7FAuoMiOOBVurYXILpFZ30nOXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/anatomy": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/anatomy/-/anatomy-1.39.1.tgz", - "integrity": "sha512-p2iFAs2pVQgv5iCDAftA7g9Z/fUYXW94dRIGk415TSbkp/YDENydm/JtRoNctp302UIx4Eeuc5QBR+7h5kuISA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@zag-js/aria-hidden": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/aria-hidden/-/aria-hidden-1.39.1.tgz", - "integrity": "sha512-wiwcz3N086qBMEU3VKfHhcvGm6Jm1PIcDXys/jEqiKPtHoYZhDip0n0cPOoasss/A1oS39QFVdk3WpLXGu3Izw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/auto-resize": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/auto-resize/-/auto-resize-1.39.1.tgz", - "integrity": "sha512-ditIo9mW7fapq+4yx3/8hMpMZlWaoOy66EOzUz8dSVqnxnTWAjnTICu/9zFh8pkWerlzGTtDOJPP1oZ8S/rgVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/avatar": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/avatar/-/avatar-1.39.1.tgz", - "integrity": "sha512-LWrgJ0bebnXPSL+uehA9z6BlCD/MZEOQBJqH/F2QQFSAAZXUUDKtzVDmc+UtwjDsHXqqTghi+v2atQJHNMcJ2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/carousel": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/carousel/-/carousel-1.39.1.tgz", - "integrity": "sha512-5z5z3IldUgZ/R+KZLNQDoJFNTXzYd28YOmgfWH61Vvyv+RarX8kwZW8ajW/fNiqcWXyhW3/VMU0lArrfjbQVtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/scroll-snap": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/collapsible": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/collapsible/-/collapsible-1.39.1.tgz", - "integrity": "sha512-Zgccg/t7M8i0JVwZPPgW7XB7kGhTO475hsmwkF/8CYLqBBckVDHUARp2we24hENCm/98eez6R0eDEmE+tldFWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/collection": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/collection/-/collection-1.39.1.tgz", - "integrity": "sha512-fyOyKmP7MRo0/U8mBmB7KgHRXHhXP27LCcasy3x+qTAQtuEfYG1EPhKuj07oBWlX/2qfcKYn2R3YopHcqFcCiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/combobox": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/combobox/-/combobox-1.39.1.tgz", - "integrity": "sha512-fmStpG+k4xrxCzqUX0ssnOMeoSietWm5ir3qmEZcagzNqNycAXMvOELAIeyXi87Kut6aDGhxLOV7o395HVXl/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/collection": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/live-region": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/core": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/core/-/core-1.39.1.tgz", - "integrity": "sha512-Yp0r49QLYXe2j7fgyAiilH4umXFydCnr5hcRDwJU+sxvUAlq00JQIJIEK2pT6k8cJiNNsFEV5WkOX7jsqpAX2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/date-picker": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/date-picker/-/date-picker-1.39.1.tgz", - "integrity": "sha512-t9q1H0aZQJkbzKTR2Bn5vMwaoFoirxekiSxw8ju0F0vr4Kg4BJ9yueOQm5I2wALKnJbZu4Ua5MgzlrDF3CQt3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/date-utils": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/live-region": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - }, - "peerDependencies": { - "@internationalized/date": ">=3.0.0" - } - }, - "node_modules/@zag-js/date-utils": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/date-utils/-/date-utils-1.39.1.tgz", - "integrity": "sha512-i4SvBhru2Yz/zsHT0XvyFhf4a+pAKYkWXeVfU0RvF2S6mPTfgaMFF9ZNPq5Sy8K31EtAa6AVXcybYaYnibn1FA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@internationalized/date": ">=3.0.0" - } - }, - "node_modules/@zag-js/dialog": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/dialog/-/dialog-1.39.1.tgz", - "integrity": "sha512-q+HTmfuRDRZthln9mb7i52wdltQOZlw3+nw3a2uygEe9xuEtHBwUz31XJzkn2UWQqhAt7cC39OwykhNLKrfkqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/aria-hidden": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-trap": "1.39.1", - "@zag-js/remove-scroll": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/dismissable": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/dismissable/-/dismissable-1.39.1.tgz", - "integrity": "sha512-7/soy93Ersd5qedhSL/+CDcZ9gNTQV0ooDcqKtM8b4IxwD4rgWwGsewJY+tbKmOqaZobwa0YcWV2+YGgI23ESw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1", - "@zag-js/interact-outside": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/dom-query": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-1.39.1.tgz", - "integrity": "sha512-k01aXeUWLyJfB61CODaXj4PLhYmVpnVMFrC+3nk/XCn1MW7my8L/8KVg0m4W8n+X9MhpaLWsZDmK/dwED/3qSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/types": "1.39.1" - } - }, - "node_modules/@zag-js/file-upload": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/file-upload/-/file-upload-1.39.1.tgz", - "integrity": "sha512-cErPOnPwPyneUXpelsfm75DKn0/4SI8aqQnlbrqo522PEqAQyDfDdBsqebGgKWG3F0A++kKFp9LO9A5zCrw5gA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/file-utils": "1.39.1", - "@zag-js/i18n-utils": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/file-utils": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/file-utils/-/file-utils-1.39.1.tgz", - "integrity": "sha512-ll/W5o74SMmoAS+l7PkmmGjPj4PLCSG/cwQh1Y/+LpaSev0YiR3Nk2OzRIIPtm3NivYVxKGawaCOf1RvT/82LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/i18n-utils": "1.39.1" - } - }, - "node_modules/@zag-js/floating-panel": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/floating-panel/-/floating-panel-1.39.1.tgz", - "integrity": "sha512-IfPbf3pwJGqBWHec/rPzpdPjfMCLed59LlEophvRy49FEdksv8eN6nr9DXl2wWZEoQhH99scXfLMbtEZsPsFWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/rect-utils": "1.39.1", - "@zag-js/store": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/focus-trap": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/focus-trap/-/focus-trap-1.39.1.tgz", - "integrity": "sha512-2ZzVefHMotvtxUo/gP4R45Szw/EPaPkTKEHaug6/il62SPDbkFODF+5r1zXyLbLuwCHq0apvQasg/ONLihwlXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/focus-visible": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-1.39.1.tgz", - "integrity": "sha512-iEuTOYHE8HRn/7ULC9c9BTTWo0C0MJRCbYVxbh/d7v8qAuq4CS76pdfceNo3KeWbb968T+yiG6q0AjiHsr8IOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/i18n-utils": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/i18n-utils/-/i18n-utils-1.39.1.tgz", - "integrity": "sha512-TKRLQQlHgJ4cxsHo3tZPtbFjGu9m1UPtfezRGFKq7A8czhdqRhaCpaWF849cd6dI7x6rWvvTan858gOFpyANnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/interact-outside": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/interact-outside/-/interact-outside-1.39.1.tgz", - "integrity": "sha512-LnSbA+txMsFmzNPn84QKH01x2yJv4At/eKHn6rT2PyxXkJQIh8PvCTS3zVz4Syw11cmhcXt2eRwhzx8yImV92w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/listbox": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/listbox/-/listbox-1.39.1.tgz", - "integrity": "sha512-Mz0UpdXobdTQTyjM+Avgi7pDVB2dKyaUHqw3TloeleQL3VwTqClclkwHXtLYYE+oXa0zOet37wI9mzfaYx9iZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/collection": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/live-region": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/live-region/-/live-region-1.39.1.tgz", - "integrity": "sha512-E7YNd0QGzJ2n1ZhnI2smv+klwifsNRf9QaDCx7quVJCVYywpupsBK4R25KN75S1z8XaK+jAy6HYKj8DIhYjYeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@zag-js/menu": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/menu/-/menu-1.39.1.tgz", - "integrity": "sha512-bRDGLGkiGhzNtORBXkbBQV/xp2zEkwpYIepfWCaUoFwKUmx7GGnShTBFxJyq0u2D4IkS9GOwcqm20EhMv6V+TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/rect-utils": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/pagination": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/pagination/-/pagination-1.39.1.tgz", - "integrity": "sha512-3Q1B9/g3ajhvXjuGffJ7otyXcXK5+uhdbE5A9CZa4bsW3pf25L9Cp+ZAjdXQMDc8T4jhZJAKFmDJfQgtr1oEIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/popover": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/popover/-/popover-1.39.1.tgz", - "integrity": "sha512-aO3ExO/O7Sa3ovdozFI6SujhNOpYdCca4bImnAiovDL8DY8zN3UNQebu35IQvw9/aRsx9VKSJL1AqzJJUImFRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/aria-hidden": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-trap": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/remove-scroll": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/popper": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/popper/-/popper-1.39.1.tgz", - "integrity": "sha512-h0UMY2dXJNfM3OvMQ9t9LzlmwvpCgjloz2IvU1txY3r32UIy7ve1H70zkKagLtLRxFTuWmhumYUPULPo/6a1DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6", - "@zag-js/dom-query": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/progress": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/progress/-/progress-1.39.1.tgz", - "integrity": "sha512-1IHyOw8DqPs3YH149Oj7W9a5oEfY5pc9GAVOPGbzYxVK/W8d/NIjVxa565I3J5cDJ0s6z3FrMSXMWUwr1ML4tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/radio-group": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/radio-group/-/radio-group-1.39.1.tgz", - "integrity": "sha512-+sC9xcAyY/GbY+8HpKlbPgSyOxBLUSB18s6fe6K1wdmyom4PM0nmhLouuxisbFZYHOyfQwAOMo+ainRENB2hzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/rating-group": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/rating-group/-/rating-group-1.39.1.tgz", - "integrity": "sha512-IfdxWmM+3zpztx/HcE3bWob72sZNb1+BzK4tSySLVyjeqs8OzLDzrCbKqt10DmibnNOvpbjbq4eX4P5hV9YN7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/rect-utils": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/rect-utils/-/rect-utils-1.39.1.tgz", - "integrity": "sha512-5gJ0PzeUme76xTWG+4XythWgmGgDKV4XAxEUaB3KKDtXgjDHwtu7PwKLIzFtlaaSf/U23PY+RNVBVCYg1GmZog==", - "dev": true, - "license": "MIT" - }, - "node_modules/@zag-js/remove-scroll": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/remove-scroll/-/remove-scroll-1.39.1.tgz", - "integrity": "sha512-uZfPR3Gl9sQFo+tJ7kbuwsBhw+RIZwWFnMDgrz5LIwSNGN6hsyC4HGOxe29clkWQ2X2AjqqmEMETwgX7Jg+wxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/scroll-snap": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/scroll-snap/-/scroll-snap-1.39.1.tgz", - "integrity": "sha512-AzCc8MAAVqkiK5Y0cJZ24OIBZDQrUmEexACMuR6M5yZmlcEbS0EA/d6Wq+LSR1JMVTD4B+UwcMj1D3vJQ90ZTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/dom-query": "1.39.1" - } - }, - "node_modules/@zag-js/slider": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/slider/-/slider-1.39.1.tgz", - "integrity": "sha512-OEA9R7Ly5cw+6ANofnMpuHH3rAo8gZEnxy7iEwePu11pq2RCnt8DSj2V+uqU+dTq15Uup1LSzRgJfTnAC4Z85A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/steps": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/steps/-/steps-1.39.1.tgz", - "integrity": "sha512-DC6swMpwITTB0DyCSxlpWyPNSUN9ul9jz4N6aAyQ0L1IK/noF/YYTZRAcXNSRzN4iutO/2mFGGbwGq/oVf+gPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/store": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/store/-/store-1.39.1.tgz", - "integrity": "sha512-zFpwP4lhiBVD9987rwAfZNVa2/f/xx4mhbCE1EEw31zxLAozY2jONeJ3UzPP05VbzKlRHBcvkaXAJQQGegTwFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "proxy-compare": "3.0.1" - } - }, - "node_modules/@zag-js/svelte": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/svelte/-/svelte-1.39.1.tgz", - "integrity": "sha512-ZOyZjyvjePZdrkNTy5fa92ijeeID9e+3LRGziKGIII3JSEvwfkG/Buf8W84N8VHFxi0G0GcKmgVCDgKHyGQYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/core": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - }, - "peerDependencies": { - "svelte": ">=5" - } - }, - "node_modules/@zag-js/switch": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/switch/-/switch-1.39.1.tgz", - "integrity": "sha512-ikeQ42c0vyyPLeyW9U0dvcqTV1Ekpx5jZ050R905HGJ2GeWE0uBGuHbMpTG5U6Pwb0a+TMzqAr+jMsquVTCwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/tabs": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/tabs/-/tabs-1.39.1.tgz", - "integrity": "sha512-P2RThO1gX9SFsNqrAGPsXJxrjn5YqP6MFs9mdExU+tzzZyVjJQADkAmh98C0eEaCb6HKLpJZ/17hrnLDhm1Tig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/tags-input": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/tags-input/-/tags-input-1.39.1.tgz", - "integrity": "sha512-tc0+bd9FiUJwa+wY2hSVVGHLIBC3C3rOZX/4zjchRMs1xgl92c1/tYbytXny7ABB8ZMHveG7MtgDppVF4VkwBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/auto-resize": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/interact-outside": "1.39.1", - "@zag-js/live-region": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/toast": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/toast/-/toast-1.39.1.tgz", - "integrity": "sha512-K7ndEfBTKDds10iQKCQUmin74s6V4BEIypAIyQxs18gQB9TCn5+wff886JAzecIKPY97PDQHDKjYR71yzRC7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dismissable": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/toggle-group": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/toggle-group/-/toggle-group-1.39.1.tgz", - "integrity": "sha512-KS4Bo17foMKXVBhQjocRf4GQxMV4pMXclTo14IWjldaHs2HIrNJ0Ar0Ri+vo47BBKBNsXs4HuNvfbMdQj94wEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/tooltip": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/tooltip/-/tooltip-1.39.1.tgz", - "integrity": "sha512-IsxFj7l8kPciwIyYJWlmQ7mhXocbjXxLj3m9z099slYOF7lApA33/ndY32w9ptrI4/nUh2nldzw6eRfSpVnuOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/focus-visible": "1.39.1", - "@zag-js/popper": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/tree-view": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/tree-view/-/tree-view-1.39.1.tgz", - "integrity": "sha512-sm6qUZjO0OaqBqO5s55KU+l5p1wXfUVScoen7BYVoFBuROH7qAZJi8YMclGvnnlyV506i8Hk0qqWnLg0F38jCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zag-js/anatomy": "1.39.1", - "@zag-js/collection": "1.39.1", - "@zag-js/core": "1.39.1", - "@zag-js/dom-query": "1.39.1", - "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" - } - }, - "node_modules/@zag-js/types": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/types/-/types-1.39.1.tgz", - "integrity": "sha512-w3vVpgxmdJvMDvv19DXTtFI6kJL6TXw//U0Z1BAc3rnDA9orcB9Ryw4uMNvIzFA607CgssyJcWDaQ/M3yAcbJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "3.2.3" - } - }, - "node_modules/@zag-js/utils": { - "version": "1.39.1", - "resolved": "https://registry.npmjs.org/@zag-js/utils/-/utils-1.39.1.tgz", - "integrity": "sha512-9k741cH7L655Ua3tedTkuMblcXVXVgCLTB9svp9oTjA7oatpOpYF4z43kgAQVjyThNXMJ7AvtO4C80ajQLTScg==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/bits-ui": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", - "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.1", - "@floating-ui/dom": "^1.7.1", - "esm-env": "^1.1.2", - "runed": "^0.35.1", - "svelte-toolbelt": "^0.10.6", - "tabbable": "^6.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/huntabyte" - }, - "peerDependencies": { - "@internationalized/date": "^3.8.1", - "svelte": "^5.33.0" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", - "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", - "dev": true, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esrap": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", - "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": 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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "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==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "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/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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.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" - ], - "dev": true, - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "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.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" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.1.tgz", - "integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^5.0.0" - } - }, - "node_modules/proxy-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", - "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "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/runed": { - "version": "0.35.1", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", - "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte", - "https://github.com/sponsors/tglide" - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "esm-env": "^1.0.0", - "lz-string": "^1.5.0" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.21.0", - "svelte": "^5.7.0" - }, - "peerDependenciesMeta": { - "@sveltejs/kit": { - "optional": true - } - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", - "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/svelte": { - "version": "5.56.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", - "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.8.1", - "esm-env": "^1.2.1", - "esrap": "^2.2.12", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", - "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "@sveltejs/load-config": "^0.2.0", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-toolbelt": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", - "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte" - ], - "dependencies": { - "clsx": "^2.1.1", - "runed": "^0.35.1", - "style-to-object": "^1.0.8" - }, - "engines": { - "node": ">=18", - "pnpm": ">=8.7.0" - }, - "peerDependencies": { - "svelte": "^5.30.2" - } - }, - "node_modules/tabbable": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", - "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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==", - "dev": true, - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "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==", - "dev": true, - "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/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/hub/database-admin/package.json b/hub/database-admin/package.json deleted file mode 100644 index dea271a..0000000 --- a/hub/database-admin/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "database-admin", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", - "format:check": "prettier --check ." - }, - "devDependencies": { - "@skeletonlabs/skeleton": "^4.15.2", - "@skeletonlabs/skeleton-svelte": "^4.15.2", - "@sveltejs/adapter-auto": "^7.0.1", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.63.0", - "@sveltejs/vite-plugin-svelte": "^7.1.2", - "@tailwindcss/vite": "^4.3.2", - "bits-ui": "^2.18.1", - "prettier": "^3.9.5", - "prettier-plugin-svelte": "^4.1.1", - "svelte": "^5.56.1", - "svelte-check": "^4.6.0", - "tailwindcss": "^4.3.2", - "typescript": "^6.0.3", - "vite": "^8.0.16" - } -} diff --git a/hub/database-admin/src/app.d.ts b/hub/database-admin/src/app.d.ts deleted file mode 100644 index da08e6d..0000000 --- a/hub/database-admin/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/hub/database-admin/src/app.html b/hub/database-admin/src/app.html deleted file mode 100644 index 827d63d..0000000 --- a/hub/database-admin/src/app.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - Database Admin - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/hub/database-admin/src/lib/api.ts b/hub/database-admin/src/lib/api.ts deleted file mode 100644 index 50c433f..0000000 --- a/hub/database-admin/src/lib/api.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Thin API client for the database-admin backend. Same-origin cookie auth, - * reusing the platform session (`cph_session`) and the admin plane's /api/me. - */ - -export class ApiError extends Error { - code: string; - status: number; - constructor(code: string, message: string, status: number) { - super(message); - this.name = 'ApiError'; - this.code = code; - this.status = status; - } -} - -async function request(method: string, url: string, body?: unknown): Promise { - const init: RequestInit = { - method, - credentials: 'same-origin', - headers: body !== undefined ? { 'content-type': 'application/json' } : undefined, - body: body !== undefined ? JSON.stringify(body) : undefined, - }; - const res = await fetch(url, init); - const text = await res.text(); - let data: unknown = null; - if (text !== '') { - try { - data = JSON.parse(text); - } catch { - data = text; - } - } - if (!res.ok) { - const err = (data as { error?: { code?: string; message?: string } } | null)?.error; - throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status); - } - return data; -} - -const get = (u: string) => request('GET', u); -const post = (u: string, b?: unknown) => request('POST', u, b); - -// --- Types --- - -export interface OrgMembership { - id: string; - slug: string; - name: string; - status: string; - role: 'OWNER' | 'ADMIN' | 'MEMBER'; -} - -export interface MeResponse { - user: { - id: string; - feishuOpenId: string; - displayName: string; - avatarUrl: string | null; - }; - organizations: OrgMembership[]; -} - -/** Unauthenticated bootstrap the login page needs: which org to OAuth against + dev toggle. */ -export interface DatabaseConfig { - siloOrganizationSlug: string; - devLoginEnabled: boolean; -} - -// --- API --- - -export const api = { - me: () => get('/api/me') as Promise, - logout: () => post('/auth/logout'), - databaseConfig: () => get('/database/config') as Promise, -}; diff --git a/hub/database-admin/src/lib/components/Aurora.svelte b/hub/database-admin/src/lib/components/Aurora.svelte deleted file mode 100644 index 27c39b4..0000000 --- a/hub/database-admin/src/lib/components/Aurora.svelte +++ /dev/null @@ -1,6 +0,0 @@ - -
-
-
-
-
diff --git a/hub/database-admin/src/lib/org.ts b/hub/database-admin/src/lib/org.ts deleted file mode 100644 index 3973154..0000000 --- a/hub/database-admin/src/lib/org.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { OrgMembership } from './api'; - -export function isOrgAdmin(org: OrgMembership | null | undefined): boolean { - if (!org) return false; - const role = String(org.role ?? '').toUpperCase(); - return role === 'OWNER' || role === 'ADMIN'; -} diff --git a/hub/database-admin/src/lib/session.ts b/hub/database-admin/src/lib/session.ts deleted file mode 100644 index a62ce30..0000000 --- a/hub/database-admin/src/lib/session.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { writable } from 'svelte/store'; -import { goto } from '$app/navigation'; -import { base } from '$app/paths'; -import { api, type MeResponse } from './api'; - -interface SessionState { - loading: boolean; - me: MeResponse | null; - error: string | null; -} - -export const session = writable({ - loading: true, - me: null, - error: null, -}); - -export async function loadSession(): Promise { - session.update((s) => ({ ...s, loading: true, error: null })); - try { - const me = await api.me(); - session.set({ loading: false, me, error: null }); - } catch (err) { - const status = (err as { status?: number }).status; - if (status === 401) { - session.set({ loading: false, me: null, error: null }); - void redirectToLogin(); - return; - } - session.set({ - loading: false, - me: null, - error: err instanceof Error ? err.message : String(err), - }); - } -} - -/** - * Send the browser to the login page (`/database/admin`). Unlike admin-web we - * don't jump straight to Feishu OAuth: the login page reads /database/config - * to build the org-scoped link and to show the dev bypass when enabled. - */ -export async function redirectToLogin(): Promise { - const loginPath = `${base}/admin`; - if (window.location.pathname === loginPath) return; - await goto(loginPath, { replaceState: true }); -} - -/** Build the org-scoped Feishu OAuth entry point (a backend route, not under base). */ -export function feishuLoginHref(orgSlug: string, returnTo: string = `${base}/dashboard`): string { - return `/auth/feishu/${encodeURIComponent(orgSlug)}?returnTo=${encodeURIComponent(returnTo)}`; -} - -export async function logout(): Promise { - await api.logout(); - session.set({ loading: false, me: null, error: null }); - await redirectToLogin(); -} diff --git a/hub/database-admin/src/routes/+layout.svelte b/hub/database-admin/src/routes/+layout.svelte deleted file mode 100644 index 5f7ae2b..0000000 --- a/hub/database-admin/src/routes/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{@render children()} diff --git a/hub/database-admin/src/routes/+page.svelte b/hub/database-admin/src/routes/+page.svelte deleted file mode 100644 index 83c0c1f..0000000 --- a/hub/database-admin/src/routes/+page.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
-
- - - - -

正在进入…

-
-
diff --git a/hub/database-admin/src/routes/admin/+page.svelte b/hub/database-admin/src/routes/admin/+page.svelte deleted file mode 100644 index 65e7732..0000000 --- a/hub/database-admin/src/routes/admin/+page.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - -
-
-
-
- D -
-

Database Admin

-

使用飞书登录以管理数据库

-
- - {#if error} -
- 无法连接后端:{error} -
- {/if} - - - - - - 使用飞书登录 - - - {#if config?.devLoginEnabled} -
-
-
- 开发模式 -
-
- - 一键登录管理员 - -

仅开发环境可见 · 跳过飞书 OAuth

- {/if} -
-
diff --git a/hub/database-admin/src/routes/app.css b/hub/database-admin/src/routes/app.css deleted file mode 100644 index c29cc79..0000000 --- a/hub/database-admin/src/routes/app.css +++ /dev/null @@ -1,96 +0,0 @@ -@import 'tailwindcss'; - -@source './**/*.{html,js,svelte,ts}'; -@source '../lib/**/*.{html,js,svelte,ts}'; - -/* - * Design system for the database-admin SPA. Migrated verbatim (in spirit) from - * the previous server-rendered pages in hub/src/database/routes/databaseRoutes.ts: - * a light aurora / glassmorphism look with violet→cyan gradients and soft rise-in - * entrances. Distinct from admin-web's flat industrial theme on purpose. - */ - -@theme { - --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', sans-serif; -} - -@keyframes aurora { - 0% { - transform: translate(0, 0) scale(1); - } - 33% { - transform: translate(6%, -8%) scale(1.15); - } - 66% { - transform: translate(-8%, 6%) scale(0.9); - } - 100% { - transform: translate(0, 0) scale(1); - } -} - -@keyframes rise { - from { - opacity: 0; - transform: translateY(16px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -@layer base { - :root { - font-family: var(--font-sans); - } - html { - height: 100%; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } -} - -@layer components { - .aurora { - filter: blur(80px); - animation: aurora 18s ease-in-out infinite; - } - .rise { - animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; - } - .rise-1 { - animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; - } - .rise-2 { - animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both; - } - .rise-3 { - animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both; - } - .grad-text { - background: linear-gradient(120deg, #7c3aed, #0891b2, #4f46e5); - background-size: 200% auto; - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: shimmer 6s linear infinite; - } - .glass { - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - } - .glow-btn { - box-shadow: 0 12px 32px -10px rgba(124, 58, 237, 0.45); - } -} diff --git a/hub/database-admin/src/routes/dashboard/+page.svelte b/hub/database-admin/src/routes/dashboard/+page.svelte deleted file mode 100644 index de75911..0000000 --- a/hub/database-admin/src/routes/dashboard/+page.svelte +++ /dev/null @@ -1,160 +0,0 @@ - - - - -{#if $session.loading} -
-
- - - - -

加载中…

-
-
-{:else if $session.error} -
-
-

无法连接后端

-

{$session.error}

- -
-
-{:else if !allowed} -
-
-

无权访问

-

数据库管理台仅向组织所有者与管理员开放。

- -
-
-{:else} -
- - - - -
-
-
-

概览

-

欢迎回来,这里是数据库管理台

-
- -
- -
-
- {#each stats as s, i} -
-
-

{s.label}

-

{s.value}

-
- {/each} -
- -
- 内容区占位 · 后续数据端点挂在 /database/* 并加 guard -
-
-
-
-{/if} diff --git a/hub/database-admin/static/favicon.svg b/hub/database-admin/static/favicon.svg deleted file mode 100644 index 43bc66d..0000000 --- a/hub/database-admin/static/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/hub/database-admin/static/robots.txt b/hub/database-admin/static/robots.txt deleted file mode 100644 index b6dd667..0000000 --- a/hub/database-admin/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# allow crawling everything by default -User-agent: * -Disallow: diff --git a/hub/database-admin/svelte.config.js b/hub/database-admin/svelte.config.js deleted file mode 100644 index 9c60538..0000000 --- a/hub/database-admin/svelte.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - pages: 'build', - assets: 'build', - fallback: 'index.html', - precompress: false, - strict: false, - }), - // This SPA is served under /database by the Hub. admin-web owns the - // root asset paths (/_app, /favicon.svg); a base path moves this app's - // assets to /database/_app/* so the two builds never collide. - paths: { base: '/database' }, - }, -}; - -export default config; diff --git a/hub/database-admin/tsconfig.json b/hub/database-admin/tsconfig.json deleted file mode 100644 index 2c2ed3c..0000000 --- a/hub/database-admin/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "rewriteRelativeImportExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript -} diff --git a/hub/database-admin/vite.config.ts b/hub/database-admin/vite.config.ts deleted file mode 100644 index 4839de8..0000000 --- a/hub/database-admin/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], - server: { - proxy: { - '/api': 'http://127.0.0.1:8788', - '/auth': 'http://127.0.0.1:8788', - // Backend owns the /database/* HTTP surface (login page, dashboard, - // data routes). Proxy it in dev so those paths hit the real server - // instead of the SPA fallback. - '/database': 'http://127.0.0.1:8788', - }, - }, -}); From 325b4fc13704280e2c72d067ed865ecf8b996f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:19:10 +0800 Subject: [PATCH 10/14] =?UTF-8?q?fix(filelib-web):=20=E8=A1=A5=E5=9B=9E?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E4=B8=A2=E5=A4=B1=E7=9A=84=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=E5=B1=82=E4=B8=8E=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第一版迁移只把 uiTheme.ts 的 @theme 颜色令牌搬了过来,155 行里约 90 行的组件类(.btn/.panel/.input/.select/table.list/.tag/.switch/ .link-danger/.quiet 等)被丢掉,于是每个组件各自内联重述按钮、输入框、 面板的样式 —— 正是旧代码的重复问题被原样复刻,后台观感明显退化。 现在 app.css 是设计系统的唯一去处:@theme 管令牌,@layer components 管组件类。组件只带布局工具类,不重述组件样式。 图标集同样是丢的:旧面板有 13 个内联 SVG,新版一个不剩,只有纯文字的 「+」「删除」—— 这是"简陋"最直接的来源。提成 Icon.svelte 共享。 Group 节点沿用两人剪影而非文件夹图标:MemberGroup 与文件库的 FOLDER/PROJECT 是两套无关层级,图标不应混淆(ADR-0028/0021)。 顺手修 FilesPanel 的 uploadInput:bind:this 的目标要用 $state, 否则 Svelte 5 下不保证更新。 --- hub/filelib-web/src/app.css | 201 +++++++++++++++++++++- hub/filelib-web/src/lib/Avatar.svelte | 31 ++++ hub/filelib-web/src/lib/FileEditor.svelte | 29 ++-- hub/filelib-web/src/lib/FilesPanel.svelte | 44 ++--- hub/filelib-web/src/lib/Icon.svelte | 41 +++++ 5 files changed, 309 insertions(+), 37 deletions(-) create mode 100644 hub/filelib-web/src/lib/Avatar.svelte create mode 100644 hub/filelib-web/src/lib/Icon.svelte diff --git a/hub/filelib-web/src/app.css b/hub/filelib-web/src/app.css index 1beee79..526846b 100644 --- a/hub/filelib-web/src/app.css +++ b/hub/filelib-web/src/app.css @@ -23,8 +23,7 @@ } html, -body, -#app { +body { height: 100%; } @@ -47,6 +46,204 @@ body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } +/* 共享组件层(ADR-0029)。 + * + * 从已删除的 hub/src/database/routes/uiTheme.ts 原样搬来。组件只带布局工具类, + * 不重述这里的组件样式 —— 第一版迁移只搬了上面的 @theme 令牌,把按钮/输入框/ + * 面板样式在每个组件里内联重写了一遍,后台随即明显退化。 */ +@layer components { + .btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 6px 14px; + border-radius: 8px; + border: 1px solid var(--color-line); + background: var(--color-panel); + color: var(--color-ink); + font-size: 12.5px; + font-weight: 500; + cursor: pointer; + transition: all 120ms ease; + white-space: nowrap; + } + .btn:hover { + background: var(--color-hover); + } + .btn-sm { + padding: 4px 9px; + font-size: 11.5px; + } + .btn-primary { + background: var(--color-accent); + border-color: var(--color-accent); + color: #fff; + } + .btn-primary:hover { + background: var(--color-accent-hover); + border-color: var(--color-accent-hover); + } + .btn-danger { + border-color: transparent; + background: transparent; + color: var(--color-danger); + } + .btn-danger:hover { + background: color-mix(in srgb, var(--color-danger) 7%, transparent); + } + + .panel { + background: var(--color-panel); + border: 1px solid var(--color-line-soft); + border-radius: 10px; + padding: 20px 22px; + } + + .tag { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 10.5px; + font-weight: 500; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--color-line-soft); + color: var(--color-ink-3); + background: var(--color-panel); + } + + .input, + .select, + .textarea { + width: 100%; + padding: 7px 11px; + border-radius: 8px; + border: 1px solid var(--color-line); + background: var(--color-panel); + font-size: 13px; + color: var(--color-ink); + font-family: inherit; + outline: none; + transition: border-color 120ms ease; + } + .input:focus, + .select:focus, + .textarea:focus { + border-color: var(--color-accent); + } + .textarea { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12.5px; + line-height: 1.75; + resize: vertical; + } + .form-label { + display: block; + font-size: 11.5px; + color: var(--color-ink-3); + margin-bottom: 4px; + } + .form-row { + margin-bottom: 12px; + } + + table.list { + width: 100%; + border-collapse: collapse; + font-size: 13px; + } + table.list th { + text-align: left; + font-size: 11.5px; + font-weight: 500; + color: var(--color-ink-3); + padding: 4px 0; + } + table.list td { + padding: 8px 0; + border-top: 1px solid var(--color-line-soft); + } + table.list tr:first-child td { + border-top: none; + } + + .quiet { + color: var(--color-ink-3); + font-size: 12.5px; + } + .section-title { + font-size: 13px; + font-weight: 600; + } + .section-note { + font-size: 11.5px; + color: var(--color-ink-3); + } + .file-meta { + font-size: 11px; + color: var(--color-ink-3); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + } + .link-danger { + color: var(--color-danger); + font-size: 12.5px; + background: none; + border: none; + cursor: pointer; + padding: 0; + } + .link-danger:hover { + text-decoration: underline; + } + + /* 开关。真 checkbox 藏在下面 —— 保留键盘可达与 :checked 语义,不做 div 假开关。 */ + .switch { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + } + .switch > input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + } + .switch > span { + position: relative; + flex-shrink: 0; + width: 30px; + height: 17px; + border-radius: 999px; + background: var(--color-line); + transition: background 0.16s; + } + .switch > span::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 13px; + height: 13px; + border-radius: 50%; + background: #fff; + transition: transform 0.16s; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + } + .switch > input:checked + span { + background: var(--color-accent); + } + .switch > input:checked + span::after { + transform: translateX(13px); + } + .switch > input:focus-visible + span { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + } +} + /* diff 渲染 */ pre.diff { white-space: pre-wrap; diff --git a/hub/filelib-web/src/lib/Avatar.svelte b/hub/filelib-web/src/lib/Avatar.svelte new file mode 100644 index 0000000..fd1ad7c --- /dev/null +++ b/hub/filelib-web/src/lib/Avatar.svelte @@ -0,0 +1,31 @@ + + +{#if avatarUrl} + +{:else} + +{/if} diff --git a/hub/filelib-web/src/lib/FileEditor.svelte b/hub/filelib-web/src/lib/FileEditor.svelte index a046e1e..8339366 100644 --- a/hub/filelib-web/src/lib/FileEditor.svelte +++ b/hub/filelib-web/src/lib/FileEditor.svelte @@ -3,6 +3,7 @@ import { toastOk, toastErr, toast } from "./stores.js"; import type { FileContent, VersionInfo, Role } from "./types.js"; import Modal from "./Modal.svelte"; + import Icon from "./Icon.svelte"; let { projectId, path, role, onchanged, onclose }: { projectId: string; path: string; role: Role; onchanged: () => void; onclose?: () => void } = $props(); @@ -104,32 +105,32 @@ {#if loadError} -
{loadError}
+
{loadError}
{:else if file} -
+
- {file.path} @ {file.version} -
- 下载 - + {file.path} @ {file.version} +
+ 下载 + {#if canEdit} - + {/if} {#if onclose} - + {/if}
{#if file.encoding === "base64"} -
二进制文件({file.size} B),不支持在线编辑
+
二进制文件({file.size} B),不支持在线编辑
{:else} - + {/if} {#if canEdit && file.encoding !== "base64"}
- +
{/if} @@ -139,13 +140,13 @@
{@html renderDiff(conflict.diff)}
请人工合并后,以最新内容为全文重新提交(基版将更新为 {conflict.currentVersion})
- +
{/if}
{:else} -
加载中…
+
加载中…
{/if} {#if showHistory} @@ -159,7 +160,7 @@ {/each}
- +
{/if} diff --git a/hub/filelib-web/src/lib/FilesPanel.svelte b/hub/filelib-web/src/lib/FilesPanel.svelte index 8d3b7ca..0806afd 100644 --- a/hub/filelib-web/src/lib/FilesPanel.svelte +++ b/hub/filelib-web/src/lib/FilesPanel.svelte @@ -4,6 +4,7 @@ import { selectedFilePath, filesVersion } from "./browser.js"; import type { FileEntry, NodeDetail } from "./types.js"; import Modal from "./Modal.svelte"; + import Icon from "./Icon.svelte"; let { node }: { node: NodeDetail } = $props(); @@ -12,7 +13,8 @@ let showNewFile = $state(false); let newPath = $state(""); let newContent = $state(""); - let uploadInput: HTMLInputElement; + // bind:this 的目标要用 $state,否则 svelte 5 warn 不会正确更新。 + let uploadInput = $state(null); const canEdit = $derived(node.role !== "VIEW"); @@ -84,34 +86,34 @@ } -
-
-
项目文件({files?.length ?? 0})
+
+
+
项目文件({files?.length ?? 0})
{#if canEdit} -
- - +
+ +
{/if}
{#if files === null && loadError === null} -
加载中…
+
加载中…
{:else if loadError}
{loadError}
{:else if files && files.length === 0} -
空仓库 · 可新建或上传文件
+
空仓库 · 可新建或上传文件
{:else if files} - +
{#each files as f (f.path)} selectedFilePath.set(f.path)} > - - + + {/each} @@ -121,17 +123,17 @@ {#if showNewFile} (showNewFile = false)}> -
- - +
+ +
-
- - +
+ +
- - + +
{/if} diff --git a/hub/filelib-web/src/lib/Icon.svelte b/hub/filelib-web/src/lib/Icon.svelte new file mode 100644 index 0000000..7c98aed --- /dev/null +++ b/hub/filelib-web/src/lib/Icon.svelte @@ -0,0 +1,41 @@ + + + + + From eeb8f56742441a7e2fdc4ade55c8a0cc693ea466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:19:30 +0800 Subject: [PATCH 11/14] =?UTF-8?q?fix(filelib-web):=20=E8=A1=A5=E9=BD=90=20?= =?UTF-8?q?Group=20=E7=AE=A1=E7=90=86=E9=9D=A2=E6=9D=BF,=E4=B8=8E=E6=97=A7?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=9D=A2=E6=9D=BF=E9=80=90=E6=9D=A1=E5=AF=B9?= =?UTF-8?q?=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移时误把分支上一个早先存在的简易 GroupAdmin(281 行)当成迁移产物, 它与旧 renderGroupsPanel(747 行)从来不是同一个东西,于是后端 8 个 group 端点前端只调了 5 个。 补上的功能(端点一直可用,只是没有入口): PATCH /groups/:id 重命名 / 改描述 GET /groups?includeArchived=1 列出已归档组 POST /groups/:id/restore 恢复(连带恢复已归档祖先链,子树仍归档) GET /users/search 成员选择器,不再手敲 userId 影响最实际的是恢复:软删的组此前在界面上无法恢复。 补上的交互:折叠树、组名过滤(命中项保留整条祖先链,过滤态强制展开)、 右键菜单(归档组只给「恢复」)、面包屑、统计条、树底部计数、成员表的 头像/openId/加入时间三列。 types.ts 之前也是截断的:MemberGroupNode 少 archivedAt, MemberGroupMember 少 feishuOpenId/avatarUrl/joinedAt —— 类型里没有, UI 自然渲染不出来。 一处实现偏离:折叠状态用数组而非 Set。Svelte 5 的 $state 深层代理不 跟踪 Set 变更,用 Set 会点了没反应。 groups tab 外框补 padding:20px/overflow:hidden,对齐旧 #tab-groups, 否则面板贴着侧边栏。 --- hub/filelib-web/src/lib/GroupAdmin.svelte | 766 ++++++++++++++++++---- hub/filelib-web/src/lib/types.ts | 64 ++ 2 files changed, 688 insertions(+), 142 deletions(-) diff --git a/hub/filelib-web/src/lib/GroupAdmin.svelte b/hub/filelib-web/src/lib/GroupAdmin.svelte index 8eaea46..4577c88 100644 --- a/hub/filelib-web/src/lib/GroupAdmin.svelte +++ b/hub/filelib-web/src/lib/GroupAdmin.svelte @@ -1,78 +1,174 @@ -
- - - -
+
{loaded ? treeFoot : ""}
+
+ + +
{#if selected === null} -
从左侧选择一个 Group 查看成员
+
+ + 从左侧选择一个 Group 查看成员 +
{:else} -
{selected.name}
- {#if selected.description} -

{selected.description}

- {:else} -
+ {#if isArchived} + +
+ + + 此 Group 已删除于 {fmtDate(selected.archivedAt ?? "")} · 成员只读,不再授予任何权限 + + +
{/if} -
-
- - { if (e.key === "Enter") void addMember(); }} - /> +
+
+ {#each chain as c, i (c.id)} + {#if i > 0}/{/if} + {c.name} + {/each} +
+ +
+ + + +
+
{selected.name}
+ {#if selected.description !== null && selected.description !== ""} +
{selected.description}
+ {:else} +
无描述
+ {/if} +
+ + {#if !isArchived} + + + {/if} +
+ +
+ {members.length} 名成员 + 层级 {selected.depth} + {childCount} 个子组
-
-
-
- 成员 {members === null ? "" : `(${members.length})`} +
+
+ + + +
- {#if members === null} -
加载中…
- {:else if membersError} -
{membersError}
+ + {memberFilter.trim() === "" ? "" : `${shownMembers.length} / ${members.length}`} + +
+ +
+ {#if !membersLoaded} +
加载中…
+ {:else if membersError !== null} +
{membersError}
{:else if members.length === 0} -
暂无成员
+
+ + {isArchived ? "此组无成员记录" : "此组暂无成员 · 点右上「添加成员」"} +
+ {:else if shownMembers.length === 0} +
无匹配成员
{:else} - {#each members as m (m.userId)} -
- {m.displayName} - {m.userId} - -
- {/each} +
{f.path}{f.size} B{f.path}{f.size} B
+ + + + + + + {#if !isArchived}{/if} + + + + {#each shownMembers as m (m.userId)} + + + + + + {#if !isArchived} + + {/if} + + {/each} + +
成员userId飞书 openId加入时间操作
+ + + {m.displayName || "(未命名)"} + + {m.userId}{m.feishuOpenId || "—"}{fmtDate(m.joinedAt)} + +
{/if}
{/if} - +
+ +{#if menu !== null} + +{/if} + {#if showCreate} - (showCreate = false)}> -
- - + (showCreate = false)} + > +
+ +
-
- - +
+ +
- - + + +
+ +{/if} + +{#if showRename && renameTarget !== null} + (showRename = false)}> +
+ + +
+
+ + +
+
+ + +
+
+{/if} + +{#if showAdd && selected !== null} + (showAdd = false)}> +
+ +
+ { + if (e.key === "Enter") void searchUsers(); + }} + /> + +
+
已在本组的成员不会出现在结果里。
+
+ +
+ {#if addSearching} +
搜索中…
+ {:else if addResults.length === 0} +
无候选用户 · 先点「搜索」
+ {:else} + {#each addResults as u (u.userId)} +
+ +
+
{u.displayName || "(未命名)"}
+
{u.feishuOpenId || u.userId}
+
+ +
+ {/each} + {/if} +
+ +
+
{/if} diff --git a/hub/filelib-web/src/lib/types.ts b/hub/filelib-web/src/lib/types.ts index 22e5179..f0bffd6 100644 --- a/hub/filelib-web/src/lib/types.ts +++ b/hub/filelib-web/src/lib/types.ts @@ -36,6 +36,9 @@ export interface NodeDetail { export interface MeResponse { readonly userId: string; readonly isWebsiteAdmin: boolean; + /** 侧栏身份区显示用;后端取不到 User 行时回落为 userId。 */ + readonly displayName: string; + readonly avatarUrl: string | null; } export interface FileEntry { @@ -92,9 +95,70 @@ export interface MemberGroupNode { readonly description: string | null; readonly depth: number; readonly memberCount: number; + /** 软删标记(ADR-0028 决策4)。null = 活跃;非 null = 已归档,不贡献任何权限。 + * 仅在 ?includeArchived=1 时可能非 null。ISO 串(后端 JSON 序列化后不再是 Date)。 */ + readonly archivedAt: string | null; } export interface MemberGroupMember { readonly userId: string; readonly displayName: string; + readonly feishuOpenId: string; + readonly avatarUrl: string | null; + /** 加入本组时间;ISO 串。 */ + readonly joinedAt: string; +} + +/** 节点授权(GET /database/api/nodes/:id/grants)。 */ +export interface Grant { + readonly id: string; + readonly principalType: "USER" | "GROUP"; + readonly principalId: string; + readonly role: Role; + /** 创建者授权不可收回、不可改(契约 8.1)。 */ + readonly isCreatorGrant: boolean; + readonly createdAt: string; +} + +/** Group 选择器候选(GET /database/api/groups/search)。 */ +export interface MemberGroupSearchResult { + readonly id: string; + readonly name: string; + /** 祖先链(根在前,自身在末),用 " / " 连接。 */ + readonly breadcrumb: string; +} + +/** 成员选择器候选(GET /database/api/users/search)。 */ +export interface UserSearchResult { + readonly userId: string; + readonly displayName: string; + readonly feishuOpenId: string; + readonly avatarUrl: string | null; +} + +/** 管理后台概览统计(GET /database/api/stats)。 */ +export interface DashboardStats { + readonly folders: number; + readonly projects: number; + readonly files: number; + readonly grants: number; + readonly recent: ReadonlyArray<{ + readonly action: string; + readonly actor: string; + readonly label: string; + /** ISO 串;后端 JSON 序列化后不再是 Date。 */ + readonly when: string; + }>; +} + +/** org 成员(GET /api/org/:slug/members);用户管理面板消费。 */ +export type OrgRole = "OWNER" | "ADMIN" | "MEMBER"; + +export interface OrgMember { + readonly userId: string; + readonly feishuOpenId: string; + readonly displayName: string; + readonly avatarUrl: string | null; + readonly role: OrgRole; + readonly createdAt: string; } From cdeb29ccf24f976f4b0469d57fbee303e64f4b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:19:47 +0800 Subject: [PATCH 12/14] =?UTF-8?q?fix(database):=20=E8=A1=A5=E5=9B=9E?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=BA=93=E7=9A=84=E3=80=8C=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E3=80=8Dtab=20=E4=B8=8E=20/me=20=E7=9A=84=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移时整个授权 tab 连同四个端点一起漏掉了 —— 后端一直可用,前端零调用: GET/PUT/DELETE /nodes/:id/grants PUT /projects/:id/independent-permission 权限编辑是这个后台的核心用途,而它此前在界面上完全不可达。 tab 组装也修正为与旧 libraryBrowser 一致:概览恒有、文件仅 PROJECT、 授权仅 MANAGE。注意文件夹也有授权 tab —— 它虽是透明组织节点,授权仍 挂在节点上(ADR-0021);此前文件夹一个 tab 都没有。 GrantsPanel 的语义按契约 8.1:创建者授权不给收回入口;MANAGE 仅创建者 可授,前端不拦,后端 fail closed 的报错原样呈现;GROUP 主体走 /groups/search 下拉选,不手敲 id。 /database/api/me 加 displayName 与 avatarUrl:侧栏此前显示原始 userId。 旧页面是服务端渲染,handler 里查 Prisma 就有名字;页面不再服务端渲染后 (ADR-0029),模板闭包过的数据也是被迁移的契约的一部分,不是旧实现的 无关细节。 概览面板同时补回丢失的「类型」「更新时间」两行、导出 target 下拉、 节点标题旁的角色 tag,以及整块缺失的独立权限开关。 --- hub/filelib-web/src/lib/GrantsPanel.svelte | 191 ++++++++++++++++++ .../src/lib/NodeDetailPanel.svelte | 83 +++++--- hub/filelib-web/src/lib/OverviewPanel.svelte | 82 +++++--- hub/src/database/routes/filelibRoutes.ts | 14 +- 4 files changed, 312 insertions(+), 58 deletions(-) create mode 100644 hub/filelib-web/src/lib/GrantsPanel.svelte diff --git a/hub/filelib-web/src/lib/GrantsPanel.svelte b/hub/filelib-web/src/lib/GrantsPanel.svelte new file mode 100644 index 0000000..3199707 --- /dev/null +++ b/hub/filelib-web/src/lib/GrantsPanel.svelte @@ -0,0 +1,191 @@ + + +
+ {#if error !== null} +
{error}
+ {:else if grants === null} +
加载中…
+ {:else} + + + + + + {#if grants.length === 0} + + {:else} + {#each grants as g (g.id)} + + + + + + {/each} + {/if} + +
主体级别
暂无显式授权
+ + + {g.principalId} + {#if g.isCreatorGrant}(创建者){/if} + + {g.role} + + {#if !g.isCreatorGrant && canManage} + + {/if} +
+ {/if} + + {#if canManage} +
+
新增授权
+
+ + + {#if principalType === "USER"} + + {:else if groupOptions === null} + 加载 Group 列表… + {:else if groupOptions.length === 0} + 暂无可选 Group · 先到「Group 管理」建一个 + {:else} + + {/if} + + + +
+
MANAGE 仅创建者可授;创建者授权不可动(契约 8.1)
+ {/if} + +
diff --git a/hub/filelib-web/src/lib/NodeDetailPanel.svelte b/hub/filelib-web/src/lib/NodeDetailPanel.svelte index 13209d3..fe437c2 100644 --- a/hub/filelib-web/src/lib/NodeDetailPanel.svelte +++ b/hub/filelib-web/src/lib/NodeDetailPanel.svelte @@ -4,9 +4,12 @@ import { toastOk, toastErr } from "./stores.js"; import OverviewPanel from "./OverviewPanel.svelte"; import FilesPanel from "./FilesPanel.svelte"; + import GrantsPanel from "./GrantsPanel.svelte"; import Modal from "./Modal.svelte"; + import Icon from "./Icon.svelte"; - let tab = $state<"detail" | "files">("detail"); + type Tab = "detail" | "files" | "grants"; + let tab = $state("detail"); let showCreateChild = $state(false); let newName = $state(""); let newKind = $state<"FOLDER" | "PROJECT">("FOLDER"); @@ -17,6 +20,15 @@ const canManage = $derived(node?.role === "MANAGE"); const canEdit = $derived(canManage || node?.role === "EDIT"); + // 与旧 libraryBrowser 的 tab 组装一致:概览恒有;文件仅 PROJECT;授权仅 MANAGE + // (FOLDER 也有授权 —— 它虽是透明组织节点,授权仍挂在节点上,ADR-0021)。 + const tabs = $derived.by((): ReadonlyArray => { + const out: Array = [["detail", "概览"]]; + if (node?.kind === "PROJECT") out.push(["files", "文件"]); + if (canManage) out.push(["grants", "授权"]); + return out; + }); + $effect(() => { void node?.id; tab = "detail"; @@ -84,36 +96,43 @@
-
+
{node.name} - {node.kind === "PROJECT" ? "项目" : "文件夹"} + {node.kind === "PROJECT" ? "项目" : "文件夹"} + {node.role}
-
+
{#if canEdit && node.kind === "FOLDER"} - + {/if} {#if canManage} - - + + {/if}
- {#if node.kind === "FOLDER"} -
点击左侧树展开以浏览子内容
+
+ {#each tabs as [id, label] (id)} + + {/each} +
+ + {#if tab === "grants"} + + {:else if tab === "files" && node.kind === "PROJECT"} + {:else} -
- {#each [["detail", "概览"], ["files", "文件"]] as [id, label] (id)} - - {/each} -
- {#if tab === "detail"} - - {:else} - + + {#if node.kind === "FOLDER"} +
文件夹是透明组织节点,点左侧树展开以浏览子内容。
{/if} {/if}
@@ -121,24 +140,24 @@ {#if showCreateChild && node} (showCreateChild = false)}> -
- - +
+ +
-
- -
-
- - +
+ +
- - + +
{/if} diff --git a/hub/filelib-web/src/lib/OverviewPanel.svelte b/hub/filelib-web/src/lib/OverviewPanel.svelte index cf5a6f7..3c3b31d 100644 --- a/hub/filelib-web/src/lib/OverviewPanel.svelte +++ b/hub/filelib-web/src/lib/OverviewPanel.svelte @@ -12,8 +12,25 @@ let exportJob = $state(null); const canEdit = $derived(node.role === "MANAGE" || node.role === "EDIT"); + const canManage = $derived(node.role === "MANAGE"); const roleLabel = $derived(node.role === "MANAGE" ? "可管理" : node.role === "EDIT" ? "可编辑" : "只读"); + /** 独立权限开关(仅 PROJECT;关闭时只继承父级权限,创建者除外)。 */ + async function toggleIndependent(): Promise { + try { + await api(`/database/api/projects/${node.id}/independent-permission`, { + method: "PUT", + body: { enabled: !node.independentPermission }, + }); + toastOk("已切换"); + currentNode.update((n) => + n !== null && n.id === node.id ? { ...n, independentPermission: !node.independentPermission } : n, + ); + } catch (e) { + toastErr(e instanceof Error ? e.message : String(e)); + } + } + $effect(() => { void node.id; exportJob = null; @@ -67,7 +84,7 @@ } -
+
简介
@@ -77,7 +94,7 @@ 暂无简介 {/if} {#if canEdit} - + {/if}
@@ -85,39 +102,54 @@
+
类型 {node.kind === "PROJECT" ? "项目" : "文件夹"}
我的角色 {roleLabel}
-
创建时间 {new Date(node.createdAt).toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" })}
-
最近修改 {new Date(node.updatedAt).toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" })}
+
创建时间 {new Date(node.createdAt).toLocaleString("zh-CN")}
+
更新时间 {new Date(node.updatedAt).toLocaleString("zh-CN")}
-
+ + {#if node.kind === "PROJECT"} +
+
+ 独立权限 + {node.independentPermission ? "开启" : "关闭"} + {#if canManage} + + {/if} + 关闭时仅继承父级权限(创建者除外) +
-
导出
-
- - {#if exportJob} - - {#if exportJob.status === "DONE"} - 完成 · 下载 - {:else if exportJob.status === "FAILED"} - 失败:{exportJob.error ?? ""} - {:else} - {exportJob.status}… - {/if} - - {/if} -
+
+ +
导出
+
+ + + {#if exportJob} + + {#if exportJob.status === "DONE"} + 完成 · 下载 + {:else if exportJob.status === "FAILED"} + 失败:{exportJob.error ?? ""} + {:else} + {exportJob.status}… + {/if} + + {/if} +
+ {/if}
{#if showEditDesc} (showEditDesc = false)}> -
- - +
+ +
- - + +
{/if} diff --git a/hub/src/database/routes/filelibRoutes.ts b/hub/src/database/routes/filelibRoutes.ts index 3ce1bbc..39cd339 100644 --- a/hub/src/database/routes/filelibRoutes.ts +++ b/hub/src/database/routes/filelibRoutes.ts @@ -44,10 +44,22 @@ export async function registerFileLibRoutes( /* ------------------------------------------------------------ 身份 */ // 前端判断能力面用:是否网站管理员(root 创建按钮显隐)。 + // displayName/avatarUrl 供侧栏身份区显示 —— 页面不再服务端渲染(ADR-0029), + // 旧 renderDashboard 在 handler 里查 Prisma 拿到的名字,现在必须由这里带出去, + // 否则前端只有 userId 可显示。 app.get("/database/api/me", async (request, reply) => { const actor = await actorOrNull(request, reply, deps); if (actor === null) return reply; - return { userId: actor.userId, isWebsiteAdmin: actor.isWebsiteAdmin }; + const user = await deps.prisma.user.findUnique({ + where: { id: actor.userId }, + select: { displayName: true, avatarUrl: true }, + }); + return { + userId: actor.userId, + isWebsiteAdmin: actor.isWebsiteAdmin, + displayName: user?.displayName ?? actor.userId, + avatarUrl: user?.avatarUrl ?? null, + }; }); /* ------------------------------------------------------------ 树节点 */ From 69900822477a8e9b17d86d1e97416998e0713c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:23:15 +0800 Subject: [PATCH 13/14] =?UTF-8?q?build(deploy):=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E4=B8=8E=E9=99=90=E6=B5=81=E9=85=8D=E7=BD=AE=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=88=B0=20filelib-web,=E5=B9=B6=E5=8A=A0=E5=85=B1=E5=AD=98?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处引用旧工程名/旧资源路径的地方一并更新,它们必须同时改 —— 少改一处 就是静默故障,而不是构建期报错: 1. 部署脚本(deploy_platform.sh / deploy_fleet_release.sh):npm ci 的 prefix、rsync 排除项、构建产物存在性检查从 database-admin 换成 filelib-web。最后一项是真门禁:static.ts 缺产物时只 warn 不注册路由, 漏改会让 /app 与 /database 静默 404 —— 恰是 database-admin 长期处于 禁用状态的原因。 2. silo 限流豁免:资源路径随 appDir 改名而变(/database/_app/* 已不存在, 现为 /_filelib/*);/app/* 此前不在豁免列表,它现在也是 SPA 外壳, 客户端路由无法预先枚举。 注:/database/* 是整体豁免,filelib 的 JSON API 也绕过限流预算。这是 迁移前就有的行为,原样保留,但覆盖面因多了 /app/* 而变宽。 3. 回归测试:把 registerStaticSpa 与 registerDatabaseSpa 挂到同一个 Fastify 实例,断言 ready() 不因重复路由抛错 —— appDir 若用回默认的 _app,这里会红(ADR-0029 的承重约束)。另断言 /app 与 /database/dashboard/users 返回同一份字节(SPA 回退不读请求)、body 含 /_filelib/。构建产物缺失时不 skip 而是直接失败:那说明该先跑 filelib-web 的 build,不是测试不适用。 --- hub/deploy/deploy_fleet_release.sh | 8 ++-- hub/deploy/deploy_platform.sh | 6 +-- hub/src/deployment/siloRateLimit.ts | 14 +++---- hub/test/unit/silo-rate-limit.test.ts | 11 +++++ hub/test/unit/spa-route-registration.test.ts | 43 ++++++++++++++++++++ 5 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 hub/test/unit/spa-route-registration.test.ts diff --git a/hub/deploy/deploy_fleet_release.sh b/hub/deploy/deploy_fleet_release.sh index 53d2148..2d52e9b 100755 --- a/hub/deploy/deploy_fleet_release.sh +++ b/hub/deploy/deploy_fleet_release.sh @@ -82,11 +82,11 @@ REMOTE rsync -az --delete \ --exclude node_modules --exclude dist --exclude .env \ --exclude admin-web/node_modules --exclude admin-web/build --exclude admin-web/.svelte-kit \ - --exclude database-admin/node_modules --exclude database-admin/build --exclude database-admin/.svelte-kit \ + --exclude filelib-web/node_modules --exclude filelib-web/build --exclude filelib-web/.svelte-kit \ -e "ssh ${SSH_OPTS[*]}" \ "$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/" - echo "[fleet] npm ci + build (tsc + admin-web & database-admin SPAs)" + echo "[fleet] npm ci + build (tsc + admin-web & filelib-web SPAs)" ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" bash -s < { expect(isSiloHttpRateLimitExempt("/admin/org/para-26071100/members")).toBe(true); }); + it("exempts the filelib-web SPA assets and both of its mount prefixes", () => { + // appDir 改名为 `_filelib`(见 src/database/static.ts):同一份产物服务 + // 老师端 /app 与管理后台 /database。 + expect(isSiloHttpRateLimitExempt("/_filelib/version.json")).toBe(true); + expect(isSiloHttpRateLimitExempt("/_filelib/immutable/chunks/foo.js?v=1")).toBe(true); + expect(isSiloHttpRateLimitExempt("/app")).toBe(true); + expect(isSiloHttpRateLimitExempt("/app/dev-login-teacher")).toBe(true); + expect(isSiloHttpRateLimitExempt("/database")).toBe(true); + expect(isSiloHttpRateLimitExempt("/database/dashboard/users")).toBe(true); + }); + it("still rate-limits APIs and auth", () => { expect(isSiloHttpRateLimitExempt("/api/me")).toBe(false); expect(isSiloHttpRateLimitExempt("/api/org/x/members")).toBe(false); diff --git a/hub/test/unit/spa-route-registration.test.ts b/hub/test/unit/spa-route-registration.test.ts new file mode 100644 index 0000000..38f6cdb --- /dev/null +++ b/hub/test/unit/spa-route-registration.test.ts @@ -0,0 +1,43 @@ +/** + * 两套 SPA 静态托管共存于同一个 Fastify 实例。 + * + * 这是 ADR-0029 的承重约束的回归测试:filelib-web 的 appDir 若用 SvelteKit 默认的 + * `_app`,就会与 admin-web 在根上注册的 `/_app/*` 撞成重复路由,Fastify 启动即抛错。 + * 改名为 `_filelib` 后两者共存 —— 这里把两个注册函数挂到同一实例上验证。 + */ +import Fastify from "fastify"; +import { describe, expect, it } from "vitest"; +import { registerStaticSpa } from "../../src/admin/static.js"; +import { registerDatabaseSpa } from "../../src/database/static.js"; + +describe("SPA static route registration", () => { + it("admin-web and filelib-web shells coexist without duplicate routes", async () => { + const app = Fastify({ logger: false }); + await registerStaticSpa(app); + await registerDatabaseSpa(app); + await expect(app.ready()).resolves.toBeDefined(); + await app.close(); + }); + + it("serves the same index.html at /app and /database, and assets under /_filelib", async () => { + const app = Fastify({ logger: false }); + await registerDatabaseSpa(app); + await app.ready(); + + const appShell = await app.inject({ method: "GET", url: "/app" }); + const dbShell = await app.inject({ method: "GET", url: "/database/dashboard/users" }); + + // 前置:构建产物必须存在。缺失时 registerDatabaseSpa 只 warn 不注册路由, + // 断言会全部落到 404 —— 那说明该先跑 `npm run build --prefix filelib-web`, + // 不是测试不适用,所以这里不跳过而是直接失败。 + expect(appShell.statusCode).toBe(200); + expect(dbShell.statusCode).toBe(200); + // 同一份字节:SPA 回退不读请求(ADR-0029)。 + expect(dbShell.body).toBe(appShell.body); + expect(appShell.headers["content-type"]).toContain("text/html"); + // 资源用绝对路径引用(paths.relative=false),否则深层 URL 下会解析错。 + expect(appShell.body).toContain("/_filelib/"); + + await app.close(); + }); +}); From 8a13e455fba8f4160e26c01bfc6d3ea5d9d821cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:29:59 +0800 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=20.omo/=20?= =?UTF-8?q?=E4=B8=8E=E6=96=87=E4=BB=B6=E5=BA=93-=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=A5=91=E7=BA=A6.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .omo/ 下 12 个 run-continuation/ses_*.json 是 agent 会话续跑状态, 机器生成,本不该进版本库;文件库-开工计划.md 一并删除。 《文件库-接口契约.md》(C/D 编号)同时删除。两份文档的内容都可从 git 历史取回。 代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等)因此不再有在库 文档可查,分布在 filelib 的 model / grantService / treeService / guards、prisma schema 与迁移、以及 ADR-0028。README 原先按路径引用 这两份文档,现改为说明出处与取回方式。 --- .../ses_0705f8afbffePWIq7GrFfwfFwV.json | 10 - .../ses_07252990dffeRFmXnvdyynty2z.json | 10 - .../ses_075d214a0ffe6mU8VNgukUfH5L.json | 10 - .../ses_075d3282bffejz0HFE00taOaGM.json | 10 - .../ses_0781484caffeKJHXVKiD4BqEJw.json | 10 - .../ses_07b163c13ffeudULxSfIfK986T.json | 10 - .../ses_07b1e6c8effeq7bdmj6NgO0LqC.json | 10 - .../ses_07b932dddffecSipFLwlJOvoJ9.json | 10 - .../ses_07b9c4268ffeic4mMXCPUt225j.json | 10 - .../ses_07b9c4346ffeVBdV5pf4h6s9PW.json | 10 - .../ses_07b9c9a75ffeOyJ4ewp3DBvXhz.json | 10 - .../ses_07cdbac0effeiBrdcHLUUUfwuF.json | 10 - .omo/文件库-开工计划.md | 114 ------- hub/src/database/README.md | 8 +- 文件库-接口契约.md | 304 ------------------ 15 files changed, 5 insertions(+), 541 deletions(-) delete mode 100644 .omo/run-continuation/ses_0705f8afbffePWIq7GrFfwfFwV.json delete mode 100644 .omo/run-continuation/ses_07252990dffeRFmXnvdyynty2z.json delete mode 100644 .omo/run-continuation/ses_075d214a0ffe6mU8VNgukUfH5L.json delete mode 100644 .omo/run-continuation/ses_075d3282bffejz0HFE00taOaGM.json delete mode 100644 .omo/run-continuation/ses_0781484caffeKJHXVKiD4BqEJw.json delete mode 100644 .omo/run-continuation/ses_07b163c13ffeudULxSfIfK986T.json delete mode 100644 .omo/run-continuation/ses_07b1e6c8effeq7bdmj6NgO0LqC.json delete mode 100644 .omo/run-continuation/ses_07b932dddffecSipFLwlJOvoJ9.json delete mode 100644 .omo/run-continuation/ses_07b9c4268ffeic4mMXCPUt225j.json delete mode 100644 .omo/run-continuation/ses_07b9c4346ffeVBdV5pf4h6s9PW.json delete mode 100644 .omo/run-continuation/ses_07b9c9a75ffeOyJ4ewp3DBvXhz.json delete mode 100644 .omo/run-continuation/ses_07cdbac0effeiBrdcHLUUUfwuF.json delete mode 100644 .omo/文件库-开工计划.md delete mode 100644 文件库-接口契约.md diff --git a/.omo/run-continuation/ses_0705f8afbffePWIq7GrFfwfFwV.json b/.omo/run-continuation/ses_0705f8afbffePWIq7GrFfwfFwV.json deleted file mode 100644 index 35dd765..0000000 --- a/.omo/run-continuation/ses_0705f8afbffePWIq7GrFfwfFwV.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_0705f8afbffePWIq7GrFfwfFwV", - "updatedAt": "2026-07-23T15:40:15.630Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-23T15:40:15.630Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07252990dffeRFmXnvdyynty2z.json b/.omo/run-continuation/ses_07252990dffeRFmXnvdyynty2z.json deleted file mode 100644 index 13afc86..0000000 --- a/.omo/run-continuation/ses_07252990dffeRFmXnvdyynty2z.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07252990dffeRFmXnvdyynty2z", - "updatedAt": "2026-07-23T06:33:33.086Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-23T06:33:33.086Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_075d214a0ffe6mU8VNgukUfH5L.json b/.omo/run-continuation/ses_075d214a0ffe6mU8VNgukUfH5L.json deleted file mode 100644 index 7c08344..0000000 --- a/.omo/run-continuation/ses_075d214a0ffe6mU8VNgukUfH5L.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_075d214a0ffe6mU8VNgukUfH5L", - "updatedAt": "2026-07-22T14:15:01.741Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-22T14:15:01.741Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_075d3282bffejz0HFE00taOaGM.json b/.omo/run-continuation/ses_075d3282bffejz0HFE00taOaGM.json deleted file mode 100644 index 1ab24cd..0000000 --- a/.omo/run-continuation/ses_075d3282bffejz0HFE00taOaGM.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_075d3282bffejz0HFE00taOaGM", - "updatedAt": "2026-07-22T14:13:54.785Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-22T14:13:54.785Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_0781484caffeKJHXVKiD4BqEJw.json b/.omo/run-continuation/ses_0781484caffeKJHXVKiD4BqEJw.json deleted file mode 100644 index 5220743..0000000 --- a/.omo/run-continuation/ses_0781484caffeKJHXVKiD4BqEJw.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_0781484caffeKJHXVKiD4BqEJw", - "updatedAt": "2026-07-22T06:17:18.238Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-22T06:17:18.238Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b163c13ffeudULxSfIfK986T.json b/.omo/run-continuation/ses_07b163c13ffeudULxSfIfK986T.json deleted file mode 100644 index a20e325..0000000 --- a/.omo/run-continuation/ses_07b163c13ffeudULxSfIfK986T.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b163c13ffeudULxSfIfK986T", - "updatedAt": "2026-07-21T13:55:58.878Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T13:55:58.878Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b1e6c8effeq7bdmj6NgO0LqC.json b/.omo/run-continuation/ses_07b1e6c8effeq7bdmj6NgO0LqC.json deleted file mode 100644 index e11862c..0000000 --- a/.omo/run-continuation/ses_07b1e6c8effeq7bdmj6NgO0LqC.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b1e6c8effeq7bdmj6NgO0LqC", - "updatedAt": "2026-07-21T13:38:01.132Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T13:38:01.132Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b932dddffecSipFLwlJOvoJ9.json b/.omo/run-continuation/ses_07b932dddffecSipFLwlJOvoJ9.json deleted file mode 100644 index c0350d6..0000000 --- a/.omo/run-continuation/ses_07b932dddffecSipFLwlJOvoJ9.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b932dddffecSipFLwlJOvoJ9", - "updatedAt": "2026-07-21T11:27:10.420Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T11:27:10.420Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b9c4268ffeic4mMXCPUt225j.json b/.omo/run-continuation/ses_07b9c4268ffeic4mMXCPUt225j.json deleted file mode 100644 index 6e90994..0000000 --- a/.omo/run-continuation/ses_07b9c4268ffeic4mMXCPUt225j.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b9c4268ffeic4mMXCPUt225j", - "updatedAt": "2026-07-21T11:18:53.449Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T11:18:53.449Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b9c4346ffeVBdV5pf4h6s9PW.json b/.omo/run-continuation/ses_07b9c4346ffeVBdV5pf4h6s9PW.json deleted file mode 100644 index 15ecee9..0000000 --- a/.omo/run-continuation/ses_07b9c4346ffeVBdV5pf4h6s9PW.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b9c4346ffeVBdV5pf4h6s9PW", - "updatedAt": "2026-07-21T11:18:29.979Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T11:18:29.979Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07b9c9a75ffeOyJ4ewp3DBvXhz.json b/.omo/run-continuation/ses_07b9c9a75ffeOyJ4ewp3DBvXhz.json deleted file mode 100644 index 49b51ec..0000000 --- a/.omo/run-continuation/ses_07b9c9a75ffeOyJ4ewp3DBvXhz.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07b9c9a75ffeOyJ4ewp3DBvXhz", - "updatedAt": "2026-07-21T11:21:09.776Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-21T11:21:09.776Z" - } - } -} \ No newline at end of file diff --git a/.omo/run-continuation/ses_07cdbac0effeiBrdcHLUUUfwuF.json b/.omo/run-continuation/ses_07cdbac0effeiBrdcHLUUUfwuF.json deleted file mode 100644 index 9a0ffe1..0000000 --- a/.omo/run-continuation/ses_07cdbac0effeiBrdcHLUUUfwuF.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_07cdbac0effeiBrdcHLUUUfwuF", - "updatedAt": "2026-07-23T15:37:59.918Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-07-23T15:37:59.918Z" - } - } -} \ No newline at end of file diff --git a/.omo/文件库-开工计划.md b/.omo/文件库-开工计划.md deleted file mode 100644 index 945d2d0..0000000 --- a/.omo/文件库-开工计划.md +++ /dev/null @@ -1,114 +0,0 @@ -# 文件库 · 开工计划(v1.0) - -> 配套文档:《文件库-接口契约.md》(v0.1,仓库根目录)。本计划已吸收规划顾问评审意见,包含开工前必须先冻结的**架构收口决策 D11–D19**(回写契约 v0.2 时并入)。 -> 工期按 1 人全栈估算;前端从 Phase 2 后可并行。 - -## 开工判定 - -**可以开工。** 四个外部依赖(版本包 / Group / 审计 / 平台身份)全部有契约可依、可 mock 并行开发;无阻塞项。唯一前置:本文件的 D11–D19 决策在写第一行 schema 前过一遍(半天,自查即可,有异议再升级)。 - -## 架构收口决策(新增,先冻结再动工) - -| 编号 | 决策 | 理由 | -|---|---|---| -| D11 | `creator` 是 Node 的**不可变列**,创建时同时落一条 Manage grant;独立权限开关关闭时,**creator 的 Manage 仍生效**,其余项目级 grant 冻结不参与计算 | 否则 creator 可能被自己关掉的开关锁死 | -| D12 | 移动节点授权 = **本节点 Manage + 目标父 Edit+**;移动在事务 + Postgres 咨询锁内完成,防并发成环 | 预检环检测在并发下不安全 | -| D13 | Group resolve 失败返回 **503**(不伪装 404/403);每次 HTTP 请求 fresh resolve,不跨请求缓存;单请求内多次校验合并为一次调用(此点与 Group 团队确认,OPEN-9) | 依赖故障 ≠ 无权限;契约 G4 要求实时 | -| D14 | 命名规则:NFC 归一化、trim、≤128 字符、禁 `/` 与控制字符;**活跃兄弟节点大小写不敏感唯一**;根节点全局唯一 | 缺命名规则必有脏数据 | -| D15 | 删除 = 只给被删节点打标,后代**不递归打标**,靠"任一祖先已删"过滤;所有查询链路强制此过滤(含递归 SQL,不只靠 Prisma middleware) | 递归打标的写放大与恢复复杂度不可控 | -| D16 | `baseVersion` 为**文件级版本**(GET file 返回的 version);无关文件的提交不产生冲突 | 与 C1 `head(dir, filePath)` 语义一致 | -| D17 | breadcrumb 只暴露用户有 View 的祖先名称;无权限祖先显示占位符 `…`,不暴露名字 | 404 不泄露语义(D8)延伸到面包屑 | -| D18 | v1 **单副本部署**(git 仓库在本地磁盘);多副本需共享存储或分片,后置 | 不解决不存在的扩展问题 | -| D19 | 网站管理员**不隐式读内容**;force_adjust 凭 node id 操作(id 从审计或用户上报获得) | C4 已定的最小权限原则 | - -## 阶段计划 - -### Phase 0 · 骨架与身份先行(1–2 天) - -- [ ] 过 D11–D19,回写契约 v0.2 -- [ ] repo 骨架 `server/` + `web/`,Postgres docker-compose,CI(lint / typecheck / vitest / 集成测试用真实 Postgres service container) -- [ ] **JWT 中间件 + 本地 JWKS 测试服务 + 签名 token fixtures**(有效/过期/错 issuer/错 audience/错算法/未知 kid/轮换)——身份先行,不做"宽松 mock 身份" -- [ ] 4 个 port 接口定义 + mock:`VersionStore`、`GroupResolver`、`AuditSink`(直写 outbox 表的实现就是真的,mock 的是对端服务)、`ExportAdapter` -- [ ] fastify-swagger 接入,OpenAPI 骨架——**OpenAPI 随每个端点同步产出,不留到最后** -- **验收**:骨架可 `pnpm dev` 起服务;本地 JWKS 六类 token fixture(有效/过期/错 issuer/错 audience/错算法/未知 kid)中间件行为全对;CI 流水线绿 - -### Phase 1 · 数据模型与权限引擎(2–3 天)⚑ 心脏 - -- [ ] Prisma schema:`Node`(parentId **权威** + id 编码的 materialized path 派生列,name 不入 path)、`Grant`、`ProjectIndependentSettings`、`OutboxEvent`(含 attempts/nextAttemptAt/lease/lastError,relay 字段一次到位)、`ExportJob` -- [ ] 数据库约束:kind 枚举、项目无子节点、活跃兄弟名唯一(部分唯一索引,root 的 NULL parent 特殊处理)、活跃 grant 按 (nodeId, principalType, principalId) 唯一、独立设置仅项目 -- [ ] **纯权限 reducer**:输入已解析的 grants + 祖先链 + 用户组集合,输出 role|null;不碰 DB/网络。fast-check 属性测试:max 单调、加 grant 只升不降、祖先继承、toggle 行为、顺序无关、软删过滤 -- [ ] 数据获取层 + `effective(user, node)` 组装 -- [ ] 树操作服务:create(creator 自动 Manage、root 仅管理员且必带 ≥1 Manage)、rename、move(D12 事务+咨询锁)、soft delete(D15)、breadcrumb(D17) -- **验收**:并发对移(A→B 与 B→A)、移动+并发建子、property tests、删除祖先过滤,真实 Postgres 全绿 - -### Phase 2 · 树与授权 API(1–2 天) - -- [ ] nodes 端点组 + grants 端点组 + independent-permission 开关 + effective-permission 自查 -- [ ] 授权矩阵校验(契约 8.1:creator-only 授 Manage、Manage 不可动 creator、force_adjust 独立通道) -- [ ] 全部写操作落 outbox 事件(C3 词汇表) -- **验收**:契约 8.1 矩阵逐格 API 测试;D8 的 404/403/503 三分语义测试 - -### Phase 3 · 仓库生命周期与文件 API(2 天) - -- [ ] 项目 provisioning 状态机 `PROVISIONING → READY | FAILED`:先建 DB 行(PROVISIONING)→ 调 `VersionStore.init`(幂等)→ 置 READY;失败可重试;孤儿仓库对账 job -- [ ] 每项目写锁(Postgres 咨询锁,跨进程安全)——不假设版本包能跨进程串行 -- [ ] 路径安全:canonical 后必须落在项目根内、拒绝 `.git`、禁 symlink 逃逸、路径长度上限 -- [ ] 上传限额(单文件 10MB / 单项目配额,数值 OPEN-5)+ 流式接收 -- [ ] files 端点组全量(list/read/upload/commits→409/diff/history/delete)+ 双客户端冲突 e2e -- **验收**:DB/git 故障注入(init 失败、提交后崩溃)→ 对账能收敛;路径穿越语料库测试全拒 - -### Phase 4 · 外部集成(1–2 天) - -- [ ] `GroupResolver` HTTP client:超时/5xx/429/畸形 JSON/万级 group 的故障注入测试;失败 → 503(D13) -- [ ] Audit relay worker:租约领取、指数退避、eventId 幂等、重启恢复、积压告警 -- [ ] 导出 job:状态机 + 轮询 + 下载权限校验;`ExportAdapter` 桩(参数 OPEN-6 未定前**不计入完成标准**) -- **验收**:relay 重启不丢不重(对端幂等);group 故障时写操作 503、读操作按 D13 - -### Phase 5 · 前端(3–5 天,Phase 2 后并行启动) - -- [ ] 登录(平台 SSO 占位 + 本地 JWKS 直通开关) -- [ ] 树浏览(懒加载 + 分页)、breadcrumb(D17 占位符)、创建对话框(类型 + 批量授权选择器,Group search 接 C2) -- [ ] 权限管理面板(按 8.1 矩阵控制可选项,creator 标识) -- [ ] 文件页:查看/上传/下载/历史列表(编辑 UI 不归我方,留对接位) -- [ ] 导出按钮 + job 轮询 -- **验收**:无权限节点全链路不可见;授权面板不会送出矩阵禁止的组合 - -### Phase 6 · 硬化与交付(1–2 天) - -- [ ] 404/403/503 全端点核对;并发与重试幂等抽查 -- [ ] 备份脚本(DB + git 仓库一致性快照)、健康检查(DB/JWKS/磁盘/Group)、磁盘水位告警 -- [ ] OpenAPI 终稿 + 部署脚本 + 一页运维手册 - -## 里程碑 - -| 里程碑 | 内容 | 累计工期 | -|---|---|---| -| M1 | Phase 0–1:骨架 + 权限引擎 + 树操作可跑 | ~4 天 | -| M2 | Phase 2–3:后端 API 全量(含文件冲突流) | ~7 天 | -| M3 | Phase 4:外部集成(导出不计) | ~9 天 | -| M4 | Phase 5–6:前端 + 硬化,可交付 | ~14–18 天 | - -## Mock 保真红线(mock 可以顶,但不许骗) - -| Mock | 不许掩盖的事 | 真身到位后的契约测试 | -|---|---|---| -| VersionStore | 磁盘延迟、半初始化、锁残留、跨进程并发 | 真包跑临时仓库:并发 commit/init 重试/遍历与 symlink 语料 | -| GroupResolver | 超时、5xx、畸形响应、大规模组集 | 故障注入 HTTP 桩全过;我方绝不自己推祖先 | -| GroupSearch(前端选择器) | 与 resolve 是**两个独立端点**,不可用 resolve mock 顶替 | C2 `/groups/search` 真身到位后跑分页/空结果/超时 | -| Audit 对端 | 重复投递、乱序、长时间不可用 | relay 重启恢复 + 对端幂等 | -| JWT | issuer/audience/算法/轮换 | 本地 JWKS 全用例 | -| ExportAdapter | 参数、幂等、耗时 | 参数定了再写,之前不算完成 | - -## Top 5 风险 - -1. **DB/git 分裂**(崩溃导致元数据、仓库、审计三边不一致)→ provisioning 状态机 + 对账 job,Phase 3 前置 -2. **移动子树改变整树权限** → D12 事务+锁+源目标双授权,并发测试 -3. **creator/独立权限开关交互** → D11 先冻结 -4. **外部契约缺口**(导出参数、请求内 resolve 合并)→ OPEN 清单跟踪,mock 保真红线不许掩盖 -5. **存储滥用/路径逃逸** → 路径安全 + 配额进 Phase 3 验收,不拖到硬化 - -## OPEN 清单(在契约文档基础上增补) - -- OPEN-9:单请求内合并多次 resolve 是否符合 G4"实时"语义(找 Group 团队确认) -- OPEN-10:恶意软件扫描归属(我方/平台/不做) -- 其余 OPEN-1~8 见《文件库-接口契约.md》第 10 节 diff --git a/hub/src/database/README.md b/hub/src/database/README.md index 9d18952..af2bde1 100644 --- a/hub/src/database/README.md +++ b/hub/src/database/README.md @@ -96,9 +96,11 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真 ## 文件库(filelib/) -独立文件库模块,语义由仓库根《文件库-接口契约.md》(C/D 编号)与 -`.omo/文件库-开工计划.md`(D11–D19)锚定。**与 hub 自己的 Folder/Project -(ADR-0021 explorer)是两套体系,不复用。** +独立文件库模块。代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等) +出自两份已删除的文档:《文件库-接口契约.md》与 `.omo/文件库-开工计划.md`, +内容可从 git 历史取回。其中 D19(网站管理员 = silo org OWNER/ADMIN) +另见 ADR-0028。**与 hub 自己的 Folder/Project(ADR-0021 explorer)是 +两套体系,不复用。** | 文件 | 职责 | |------|------| diff --git a/文件库-接口契约.md b/文件库-接口契约.md deleted file mode 100644 index 2a1de4a..0000000 --- a/文件库-接口契约.md +++ /dev/null @@ -1,304 +0,0 @@ -# 文件库系统 · 接口契约与决策(v0.1 我方提案) - -> **读者**:版本团队、Group 团队、审计团队、平台方、编辑团队、产品。 -> **用法**:本文档是我方(文件库)提出的对齐基线。所有决策带 `D-x` 编号、所有契约带 `C-x` 编号,评审时请按编号引用("D5 我们不同意,建议……")。评审通过后本文件冻结,各方照此实现;任何变更走文档修订,不接受口头对齐。 -> **状态**:v0.1 提案,未冻结。 - ---- - -## 1. 系统边界与分工 - -| 子系统 | 负责方 | 与我方关系 | -|---|---|---| -| 文件库(目录树 / 权限引擎 / 内容存储 / 对外 API / 前端) | **我方** | — | -| 版本能力(git 化、commit、diff、历史) | 版本团队 | 交付 **npm 工具包**,我方在自有存储上集成(见 C1) | -| Group 系统(全局嵌套组) | Group 团队 | 我方**实时消费**其查询接口(见 C2) | -| 审计系统(存储 / 查询 / 保留) | 审计团队 | 我方**上报事件**(见 C3);查询界面归审计方 | -| 平台身份(登录 / 角色) | 平台方 | 我方验签消费角色(见 C4) | -| 在线编辑与冲突合并 UI(需求 2.5) | 编辑团队 | **消费我方文件 API**(见第 9 节) | -| 导出工具 | 已有工具 | 我方按需传参调用(参数清单 OPEN-6) | - -**内容归属**:文件内容(git 仓库)物理存储在文件库自己的服务器上,由我方管理;版本团队不持有任何数据,只交付代码。 - -## 2. 总体架构 - -``` -编辑团队(2.5 UI) ──┐ -其他消费方 ──┤ HTTP + JWT - ▼ - ┌─────────────────────┐ - │ 文件库服务(我方) │ - │ 目录树 + 权限引擎 │──── 实时查询 ──▶ Group 系统(C2) - │ 文件 API + 授权 API │──── 验签 JWT ──▶ 平台身份(C4) - └─────────┬───────────┘ - │ import(函数调用) - ▼ - ┌─────────────────────┐ outbox 中继(at-least-once) - │ 版本工具包(C1,npm) │ ┌──────────────────────▶ 审计系统(C3) - │ 操作我方磁盘上的 │ │ - │ git 仓库(内容归我方)│ ▼ - └─────────────────────┘ 我方 DB:业务表 + outbox 表(同事务) -``` - -## 3. 决策总表 - -| 编号 | 决策 | 一句话理由 | 影响方 | -|---|---|---|---| -| D1 | 文件库后端 TypeScript + Fastify + Prisma(PostgreSQL);前端 React | 与平台技术心智一致 | 我方 | -| D2 | 文件内容物理存储在文件库服务器,版本能力以 npm 包交付 | 需求 2.1"项目初始化为 Git 仓库",仓库是项目的存储 | 版本团队 | -| D3 | 版本包冲突用**返回值**表达,异常仅用于系统错误 | 冲突是正常业务流,不是故障 | 版本、编辑 | -| D4 | Group 查询**实时调用、不缓存** | 需求 3.3"成员变更实时生效" | Group | -| D5 | 审计走"本地 outbox + 后台中继"满足"同事务或可靠消息" | 跨服务做不到真同事务,outbox 是最简单的可靠方案 | 审计 | -| D6 | 平台身份用 JWT(RS256)验签,平台角色 `platform.admin` 映射网站管理员 | 无状态、无需每次回调平台 | 平台 | -| D7 | 权限管理规则按第 8 节补齐(2.4 空洞的我方版本) | 需求 2.4 为空,必须先有规则才能写码 | 产品确认 | -| D8 | 无 View 权限时 API 返回 **404**(不泄露存在性);有 View 但操作越权返回 403 | 防资源探测 | 所有消费方 | -| D9 | 软删除:删除即打标隐藏,所有 API 默认不可见 | 需求 2.6/3.3 | 我方 | -| D10 | 导出为**异步任务**(提交返回 job,轮询取结果) | 导出耗时不确定,同步会超时 | 编辑/前端 | - ---- - -## 4. C1 · 版本工具包契约(给版本团队) - -**交付形态**:npm 包(TypeScript,自带类型定义),运行在文件库后端进程内。实现技术(isomorphic-git / nodegit / 其他)由版本团队自选,**本契约只约束接口与语义**。 - -### 4.1 接口签名 - -```typescript -export type VersionId = string; // 不透明字符串,调用方不得解析(当前为 git commit hash) - -export interface CommitRequest { - /** 编辑的起始版本;null 表示新建文件 */ - baseVersion: VersionId | null; - content: string | Buffer; // 文本用 string(UTF-8),二进制用 Buffer - message?: string; // 缺省由包自动生成 - author?: string; // 操作者标识,写入版本记录 -} - -export type CommitResult = - | { status: "ok"; version: VersionId } - | { status: "conflict"; currentVersion: VersionId }; - -export interface VersionInfo { - version: VersionId; - message: string; - author?: string; - committedAt: string; // ISO 8601 -} - -export interface FileEntry { path: string; size: number; } - -export interface VersionStore { - init(projectDir: string): Promise; // S7: 幂等 - list(projectDir: string, prefix?: string): Promise; - head(projectDir: string, filePath: string): Promise; // 文件不存在 → FileNotFoundError - read(projectDir: string, filePath: string, at?: VersionId): Promise; // 缺省读最新 - commit(projectDir: string, filePath: string, req: CommitRequest): Promise; - remove(projectDir: string, filePath: string, baseVersion: VersionId): Promise; - diff(projectDir: string, filePath: string, from: VersionId, to: VersionId): Promise; // unified diff - history(projectDir: string, filePath: string, limit?: number): Promise; -} -``` - -### 4.2 语义规则 - -| 编号 | 规则 | -|---|---| -| S1 | **冲突不是异常**。`baseVersion` 落后于当前版本时返回 `{status:"conflict"}`;异常仅用于 IO 故障、仓库损坏等系统错误 | -| S2 | `baseVersion: null` = 新建;路径已存在时返回 conflict | -| S3 | 二进制文件与文本同样版本化;`diff` 仅保证对文本有意义,二进制可返回占位说明 | -| S4 | 包必须保证**同一 projectDir 的写操作(commit/remove)串行化**,调用方可并发调用 | -| S5 | 存储布局不透明:我方不直接读写仓库目录,一切经包接口 | -| S6 | 在线编辑仅针对文本文件(需求 2.5);二进制材料走上传/下载 | -| S7 | `init` 幂等,重复调用不报错、不重建 | -| S8 | 规模假设:单项目文件数千级、单文件 10MB 以内,超出另行对齐 | - ---- - -## 5. C2 · Group 查询契约(给 Group 团队) - -**形态**:HTTP + JSON。我方只读,不写 Group 系统任何数据(需求 3.3)。 - -### 5.1 接口 - -``` -GET /groups/resolve-member-groups?userId={userId} - → { "groupIds": ["g1","g2",...] } - # 权限计算专用:用户直接所属的全部 group + 这些 group 的所有祖先 group,去重。 - # 方向是"向祖先"收集(需求 3.2:权限沿 group 树向下传递)。 - -GET /groups/search?q={keyword}&limit={n} - → [{ "id":"g1","name":"物理教研组","breadcrumb":"总部 / 教研 / 物理" }] - # 前端授权选择器用。 - -GET /groups/{groupId} - → { "id":"g1","name":"物理教研组","parentId":"g0","status":"active" } -``` - -### 5.2 语义规则 - -| 编号 | 规则 | -|---|---| -| G1 | `groupId` 为不透明、全局唯一字符串 | -| G2 | resolve 只含**祖先方向**,不含子孙;用户无所属时返回空数组 | -| G3 | 软删除的 group 不出现在任何接口结果中 | -| G4 | **实时性**:我方每次权限计算都实时调用 resolve,不缓存;Group 方成员变更须即刻在 resolve 结果中可见(需求 3.3) | -| G5 | 文件夹/项目的授权记录只存在文件库,Group 系统不感知(需求 3.3) | -| G6 | 可用性:resolve 位于我方**每一次受保护请求**的权限计算路径上,其可用性即文件库可用性 → 需要 Group 方给出延迟与可用性承诺(OPEN-1) | - ---- - -## 6. C3 · 审计上报契约(给审计团队) - -### 6.1 机制(满足需求 5.1"同事务或可靠消息") - -1. 我方在每个关键写操作的**同一数据库事务**内,向本地 `audit_outbox` 表写入事件; -2. 后台中继进程读取 outbox,POST 到审计系统 `POST /audit/events`,**at-least-once**,失败重试; -3. 审计系统按 `eventId` **幂等去重**; -4. 中继只负责送达,业务操作不因审计系统不可用而失败(但事件绝不丢)。 - -### 6.2 事件信封(补齐需求 5.3 空缺的字段定义) - -```json -{ - "eventId": "01J…", // 我方生成的唯一 id,幂等键 - "schemaVersion": 1, - "occurredAt": "2026-07-21T10:00:00.000Z", - "sourceService": "filelib", - "actor": { "userId": "u123", "platformRole": "teacher" }, - "action": "permission.grant", // 见 6.3 词汇表 - "object": { "type": "folder", "id": "n456", "path": "/物理/必修一" }, - "result": "success", // success | failure - "errorCode": null, - "detail": { /* 按 action 而定的负载,如 grant 的 {principalType, principalId, role} */ } -} -``` - -### 6.3 action 词汇表(文件库范围,对照需求 5.2) - -| action | 需求 5.2 对应 | -|---|---| -| `folder.create` / `folder.rename` / `folder.move` / `folder.delete` | 创建/删除/移动/重命名文件夹 | -| `project.create` / `project.rename` / `project.move` / `project.delete` | 同上(项目) | -| `permission.grant` / `permission.update` / `permission.revoke` | 权限变更(detail 含个人/Group、Manage/Edit/View) | -| `project.independent_permission.enable` / `.disable` / `.change` | 项目独立权限开启/关闭/变更 | -| `file.upload` / `file.rename` / `file.delete` | 材料文件管理 | -| `file.commit` | 文件编辑提交(含冲突合并后提交;编辑经我方 API 落盘,故由我方上报) | -| `file.conflict_detected` | 冲突检测发生(detail 含起始版本与冲突版本) | -| `export.run` | 导出操作 | -| `admin.force_adjust` | 网站管理员强制权限调整(高危) | - -Group 相关动作(创建/删除/成员变更/嵌套变更)由 **Group 系统自行上报**;归档类动作待归档功能定案后补充(OPEN-4)。 - -### 6.4 边界 - -- 审计的存储、防篡改、保留策略(需求 5.5 ≥180 天)、查询接口与查询界面,全部归审计系统; -- 文件库前端**不内嵌**审计查询页(若产品要求嵌入,OPEN-8 再议)。 - ---- - -## 7. C4 · 平台身份契约(给平台方) - -1. 调用方在 `Authorization: Bearer ` 中携带平台签发的令牌(RS256);平台通过 JWKS endpoint 分发公钥,我方只做验签 + 过期检查,**不回调平台、不建用户表**; -2. claims 约定:`sub`(用户 id,全局唯一)、`roles`(平台角色数组)、`exp`、`iss`; -3. **角色映射**:`roles` 含 `platform.admin` → 文件库"网站管理员";其余合法令牌 → 普通用户;无令牌/验签失败/过期 → `401`; -4. 网站管理员权限范围:创建根目录、强制权限调整(必审计)、以及平台方赋予的其他高危操作;**不隐式穿透**各节点的业务权限(要管理某子树须被显式授权或走强制调整并留痕); -5. 前端登录跳转平台 SSO,具体流程由前端与平台方另行对齐。 - ---- - -## 8. 权限规则 · 2.4 补齐版(D7,产品确认后冻结) - -### 8.1 创建者与授权矩阵 - -| 角色 | 能授/改/收的级别 | 限制 | -|---|---|---| -| **节点创建者** | Manage / Edit / View | 自身 Manage 不可被收回(转让 OPEN-3) | -| **Manage 持有者** | Edit / View | **不可**授予 Manage;**不可**修改/收回创建者的任何权限 | -| **Edit / View** | 无授权能力 | — | -| **网站管理员** | 任意(走 `admin.force_adjust`,必审计) | 不属于日常授权路径 | - -### 8.2 各级别能力清单 - -| 能力 | View | Edit | Manage | -|---|:---:|:---:|:---:| -| 浏览树 / 读文件 / 下载 | ✓ | ✓ | ✓ | -| 导出 | ✓ | ✓ | ✓ | -| 创建子文件夹/项目(需求:父级 Edit+) | | ✓ | ✓ | -| 编辑文本文件 / 上传 / 删改项目内材料 | | ✓ | ✓ | -| 重命名 / 移动 / 删除**本节点** | | | ✓ | -| 授权管理(按 8.1 矩阵) | | | ✓ | -| 项目独立权限开关 | | | ✓ | - -### 8.3 其余规则 - -- **P3 根目录**:仅网站管理员可创建;创建时必须指定 ≥1 名 Manage 持有者(可以不是创建者本人),保证每棵子树都有权限链起点; -- **P4 空权限创建**:允许;此时仅创建者可见可用; -- **P5 项目独立权限**:默认关闭(仅继承父链);开启后项目级授权参与 max 计算;关闭时项目级授权**冻结不删除**,重新开启即恢复; -- **P6 计算规则**(与需求 2.3 一致的形式化): - - `effective(user, R) = max { grant.role | grant ∈ grants(s, r), s ∈ {user} ∪ resolveGroups(user), r ∈ {R} ∪ ancestors(R) }`,无 grant → 无权限。个人低权限**不构成降权**(只取最高,不做减法); -- **P7 可见性**:对某节点无任何权限的用户,该节点对其不可见(API 行为见 D8)。 - ---- - -## 9. 文件库对外 API(消费方:编辑团队及其他) - -**约定**:REST + JSON + JWT(C4);统一错误信封 `{ "error": { "code", "message", "details?" } }`;D8 可见性语义(无 View → 404;越权操作 → 403)。 - -### 9.1 树与节点 - -``` -GET /nodes?parentId={id} # 列子节点(缺省列根);仅返回有 View 的 -POST /nodes # 创建文件夹/项目 {parentId, type, name, grants?[]} - # parentId=null 仅网站管理员;type=project 时自动调 C1.init -GET /nodes/{id} # 节点详情 + 我的 effective 权限 + breadcrumb -PATCH /nodes/{id} # 重命名/移动 {name?, parentId?} (需本节点 Manage) -DELETE /nodes/{id} # 软删除(需 Manage) -``` - -### 9.2 授权 - -``` -GET /nodes/{id}/grants # 授权列表(需 Manage) -PUT /nodes/{id}/grants # 批量授予/修改 [{principalType:user|group, principalId, role}] -DELETE /nodes/{id}/grants/{grantId} # 收回 -GET /nodes/{id}/effective-permission # 当前用户在此节点的生效权限(自查) -PUT /projects/{id}/independent-permission # {enabled: bool} 独立权限开关 -``` - -### 9.3 文件内容(编辑团队的主接口) - -``` -GET /projects/{id}/files?prefix= # 文件清单 -GET /projects/{id}/files/{path} # → {content, version} 编辑起手式:拿到起始版本 -PUT /projects/{id}/files/{path} # 新建/上传 {content} (需 Edit) -POST /projects/{id}/files/{path}/commits # 提交编辑 {baseVersion, content, message?} - # → 200 {version} | 409 {currentVersion} -GET /projects/{id}/files/{path}/diff?from=&to= # 冲突时取差异(unified diff) -GET /projects/{id}/files/{path}/history # 版本历史 -DELETE /projects/{id}/files/{path} # {baseVersion} (需 Edit) -``` - -**冲突合并流程(配合编辑团队 2.5)**:编辑 UI 用 `GET files/{path}` 记录 `version` → 用户编辑 → `POST commits` 带 `baseVersion` → 若 `409`,UI 用 `diff?from=base&to=current` 拉取差异,展示三方合并区 → 用户写出最终内容后再次 `POST commits`(baseVersion 换为 currentVersion)。 - -### 9.4 导出 - -``` -POST /projects/{id}/exports {target, params} → {jobId} # D10 异步 -GET /exports/{jobId} → {status, downloadUrl?} -``` - ---- - -## 10. OPEN 清单(需对方/产品确认,不阻塞我方开工) - -| 编号 | 事项 | 等谁 | -|---|---|---| -| OPEN-1 | Group resolve 的延迟/可用性 SLA 数值 | Group 团队 | -| OPEN-2 | 审计事件投递的 endpoint、鉴权方式、保留期确认(需求建议 ≥180 天) | 审计团队 | -| OPEN-3 | 创建者离职/转让后 Manage 链如何处理 | 产品 | -| OPEN-4 | 归档功能(需求已划线"暂时未定") | 产品 | -| OPEN-5 | 二进制材料的大小上限与在线预览诉求 | 产品 | -| OPEN-6 | 导出工具的参数清单(需求原文"待对接时确认") | 导出工具方 | -| OPEN-7 | 软删除的恢复入口、"后台标签"管理界面归属 | 产品 | -| OPEN-8 | 网站管理员强制调整的前端入口(我方做还是平台做)、审计查询页是否嵌入文件库前端 | 产品 |