From 65b0bb04f861be90dbe8606115f150ab92cd6d92 Mon Sep 17 00:00:00 2001 From: wsh5485 Date: Sat, 20 Jun 2026 22:30:19 +0800 Subject: [PATCH] feat: import CarLog v2.8 code + dev plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。 --- .editorconfig | 19 + .env.example | 18 + .eslintrc.json | 35 + .gitignore | 2 +- .husky/pre-commit | 5 + .pa11yci.json | 14 + .prettierignore | 26 + .prettierrc.json | 12 + carlog-init.sql | 459 ++ client/index.html | 42 + client/package-lock.json | 6905 +++++++++++++++++ client/package.json | 24 + client/public/favicon-16x16.png | Bin 0 -> 578 bytes client/public/favicon-32x32.png | Bin 0 -> 1069 bytes client/public/pwa-icon.svg | 23 + client/public/pwa/apple-touch-icon.png | Bin 0 -> 5894 bytes client/public/pwa/pwa-192x192.png | Bin 0 -> 6065 bytes client/public/pwa/pwa-512x512.png | Bin 0 -> 21396 bytes client/public/pwa/pwa-maskable-512x512.png | Bin 0 -> 16660 bytes client/scripts/check-pwa.mjs | 166 + client/scripts/gen-pwa-icons.mjs | 42 + client/src/App.vue | 22 + client/src/api/ai.js | 19 + client/src/api/auth.js | 8 + client/src/api/chemicals.js | 16 + client/src/api/client.js | 166 + client/src/api/insurance.js | 19 + client/src/api/logs.js | 22 + client/src/api/operationLogs.js | 6 + client/src/api/settings.js | 16 + client/src/api/vehicles.js | 9 + client/src/api/washes.js | 17 + client/src/components/AiFallbackModal.vue | 64 + client/src/components/AppHeader.vue | 290 + client/src/components/AppLayout.vue | 59 + client/src/components/ChartBlock.vue | 90 + client/src/components/ChemPicker.vue | 189 + client/src/components/ConfirmDangerDialog.vue | 256 + client/src/components/DebugPanel.vue | 240 + client/src/components/MobileCardList.vue | 241 + client/src/components/PwaToasts.vue | 218 + client/src/components/StatCard.vue | 29 + client/src/composables/useAiRecognize.js | 125 + client/src/main.js | 83 + client/src/router/index.js | 69 + client/src/stores/auth.js | 44 + client/src/stores/debug.js | 59 + client/src/stores/pwa.js | 89 + client/src/style.css | 156 + client/src/utils/formDraft.js | 77 + client/src/views/BatchPurchase.vue | 246 + client/src/views/ChargingList.vue | 358 + client/src/views/ChemicalDetail.vue | 432 ++ client/src/views/ChemicalNew.vue | 168 + client/src/views/ChemicalsList.vue | 386 + client/src/views/Home.vue | 364 + client/src/views/InsuranceList.vue | 452 ++ client/src/views/Login.vue | 199 + client/src/views/MaintenanceList.vue | 369 + client/src/views/Offline.vue | 62 + client/src/views/OperationLogs.vue | 275 + client/src/views/RefuelList.vue | 374 + client/src/views/Settings.vue | 663 ++ client/src/views/Stats.vue | 414 + client/src/views/VehicleDetail.vue | 379 + client/src/views/VehicleForm.vue | 136 + client/src/views/VehiclesList.vue | 167 + client/src/views/WashNew.vue | 326 + client/src/views/WashShow.vue | 352 + client/src/views/WashesList.vue | 351 + client/vite.config.js | 158 + docs/DEV-PLAN.md | 1134 +++ docs/install/INSTALL-BT-NODE.md | 180 + lighthouserc.json | 24 + package.json | 58 + server/migrations/0001_init.sql | 155 + server/migrations/0002_auth.sql | 68 + server/migrations/0003_vehicles.sql | 45 + server/migrations/0004_grocy_full.sql | 27 + server/migrations/0005_inventory_detail.sql | 39 + server/migrations/0006_unit_conversion.sql | 13 + server/migrations/0007_vehicle_logs.sql | 75 + .../migrations/0008_mileage_and_insurance.sql | 27 + server/migrations/0009_vehicle_powertrain.sql | 5 + server/migrations/0010_operation_logs.sql | 18 + server/migrations/0011_soft_delete.sql | 19 + .../0012_operation_logs_recovery.sql | 5 + server/migrations/0013_weather_wttr.sql | 26 + server/migrations/0014_grocy_auth.sql | 26 + server/migrations/0015_wash_photos.sql | 19 + server/migrations/0016_vehicle_current_km.sql | 34 + server/migrations/0017_tags.sql | 19 + server/migrations/0018_achievements.sql | 39 + server/migrations/mysql/0001_init.sql | 148 + server/migrations/mysql/0002_auth.sql | 57 + server/migrations/mysql/0003_vehicles.sql | 35 + server/migrations/mysql/0004_grocy_full.sql | 22 + .../mysql/0005_inventory_detail.sql | 31 + .../migrations/mysql/0006_unit_conversion.sql | 10 + server/migrations/mysql/0007_vehicle_logs.sql | 72 + .../mysql/0008_mileage_and_insurance.sql | 27 + .../mysql/0009_vehicle_powertrain.sql | 3 + .../migrations/mysql/0010_operation_logs.sql | 19 + server/migrations/mysql/0011_soft_delete.sql | 15 + .../mysql/0012_operation_logs_recovery.sql | 2 + server/migrations/mysql/0013_weather_wttr.sql | 3 + server/migrations/mysql/0014_grocy_auth.sql | 20 + server/migrations/mysql/0015_wash_photos.sql | 19 + .../mysql/0016_vehicle_current_km.sql | 30 + server/migrations/mysql/0017_tags.sql | 19 + server/migrations/mysql/0018_achievements.sql | 39 + server/package-lock.json | 3058 ++++++++ server/package.json | 28 + server/src/bin/backup.js | 5 + server/src/bin/export.js | 5 + server/src/bin/grocy-refresh-products.js | 7 + server/src/bin/grocy-sync.js | 7 + server/src/bin/migrate.js | 24 + server/src/bin/reset-all.js | 233 + server/src/bin/seed-demo.js | 169 + server/src/bin/serve.js | 19 + server/src/bin/users.js | 86 + server/src/bin/verify.js | 147 + server/src/bin/weather.js | 7 + server/src/config.js | 91 + server/src/db.js | 341 + server/src/http.js | 41 + server/src/index.js | 212 + server/src/middleware/auth.js | 9 + server/src/middleware/csrf.js | 15 + server/src/middleware/ipRateLimit.js | 51 + server/src/routes/achievements.js | 137 + server/src/routes/ai.js | 183 + server/src/routes/auth.js | 304 + server/src/routes/chemicals.js | 318 + server/src/routes/extra.js | 270 + server/src/routes/insurance.js | 258 + server/src/routes/logs.js | 311 + server/src/routes/notifications.js | 67 + server/src/routes/operationLogs.js | 131 + server/src/routes/settings.js | 745 ++ server/src/routes/tags.js | 112 + server/src/routes/vehicles.js | 386 + server/src/routes/washes.js | 426 + server/src/services/aiVision.js | 226 + server/src/services/auth.js | 112 + server/src/services/backup.js | 88 + server/src/services/categoryMap.js | 44 + server/src/services/challenge.js | 29 + server/src/services/exporter.js | 95 + server/src/services/grocy.js | 86 + server/src/services/grocyClient.js | 84 + server/src/services/grocyProducts.js | 160 + server/src/services/grocyWrite.js | 99 + server/src/services/monthlyReport.js | 403 + server/src/services/operationLog.js | 37 + server/src/services/rateLimit.js | 107 + server/src/services/weather.js | 156 + server/src/setup.js | 358 + server/src/swagger.js | 63 + server/test/challenge.test.js | 63 + server/test/db.keepAlive.test.js | 86 + server/test/db.softWhere.test.js | 70 + server/test/integration.middleware.test.js | 98 + server/test/middleware.auth.test.js | 66 + server/test/middleware.csrf.test.js | 122 + server/test/middleware.ipRateLimit.test.js | 131 + server/test/routes.extra.test.js | 134 + server/test/routes.notifications.test.js | 112 + server/test/routes.stats.test.js | 126 + server/test/routes.tags.test.js | 149 + server/test/routes.vehicles.test.js | 311 + server/test/setup.js | 7 + vitest.config.js | 23 + 174 files changed, 31594 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .husky/pre-commit create mode 100644 .pa11yci.json create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 carlog-init.sql create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/favicon-16x16.png create mode 100644 client/public/favicon-32x32.png create mode 100644 client/public/pwa-icon.svg create mode 100644 client/public/pwa/apple-touch-icon.png create mode 100644 client/public/pwa/pwa-192x192.png create mode 100644 client/public/pwa/pwa-512x512.png create mode 100644 client/public/pwa/pwa-maskable-512x512.png create mode 100644 client/scripts/check-pwa.mjs create mode 100644 client/scripts/gen-pwa-icons.mjs create mode 100644 client/src/App.vue create mode 100644 client/src/api/ai.js create mode 100644 client/src/api/auth.js create mode 100644 client/src/api/chemicals.js create mode 100644 client/src/api/client.js create mode 100644 client/src/api/insurance.js create mode 100644 client/src/api/logs.js create mode 100644 client/src/api/operationLogs.js create mode 100644 client/src/api/settings.js create mode 100644 client/src/api/vehicles.js create mode 100644 client/src/api/washes.js create mode 100644 client/src/components/AiFallbackModal.vue create mode 100644 client/src/components/AppHeader.vue create mode 100644 client/src/components/AppLayout.vue create mode 100644 client/src/components/ChartBlock.vue create mode 100644 client/src/components/ChemPicker.vue create mode 100644 client/src/components/ConfirmDangerDialog.vue create mode 100644 client/src/components/DebugPanel.vue create mode 100644 client/src/components/MobileCardList.vue create mode 100644 client/src/components/PwaToasts.vue create mode 100644 client/src/components/StatCard.vue create mode 100644 client/src/composables/useAiRecognize.js create mode 100644 client/src/main.js create mode 100644 client/src/router/index.js create mode 100644 client/src/stores/auth.js create mode 100644 client/src/stores/debug.js create mode 100644 client/src/stores/pwa.js create mode 100644 client/src/style.css create mode 100644 client/src/utils/formDraft.js create mode 100644 client/src/views/BatchPurchase.vue create mode 100644 client/src/views/ChargingList.vue create mode 100644 client/src/views/ChemicalDetail.vue create mode 100644 client/src/views/ChemicalNew.vue create mode 100644 client/src/views/ChemicalsList.vue create mode 100644 client/src/views/Home.vue create mode 100644 client/src/views/InsuranceList.vue create mode 100644 client/src/views/Login.vue create mode 100644 client/src/views/MaintenanceList.vue create mode 100644 client/src/views/Offline.vue create mode 100644 client/src/views/OperationLogs.vue create mode 100644 client/src/views/RefuelList.vue create mode 100644 client/src/views/Settings.vue create mode 100644 client/src/views/Stats.vue create mode 100644 client/src/views/VehicleDetail.vue create mode 100644 client/src/views/VehicleForm.vue create mode 100644 client/src/views/VehiclesList.vue create mode 100644 client/src/views/WashNew.vue create mode 100644 client/src/views/WashShow.vue create mode 100644 client/src/views/WashesList.vue create mode 100644 client/vite.config.js create mode 100644 docs/DEV-PLAN.md create mode 100644 docs/install/INSTALL-BT-NODE.md create mode 100644 lighthouserc.json create mode 100644 package.json create mode 100644 server/migrations/0001_init.sql create mode 100644 server/migrations/0002_auth.sql create mode 100644 server/migrations/0003_vehicles.sql create mode 100644 server/migrations/0004_grocy_full.sql create mode 100644 server/migrations/0005_inventory_detail.sql create mode 100644 server/migrations/0006_unit_conversion.sql create mode 100644 server/migrations/0007_vehicle_logs.sql create mode 100644 server/migrations/0008_mileage_and_insurance.sql create mode 100644 server/migrations/0009_vehicle_powertrain.sql create mode 100644 server/migrations/0010_operation_logs.sql create mode 100644 server/migrations/0011_soft_delete.sql create mode 100644 server/migrations/0012_operation_logs_recovery.sql create mode 100644 server/migrations/0013_weather_wttr.sql create mode 100644 server/migrations/0014_grocy_auth.sql create mode 100644 server/migrations/0015_wash_photos.sql create mode 100644 server/migrations/0016_vehicle_current_km.sql create mode 100644 server/migrations/0017_tags.sql create mode 100644 server/migrations/0018_achievements.sql create mode 100644 server/migrations/mysql/0001_init.sql create mode 100644 server/migrations/mysql/0002_auth.sql create mode 100644 server/migrations/mysql/0003_vehicles.sql create mode 100644 server/migrations/mysql/0004_grocy_full.sql create mode 100644 server/migrations/mysql/0005_inventory_detail.sql create mode 100644 server/migrations/mysql/0006_unit_conversion.sql create mode 100644 server/migrations/mysql/0007_vehicle_logs.sql create mode 100644 server/migrations/mysql/0008_mileage_and_insurance.sql create mode 100644 server/migrations/mysql/0009_vehicle_powertrain.sql create mode 100644 server/migrations/mysql/0010_operation_logs.sql create mode 100644 server/migrations/mysql/0011_soft_delete.sql create mode 100644 server/migrations/mysql/0012_operation_logs_recovery.sql create mode 100644 server/migrations/mysql/0013_weather_wttr.sql create mode 100644 server/migrations/mysql/0014_grocy_auth.sql create mode 100644 server/migrations/mysql/0015_wash_photos.sql create mode 100644 server/migrations/mysql/0016_vehicle_current_km.sql create mode 100644 server/migrations/mysql/0017_tags.sql create mode 100644 server/migrations/mysql/0018_achievements.sql create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/src/bin/backup.js create mode 100644 server/src/bin/export.js create mode 100644 server/src/bin/grocy-refresh-products.js create mode 100644 server/src/bin/grocy-sync.js create mode 100644 server/src/bin/migrate.js create mode 100644 server/src/bin/reset-all.js create mode 100644 server/src/bin/seed-demo.js create mode 100644 server/src/bin/serve.js create mode 100644 server/src/bin/users.js create mode 100644 server/src/bin/verify.js create mode 100644 server/src/bin/weather.js create mode 100644 server/src/config.js create mode 100644 server/src/db.js create mode 100644 server/src/http.js create mode 100644 server/src/index.js create mode 100644 server/src/middleware/auth.js create mode 100644 server/src/middleware/csrf.js create mode 100644 server/src/middleware/ipRateLimit.js create mode 100644 server/src/routes/achievements.js create mode 100644 server/src/routes/ai.js create mode 100644 server/src/routes/auth.js create mode 100644 server/src/routes/chemicals.js create mode 100644 server/src/routes/extra.js create mode 100644 server/src/routes/insurance.js create mode 100644 server/src/routes/logs.js create mode 100644 server/src/routes/notifications.js create mode 100644 server/src/routes/operationLogs.js create mode 100644 server/src/routes/settings.js create mode 100644 server/src/routes/tags.js create mode 100644 server/src/routes/vehicles.js create mode 100644 server/src/routes/washes.js create mode 100644 server/src/services/aiVision.js create mode 100644 server/src/services/auth.js create mode 100644 server/src/services/backup.js create mode 100644 server/src/services/categoryMap.js create mode 100644 server/src/services/challenge.js create mode 100644 server/src/services/exporter.js create mode 100644 server/src/services/grocy.js create mode 100644 server/src/services/grocyClient.js create mode 100644 server/src/services/grocyProducts.js create mode 100644 server/src/services/grocyWrite.js create mode 100644 server/src/services/monthlyReport.js create mode 100644 server/src/services/operationLog.js create mode 100644 server/src/services/rateLimit.js create mode 100644 server/src/services/weather.js create mode 100644 server/src/setup.js create mode 100644 server/src/swagger.js create mode 100644 server/test/challenge.test.js create mode 100644 server/test/db.keepAlive.test.js create mode 100644 server/test/db.softWhere.test.js create mode 100644 server/test/integration.middleware.test.js create mode 100644 server/test/middleware.auth.test.js create mode 100644 server/test/middleware.csrf.test.js create mode 100644 server/test/middleware.ipRateLimit.test.js create mode 100644 server/test/routes.extra.test.js create mode 100644 server/test/routes.notifications.test.js create mode 100644 server/test/routes.stats.test.js create mode 100644 server/test/routes.tags.test.js create mode 100644 server/test/routes.vehicles.test.js create mode 100644 server/test/setup.js create mode 100644 vitest.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..831249f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# .editorconfig — 统一编辑器配置 +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.vue] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5a8b4c --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# 复制为 .env 后填入实际值(所有项可选;只配需要的功能即可) + +# 服务监听 +PORT=8787 +HOST=127.0.0.1 +NODE_ENV=production + +# 城市(默认) +APP_CITY=Beijing + +# 天气 API(任选其一) +QWECHAT_API_KEY= +QWECHAT_HOST=devapi.qweather.com +OPENWEATHERMAP_API_KEY= + +# Grocy 集成 +GROCY_URL= +GROCY_API_TOKEN= diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..251cf8b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "root": true, + "env": { + "browser": true, + "es2022": true, + "node": true + }, + "parser": "espree", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "ecmaFeatures": { "jsx": false } + }, + "extends": ["eslint:recommended"], + "rules": { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "no-undef": "error", + "no-console": "off", + "no-var": "error", + "prefer-const": "warn", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "no-implicit-coercion": "warn", + "no-throw-literal": "error" + }, + "overrides": [ + { + "files": ["client/src/**/*.vue"], + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "espree" + } + } + ], + "ignorePatterns": ["node_modules/", "client/dist/", "server/storage/", "uploads/", "*.min.js"] +} diff --git a/.gitignore b/.gitignore index 86824e2..afadaed 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ yarn-error.log* *.tar.gz # Mavis -.mavis/ \ No newline at end of file +.mavis/.DS_Store diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..996328a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +# .husky/pre-commit — 提交前自动 lint + format 已暂存文件 +. "$(dirname -- "$0")/_/husky.sh" + +npx --no-install lint-staged diff --git a/.pa11yci.json b/.pa11yci.json new file mode 100644 index 0000000..1096125 --- /dev/null +++ b/.pa11yci.json @@ -0,0 +1,14 @@ +{ + "chromeLaunchConfig": { + "args": ["--no-sandbox", "--disable-setuid-sandbox", "--headless=new"] + }, + "standard": "WCAG2AA", + "runners": ["htmlcs"], + "ignore": [ + "WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.AlignAttr", + "WCAG2AA.Principle1.Guideline1_4.1_4_3.Contrast", + "color-contrast" + ], + "rules": [], + "timeout": 60000 +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..723c064 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,26 @@ +# 依赖 +node_modules/ +client/node_modules/ +server/node_modules/ + +# 构建产物 +client/dist/ +server/storage/ +uploads/ + +# 数据 +server/data/*.sqlite +server/data/*.sqlite-shm +server/data/*.sqlite-wal +server/data/*.db + +# 日志 / 临时 +*.log +.DS_Store +.setup_done +.env +.env.local + +# 备份 +*.zip +*.tar.gz diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fd90265 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "useTabs": false, + "printWidth": 120, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "vueIndentScriptAndStyle": false +} diff --git a/carlog-init.sql b/carlog-init.sql new file mode 100644 index 0000000..b27c2be --- /dev/null +++ b/carlog-init.sql @@ -0,0 +1,459 @@ +-- ============================================================ +-- 洗车管理系统 MySQL 一键初始化(v2.1) +-- 适用 MySQL 8.0+ / utf8mb4 +-- +-- 用法: +-- 1. 宝塔:选 carlog 库 →『导入』→ 选本文件 +-- 2. 命令行:mysql -uroot -p carlog < carlog-init.sql +-- +-- 完全幂等,反复重跑不会破坏数据 +-- ============================================================ + +DROP PROCEDURE IF EXISTS _add_index_if_missing; +CREATE PROCEDURE _add_index_if_missing(IN p_table VARCHAR(64), IN p_index VARCHAR(64), IN p_def TEXT, IN p_unique INT) BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = p_table AND index_name = p_index) THEN + IF p_unique = 1 THEN SET @s = CONCAT('CREATE UNIQUE INDEX ', p_index, ' ON ', p_table, ' ', p_def); + ELSE SET @s = CONCAT('CREATE INDEX ', p_index, ' ON ', p_table, ' ', p_def); END IF; + PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; + END IF; +END; + +DROP PROCEDURE IF EXISTS _try_sql; +CREATE PROCEDURE _try_sql(IN p_sql TEXT) BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN END; + SET @s = p_sql; + PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st; +END; + + +-- >>> 0001_init.sql +-- ========================================================== +-- ============================================================================= +-- 洗车记录系统 - Migration 0001: 基础表 (MySQL 8.x) +-- ============================================================================= +-- ----------------------------------------------------------------------------- +-- 1. chemicals - 药剂字典(Grocy 缓存层) +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS chemicals ( +grocy_product_id VARCHAR(255) NOT NULL, +name VARCHAR(255) NOT NULL, +category VARCHAR(255) DEFAULT NULL, +unit VARCHAR(50) NOT NULL DEFAULT 'ml', +standard_dose DOUBLE DEFAULT NULL, +notes TEXT DEFAULT NULL, +is_active TINYINT(1) NOT NULL DEFAULT 1, +fetched_at DATETIME DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +PRIMARY KEY (grocy_product_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('chemicals', 'idx_chemicals_category', '(category)', 0); +CALL _add_index_if_missing('chemicals', 'idx_chemicals_active', '(is_active)', 0); +CALL _add_index_if_missing('chemicals', 'idx_chemicals_fetched', '(fetched_at)', 0); +-- ----------------------------------------------------------------------------- +-- 2. weather_snapshots - 天气快照 +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS weather_snapshots ( +id INT AUTO_INCREMENT PRIMARY KEY, +snapshot_date VARCHAR(10) NOT NULL, +city VARCHAR(100) NOT NULL, +provider VARCHAR(50) NOT NULL, +temp_c DOUBLE DEFAULT NULL, +humidity INT DEFAULT NULL, +weather_desc VARCHAR(255) DEFAULT NULL, +weather_code VARCHAR(20) DEFAULT NULL, +wind_kph DOUBLE DEFAULT NULL, +precip_mm DOUBLE DEFAULT NULL, +raw_json TEXT DEFAULT NULL, +fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('weather_snapshots', 'uk_weather_city_date', '(city, snapshot_date)', 1); +CALL _add_index_if_missing('weather_snapshots', 'idx_weather_date', '(snapshot_date)', 0); +-- ----------------------------------------------------------------------------- +-- 3. wash_records - 洗车记录 +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS wash_records ( +id INT AUTO_INCREMENT PRIMARY KEY, +wash_date VARCHAR(10) NOT NULL, +wash_type VARCHAR(20) NOT NULL, +weather_snapshot_id INT DEFAULT NULL, +location VARCHAR(255) DEFAULT NULL, +cost DOUBLE NOT NULL DEFAULT 0, +duration_min INT DEFAULT NULL, +notes TEXT DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +CONSTRAINT chk_wash_type CHECK (wash_type IN ('quick','full','detail','other')), +CONSTRAINT fk_wash_weather FOREIGN KEY (weather_snapshot_id) REFERENCES weather_snapshots(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('wash_records', 'idx_wash_records_date', '(wash_date)', 0); +CALL _add_index_if_missing('wash_records', 'idx_wash_records_type', '(wash_type)', 0); +CALL _add_index_if_missing('wash_records', 'idx_wash_records_weather', '(weather_snapshot_id)', 0); +-- ----------------------------------------------------------------------------- +-- 4. chemical_usage - 药剂消耗 +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS chemical_usage ( +id INT AUTO_INCREMENT PRIMARY KEY, +usage_date VARCHAR(10) NOT NULL, +chemical_id VARCHAR(255) NOT NULL, +amount DOUBLE NOT NULL, +wash_record_id INT DEFAULT NULL, +notes TEXT DEFAULT NULL, +sync_status VARCHAR(20) NOT NULL DEFAULT 'pending', +sync_at DATETIME DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +CONSTRAINT chk_sync_status CHECK (sync_status IN ('pending','synced','failed')), +CONSTRAINT fk_usage_chemical FOREIGN KEY (chemical_id) REFERENCES chemicals(grocy_product_id) ON DELETE RESTRICT, +CONSTRAINT fk_usage_wash FOREIGN KEY (wash_record_id) REFERENCES wash_records(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('chemical_usage', 'idx_usage_date', '(usage_date)', 0); +CALL _add_index_if_missing('chemical_usage', 'idx_usage_chemical', '(chemical_id)', 0); +CALL _add_index_if_missing('chemical_usage', 'idx_usage_wash', '(wash_record_id)', 0); +CALL _add_index_if_missing('chemical_usage', 'idx_usage_sync', '(sync_status)', 0); +-- ----------------------------------------------------------------------------- +-- 5. settings - 运行时配置 KV +-- ----------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS settings ( +`key` VARCHAR(100) NOT NULL PRIMARY KEY, +value TEXT DEFAULT NULL, +is_secret TINYINT(1) NOT NULL DEFAULT 0, +description TEXT DEFAULT NULL, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES +('app_city', NULL, 0, '所在城市(用于天气查询)'), +('app_timezone', 'Asia/Shanghai', 0, '时区'), +('grocy_url', NULL, 0, 'Grocy 实例 URL'), +('grocy_api_token', NULL, 1, 'Grocy REST API token'), +('backup_keep_count', '10', 0, '本地备份保留份数'), +('backup_dir', 'storage/backups', 0, '备份输出目录'); +-- ----------------------------------------------------------------------------- +-- 6. views +-- ----------------------------------------------------------------------------- +DROP VIEW IF EXISTS v_wash_monthly_count; +CALL _try_sql('CREATE VIEW v_wash_monthly_count AS SELECT SUBSTRING(wash_date, 1, 7) AS month, COUNT(*) AS wash_count, SUM(COALESCE(cost, 0)) AS total_cost FROM wash_records GROUP BY SUBSTRING(wash_date, 1, 7) ORDER BY month DESC'); +DROP VIEW IF EXISTS v_chemical_monthly_usage; +CALL _try_sql('CREATE VIEW v_chemical_monthly_usage AS SELECT SUBSTRING(cu.usage_date, 1, 7) AS month, c.grocy_product_id AS grocy_product_id, c.name AS chemical_name, c.unit AS unit, SUM(cu.amount) AS total_amount, COUNT(*) AS usage_count FROM chemical_usage cu JOIN chemicals c ON c.grocy_product_id = cu.chemical_id GROUP BY SUBSTRING(cu.usage_date, 1, 7), c.grocy_product_id ORDER BY month DESC, total_amount DESC'); +DROP VIEW IF EXISTS v_last_wash; +CALL _try_sql('CREATE VIEW v_last_wash AS SELECT id AS wash_id, wash_date, wash_type, DATEDIFF(NOW(), STR_TO_DATE(wash_date, ''%Y-%m-%d'')) AS days_since FROM wash_records ORDER BY wash_date DESC, id DESC LIMIT 1'); + +-- >>> 0002_auth.sql +-- ========================================================== +-- 0002_auth.sql - 用户认证 + 防撞库 (MySQL) +CREATE TABLE IF NOT EXISTS users ( +id INT AUTO_INCREMENT PRIMARY KEY, +username VARCHAR(50) NOT NULL UNIQUE, +password_hash VARCHAR(255) NOT NULL, +role VARCHAR(20) NOT NULL DEFAULT 'user', +is_active TINYINT(1) NOT NULL DEFAULT 1, +last_login_at DATETIME DEFAULT NULL, +last_login_ip VARCHAR(45) DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +CONSTRAINT chk_role CHECK (role IN ('user','admin')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('users', 'idx_users_active', '(is_active)', 0); +CREATE TABLE IF NOT EXISTS login_attempts ( +id INT AUTO_INCREMENT PRIMARY KEY, +attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +ip_address VARCHAR(45) NOT NULL, +username VARCHAR(50) NOT NULL, +success TINYINT(1) NOT NULL, +user_agent VARCHAR(500) DEFAULT NULL, +failure_reason VARCHAR(100) DEFAULT NULL, +CONSTRAINT chk_success CHECK (success IN (0, 1)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('login_attempts', 'idx_attempts_ip_time', '(ip_address, attempted_at)', 0); +CALL _add_index_if_missing('login_attempts', 'idx_attempts_user_time', '(username, attempted_at)', 0); +CALL _add_index_if_missing('login_attempts', 'idx_attempts_time', '(attempted_at)', 0); +CREATE TABLE IF NOT EXISTS auth_locks ( +lock_key VARCHAR(100) PRIMARY KEY, +lock_type VARCHAR(10) NOT NULL, +target VARCHAR(50) NOT NULL, +locked_until DATETIME NOT NULL, +reason VARCHAR(255) DEFAULT NULL, +attempts INT NOT NULL DEFAULT 0, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +CONSTRAINT chk_lock_type CHECK (lock_type IN ('ip','user')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('auth_locks', 'idx_locks_until', '(locked_until)', 0); +INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES +('session_lifetime_days', '30', 0, '登录 session 有效期(天)'), +('session_cookie_secure', 'auto', 0, 'Cookie secure 标志:true/false/auto'), +('login_max_failures_ip', '5', 0, '每 IP 允许的最大连续失败次数'), +('login_max_failures_user', '5', 0, '每用户名允许的最大连续失败次数'), +('login_lock_minutes_ip', '15', 0, 'IP 级别锁定时长(分钟)'), +('login_lock_minutes_user', '30', 0, '用户名级别锁定时长(分钟)'), +('login_global_max_failures', '10', 0, '触发全局 IP 封锁的失败次数'), +('login_global_lock_hours', '1', 0, '全局 IP 封锁时长(小时)'), +('login_attempts_retention_days', '30', 0, 'login_attempts 保留天数'), +('csrf_token_lifetime_hours', '12', 0, 'CSRF token 有效期(小时)'), +('bcrypt_cost', '12', 0, 'bcrypt cost factor'); + +-- >>> 0003_vehicles.sql +-- ========================================================== +-- 0003_vehicles.sql - 车辆管理 (MySQL) +CREATE TABLE IF NOT EXISTS vehicles ( +id INT AUTO_INCREMENT PRIMARY KEY, +name VARCHAR(100) NOT NULL, +plate VARCHAR(20) DEFAULT NULL, +type VARCHAR(20) NOT NULL DEFAULT 'car', +color VARCHAR(30) DEFAULT NULL, +notes TEXT DEFAULT NULL, +is_active TINYINT(1) NOT NULL DEFAULT 1, +sort_order INT NOT NULL DEFAULT 0, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +CONSTRAINT chk_vehicle_type CHECK (type IN ('car','suv','mpv','truck','other')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('vehicles', 'idx_vehicles_active', '(is_active)', 0); +CALL _add_index_if_missing('vehicles', 'idx_vehicles_sort', '(sort_order)', 0); +CALL _try_sql('ALTER TABLE wash_records ADD COLUMN vehicle_id INT DEFAULT NULL'); +CALL _add_index_if_missing('wash_records', 'idx_wash_records_vehicle', '(vehicle_id)', 0); +DROP VIEW IF EXISTS v_last_wash; +CALL _try_sql('CREATE VIEW v_last_wash AS SELECT w.id AS wash_id, w.wash_date, w.wash_type, w.vehicle_id, v.name AS vehicle_name, DATEDIFF(NOW(), STR_TO_DATE(w.wash_date, ''%Y-%m-%d'')) AS days_since FROM wash_records w LEFT JOIN vehicles v ON v.id = w.vehicle_id ORDER BY w.wash_date DESC, w.id DESC LIMIT 1'); + +-- >>> 0004_grocy_full.sql +-- ========================================================== +-- 0004_grocy_full.sql - Grocy 主数据同步字段 (MySQL) +CALL _try_sql('ALTER TABLE chemicals ADD COLUMN description TEXT DEFAULT NULL, ADD COLUMN current_amount DOUBLE NOT NULL DEFAULT 0, ADD COLUMN current_value DOUBLE NOT NULL DEFAULT 0, ADD COLUMN min_stock_amount DOUBLE NOT NULL DEFAULT 0, ADD COLUMN best_before_date VARCHAR(20) DEFAULT NULL, ADD COLUMN location VARCHAR(255) DEFAULT NULL, ADD COLUMN product_group_id INT DEFAULT NULL, ADD COLUMN qu_id INT DEFAULT NULL, ADD COLUMN location_id INT DEFAULT NULL, ADD COLUMN picture_file_name VARCHAR(255) DEFAULT NULL, ADD COLUMN last_synced_at DATETIME DEFAULT NULL'); +CALL _add_index_if_missing('chemicals', 'idx_chem_amount', '(current_amount)', 0); +CALL _add_index_if_missing('chemicals', 'idx_chem_pg', '(product_group_id)', 0); +CALL _add_index_if_missing('chemicals', 'idx_chem_synced', '(last_synced_at)', 0); +INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES +('grocy_sync_batch', '50', 0, 'Grocy 扣减同步每批条数'), +('grocy_low_stock_pct', '20', 0, '低库存阈值(百分比)'), +('grocy_pull_auto', '0', 0, 'Grocy 拉取模式:0=手动,1=启动时自动拉'); + +-- >>> 0005_inventory_detail.sql +-- ========================================================== +-- 0005_inventory_detail.sql (MySQL) +CALL _try_sql('ALTER TABLE chemicals ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT ''manual'', ADD COLUMN grocy_last_pulled_at DATETIME DEFAULT NULL'); +CREATE TABLE IF NOT EXISTS category_mappings ( +grocy_group_id INT PRIMARY KEY, +display_name VARCHAR(100) NOT NULL, +sort_order INT NOT NULL DEFAULT 0, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS chemical_inventory_log ( +id INT AUTO_INCREMENT PRIMARY KEY, +chemical_id VARCHAR(255) NOT NULL, +change_type VARCHAR(20) NOT NULL, +amount_delta DOUBLE NOT NULL, +amount_after DOUBLE DEFAULT NULL, +source VARCHAR(20) NOT NULL DEFAULT 'local', +source_ref VARCHAR(255) DEFAULT NULL, +note TEXT DEFAULT NULL, +occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +CONSTRAINT chk_change_type CHECK (change_type IN ('purchase','consume','inventory','transfer','adjust')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('chemical_inventory_log', 'idx_invlog_chem', '(chemical_id, occurred_at DESC)', 0); +CALL _add_index_if_missing('chemical_inventory_log', 'idx_invlog_type', '(change_type)', 0); +INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES +('grocy_categories_json', '[]', 0, 'Grocy 分类映射 JSON'); + +-- >>> 0006_unit_conversion.sql +-- ========================================================== +-- 0006_unit_conversion.sql (MySQL) +CALL _try_sql('ALTER TABLE chemicals ADD COLUMN qu_factor DOUBLE NOT NULL DEFAULT 1.0, ADD COLUMN consume_unit_id INT DEFAULT NULL, ADD COLUMN consume_unit_name VARCHAR(100) DEFAULT NULL'); +CALL _try_sql('ALTER TABLE chemical_usage ADD COLUMN unit VARCHAR(50) DEFAULT NULL, ADD COLUMN stock_amount DOUBLE DEFAULT NULL, ADD COLUMN consume_unit_id INT DEFAULT NULL'); + +-- >>> 0007_vehicle_logs.sql +-- ========================================================== +-- 0007_vehicle_logs.sql (MySQL) +CREATE TABLE IF NOT EXISTS maintenance_records ( +id INT AUTO_INCREMENT PRIMARY KEY, +vehicle_id INT NOT NULL, +maint_date VARCHAR(10) NOT NULL, +odometer_km INT DEFAULT NULL, +total_cost DOUBLE NOT NULL DEFAULT 0, +shop VARCHAR(255) DEFAULT NULL, +items_json JSON NOT NULL DEFAULT ('[]'), +next_due_date VARCHAR(10) DEFAULT NULL, +next_due_km INT DEFAULT NULL, +notes TEXT DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('maintenance_records', 'idx_maint_vehicle_date', '(vehicle_id, maint_date DESC)', 0); +CALL _add_index_if_missing('maintenance_records', 'idx_maint_date', '(maint_date DESC)', 0); +CREATE TABLE IF NOT EXISTS refuel_records ( +id INT AUTO_INCREMENT PRIMARY KEY, +vehicle_id INT NOT NULL, +refuel_date VARCHAR(10) NOT NULL, +odometer_km INT DEFAULT NULL, +liters DOUBLE NOT NULL, +price_per_liter DOUBLE DEFAULT NULL, +total_cost DOUBLE NOT NULL, +fuel_type VARCHAR(20) DEFAULT NULL, +is_full TINYINT(1) NOT NULL DEFAULT 0, +station VARCHAR(255) DEFAULT NULL, +notes TEXT DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('refuel_records', 'idx_refuel_vehicle_date', '(vehicle_id, refuel_date DESC)', 0); +CALL _add_index_if_missing('refuel_records', 'idx_refuel_date', '(refuel_date DESC)', 0); +CREATE TABLE IF NOT EXISTS charging_records ( +id INT AUTO_INCREMENT PRIMARY KEY, +vehicle_id INT NOT NULL, +charge_date VARCHAR(10) NOT NULL, +odometer_km INT DEFAULT NULL, +kwh DOUBLE NOT NULL, +price_per_kwh DOUBLE DEFAULT NULL, +total_cost DOUBLE NOT NULL, +charge_type VARCHAR(20) DEFAULT NULL, +start_soc INT DEFAULT NULL, +end_soc INT DEFAULT NULL, +station VARCHAR(255) DEFAULT NULL, +notes TEXT DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('charging_records', 'idx_charging_vehicle_date', '(vehicle_id, charge_date DESC)', 0); +CALL _add_index_if_missing('charging_records', 'idx_charging_date', '(charge_date DESC)', 0); +DROP VIEW IF EXISTS v_recent_logs; +CALL _try_sql('CREATE VIEW v_recent_logs AS SELECT ''maintenance'' AS log_type, id, vehicle_id, maint_date AS log_date, total_cost, odometer_km, shop AS location FROM maintenance_records UNION ALL SELECT ''refuel'' AS log_type, id, vehicle_id, refuel_date AS log_date, total_cost, odometer_km, station AS location FROM refuel_records UNION ALL SELECT ''charging'' AS log_type, id, vehicle_id, charge_date AS log_date, total_cost, odometer_km, station AS location FROM charging_records'); + +-- >>> 0008_mileage_and_insurance.sql +-- ========================================================== +-- 0008_mileage_and_insurance.sql (MySQL) +CALL _try_sql('ALTER TABLE maintenance_records ADD COLUMN ev_km INT DEFAULT NULL, ADD COLUMN hev_km INT DEFAULT NULL'); +CREATE TABLE IF NOT EXISTS insurance_records ( +id INT AUTO_INCREMENT PRIMARY KEY, +vehicle_id INT NOT NULL, +insurance_type VARCHAR(50) NOT NULL, +company VARCHAR(100) DEFAULT NULL, +policy_no VARCHAR(100) DEFAULT NULL, +start_date VARCHAR(10) NOT NULL, +end_date VARCHAR(10) NOT NULL, +premium DOUBLE DEFAULT NULL, +coverage_amount DOUBLE DEFAULT NULL, +notes TEXT DEFAULT NULL, +attachment_path VARCHAR(500) DEFAULT NULL, +attachment_name VARCHAR(255) DEFAULT NULL, +attachment_mime VARCHAR(100) DEFAULT NULL, +attachment_size INT DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('insurance_records', 'idx_insurance_vehicle', '(vehicle_id)', 0); +CALL _add_index_if_missing('insurance_records', 'idx_insurance_end_date', '(end_date)', 0); + +-- >>> 0009_vehicle_powertrain.sql +-- ========================================================== +-- 0009_vehicle_powertrain.sql (MySQL) +CALL _try_sql('ALTER TABLE vehicles ADD COLUMN powertrain VARCHAR(10) NOT NULL DEFAULT ''ice'''); + +-- >>> 0010_operation_logs.sql +-- ========================================================== +-- 0010_operation_logs.sql (MySQL) +CREATE TABLE IF NOT EXISTS operation_logs ( +id INT AUTO_INCREMENT PRIMARY KEY, +user_id INT DEFAULT NULL, +username VARCHAR(50) DEFAULT NULL, +action VARCHAR(50) NOT NULL, +target_type VARCHAR(50) NOT NULL, +target_ids TEXT NOT NULL, +target_summary TEXT DEFAULT NULL, +detail_json TEXT DEFAULT NULL, +ip VARCHAR(45) DEFAULT NULL, +user_agent VARCHAR(500) DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('operation_logs', 'idx_oplog_created', '(created_at DESC)', 0); +CALL _add_index_if_missing('operation_logs', 'idx_oplog_user_time', '(username, created_at DESC)', 0); +CALL _add_index_if_missing('operation_logs', 'idx_oplog_action', '(action, target_type, created_at DESC)', 0); + +-- >>> 0011_soft_delete.sql +-- ========================================================== +-- 0011_soft_delete.sql (MySQL) +CALL _try_sql('ALTER TABLE vehicles ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE wash_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE chemical_usage ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE maintenance_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE refuel_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE charging_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _try_sql('ALTER TABLE insurance_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0'); +CALL _add_index_if_missing('vehicles', 'ix_vehicles_is_deleted', '(is_deleted)', 0); +CALL _add_index_if_missing('wash_records', 'ix_wash_records_is_deleted', '(is_deleted)', 0); +CALL _add_index_if_missing('maintenance_records', 'ix_maintenance_is_deleted', '(is_deleted)', 0); +CALL _add_index_if_missing('refuel_records', 'ix_refuel_is_deleted', '(is_deleted)', 0); +CALL _add_index_if_missing('charging_records', 'ix_charging_is_deleted', '(is_deleted)', 0); +CALL _add_index_if_missing('insurance_records', 'ix_insurance_is_deleted', '(is_deleted)', 0); + +-- >>> 0012_operation_logs_recovery.sql +-- ========================================================== +-- 0012_operation_logs_recovery.sql (MySQL) +CALL _try_sql('ALTER TABLE operation_logs ADD COLUMN recovered_at DATETIME DEFAULT NULL'); + +-- >>> 0013_weather_wttr.sql +-- ========================================================== +-- 0013_weather_wttr.sql (MySQL) +-- 删除旧 CHECK 并重建(MySQL 允许 ALTER TABLE 改 CHECK,但为保险用 ALTER COLUMN) +CALL _try_sql('ALTER TABLE weather_snapshots MODIFY COLUMN provider VARCHAR(50) NOT NULL'); + +-- >>> 0014_grocy_auth.sql +-- ========================================================== +-- 0014_grocy_auth.sql (MySQL) +INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES +('grocy_username', '', 1, 'Grocy 用户名(session cookie 鉴权)'), +('grocy_password', '', 1, 'Grocy 密码(session cookie 鉴权)'), +('app_city_default', '', 0, '天气默认城市(永久生效)'); +CREATE TABLE IF NOT EXISTS grocy_sync_logs ( +id INT AUTO_INCREMENT PRIMARY KEY, +action VARCHAR(50) NOT NULL, +status VARCHAR(20) NOT NULL, +ok_count INT NOT NULL DEFAULT 0, +fail_count INT NOT NULL DEFAULT 0, +detail TEXT DEFAULT NULL, +started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +finished_at DATETIME DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CALL _add_index_if_missing('grocy_sync_logs', 'idx_grocy_sync_logs_action', '(action)', 0); +CALL _add_index_if_missing('grocy_sync_logs', 'idx_grocy_sync_logs_started', '(started_at DESC)', 0); + +-- >>> 0015_wash_photos.sql +-- ========================================================== +-- 0015_wash_photos.sql (MySQL) — 洗车对比照(before / after / detail) +CREATE TABLE IF NOT EXISTS wash_photos ( +id INT AUTO_INCREMENT PRIMARY KEY, +wash_id INT NOT NULL, +photo_type VARCHAR(20) NOT NULL DEFAULT 'detail', -- before / after / detail / scene +file_path VARCHAR(500) NOT NULL, +file_name VARCHAR(255) NOT NULL, +mime_type VARCHAR(50) DEFAULT NULL, +file_size INT DEFAULT NULL, +width INT DEFAULT NULL, +height INT DEFAULT NULL, +caption VARCHAR(255) DEFAULT NULL, +sort_order INT NOT NULL DEFAULT 0, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +is_deleted TINYINT(1) NOT NULL DEFAULT 0, +INDEX idx_wash_photos_wash (wash_id, is_deleted), +INDEX idx_wash_photos_type (photo_type), +INDEX idx_wash_photos_created (created_at DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP PROCEDURE IF EXISTS _add_index_if_missing; +DROP PROCEDURE IF EXISTS _try_sql; + +CREATE TABLE IF NOT EXISTS schema_migrations (filename VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0001_init.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0002_auth.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0003_vehicles.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0004_grocy_full.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0005_inventory_detail.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0006_unit_conversion.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0007_vehicle_logs.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0008_mileage_and_insurance.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0009_vehicle_powertrain.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0010_operation_logs.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0011_soft_delete.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0012_operation_logs_recovery.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0013_weather_wttr.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0014_grocy_auth.sql'); +INSERT IGNORE INTO schema_migrations (filename) VALUES ('0015_wash_photos.sql'); diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..577acdc --- /dev/null +++ b/client/index.html @@ -0,0 +1,42 @@ + + + + + + CarLog 车记 · 个人爱车管理 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..0eb3572 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,6905 @@ +{ + "name": "carwash-client", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "carwash-client", + "version": "2.0.0", + "dependencies": { + "axios": "^1.7.7", + "chart.js": "^4.4.4", + "pinia": "^2.2.4", + "vue": "^3.5.12", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "sharp": "^0.35.1", + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.3.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@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/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.1.tgz", + "integrity": "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.1.tgz", + "integrity": "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.1.tgz", + "integrity": "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.1.tgz", + "integrity": "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.1.tgz", + "integrity": "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.1.tgz", + "integrity": "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.1.tgz", + "integrity": "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.1.tgz", + "integrity": "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.1.tgz", + "integrity": "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.1.tgz", + "integrity": "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.1.tgz", + "integrity": "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.35.1.tgz", + "integrity": "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.1.tgz", + "integrity": "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.1.tgz", + "integrity": "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.1.tgz", + "integrity": "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.1.tgz", + "integrity": "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@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/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmmirror.com/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "vue": "3.5.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.375", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.1.tgz", + "integrity": "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/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/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/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/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-7.0.6.tgz", + "integrity": "sha512-ATTK5Q4gFVg0YDp1my2vqygyvhcklD/UV5GIlYHooGTn/NogJqIzpetkD6E5kmuVULqz/S9inUL25XcAgDRJQg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.1", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.35.1.tgz", + "integrity": "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.1", + "@img/sharp-darwin-x64": "0.35.1", + "@img/sharp-freebsd-wasm32": "0.35.1", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.1", + "@img/sharp-linux-arm64": "0.35.1", + "@img/sharp-linux-ppc64": "0.35.1", + "@img/sharp-linux-riscv64": "0.35.1", + "@img/sharp-linux-s390x": "0.35.1", + "@img/sharp-linux-x64": "0.35.1", + "@img/sharp-linuxmusl-arm64": "0.35.1", + "@img/sharp-linuxmusl-x64": "0.35.1", + "@img/sharp-webcontainers-wasm32": "0.35.1", + "@img/sharp-win32-arm64": "0.35.1", + "@img/sharp-win32-ia32": "0.35.1", + "@img/sharp-win32-x64": "0.35.1" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/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/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-pwa": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", + "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..1224a2c --- /dev/null +++ b/client/package.json @@ -0,0 +1,24 @@ +{ + "name": "carwash-client", + "version": "2.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.7", + "chart.js": "^4.4.4", + "pinia": "^2.2.4", + "vue": "^3.5.12", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "sharp": "^0.35.1", + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.3.0" + } +} diff --git a/client/public/favicon-16x16.png b/client/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5b535cc955eec321eb9df22ee3201327adcdad GIT binary patch literal 578 zcmV-I0=@l-P)L(@jXzaU93--)R@|VRPnlWe;jjOD7YmyD3fjJ9H0Ku4oK2v%#ZdPypz4`NWsTBciO_H+jM5y6 zE6qVk7y~k)JHIOF94Y=8cZX1#gVdX6QJ7{VQJUQ3NYZGJv)38RW0f_6$`YorH=M`y zT><2s?~rcrcGAu2s~A(o$hgZLjTKlKXnH+;bPk^Ye+ zS3OG@q-Ytg>zG6oV!d;oe)lprpRD1VNt4d}ps?m*86Nmk3_Xl-^vW!C?Jp69$#0`u zR8p-{5K(ySo%;42(p%Xwkj=tI=J&qKvw$eP^S!YzjE=7{_&7uJjZGfC_=?-Ng8SL$ z?RCq$$)SNX_xXD zptkjS5w14)Vj?$HfBGal$-CkfA615@E|D9_+*HX;)A0&$yz01D5=M{gC;v=~CSh;s QbpQYW07*qoM6N<$f_9<}kN^Mx literal 0 HcmV?d00001 diff --git a/client/public/favicon-32x32.png b/client/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..6493eeb93f6becbff2644eb55b73dbde220dd313 GIT binary patch literal 1069 zcmV+|1k(G7P)@i1NVLXBSPqN6g{ ziPDx07^SdoqKT3W1eM5znq>s1S)#JcOs}R1grscB*r4oC28AA=h|mHRzAjr2xG?%R z&xdYiJp!}+Cr^6uy>EZd^L*d?y!kWskMe7=Rj&8e$@Shrg^u&fYdFYtOOWdpA+P=u zvT6@xM;9PFIuBXZ9HdolNGn~CR?b3NF@x-iKahQdVj9_pe~0Ao6eQ(NNXjQ6(M~|5 zb@;_<+n_{ktY-Rt|_Tx)A@|rut3cVK!0zYB6SnGgTV~1E{gIH~aux#8cEVJi4 zsyvBsk$RjDp=!*`cY@lUY`Caw45Bg%gry^TCa>|D)&t+!u#gaZhhZu9k@;j zqz;~SUfWelSHOFQlm%-`vN7LS4j-600000NkvXXu0mjfcoqKt literal 0 HcmV?d00001 diff --git a/client/public/pwa-icon.svg b/client/public/pwa-icon.svg new file mode 100644 index 0000000..8fe501a --- /dev/null +++ b/client/public/pwa-icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + CL + diff --git a/client/public/pwa/apple-touch-icon.png b/client/public/pwa/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5d86da70db0a1e63f3bb59a6dafa0e1a5ecdd7d6 GIT binary patch literal 5894 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 literal 0 HcmV?d00001 diff --git a/client/public/pwa/pwa-512x512.png b/client/public/pwa/pwa-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..aefb992b62b6321bbf05697810701d8367abf577 GIT binary patch literal 21396 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 Ub5{*;n1+GD)78&qol`;+0J?|e@&Et; literal 0 HcmV?d00001 diff --git a/client/public/pwa/pwa-maskable-512x512.png b/client/public/pwa/pwa-maskable-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..84769b37e3c1629b27003c5be2e9de0be3a0de36 GIT binary patch literal 16660 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 XTwb`*Z59EPLl+g*&ZnF;e*Av`EocP^ literal 0 HcmV?d00001 diff --git a/client/scripts/check-pwa.mjs b/client/scripts/check-pwa.mjs new file mode 100644 index 0000000..da3c11a --- /dev/null +++ b/client/scripts/check-pwa.mjs @@ -0,0 +1,166 @@ +/** + * PWA 安装性验证脚本 + * 检查: + * 1. manifest.webmanifest 合法 + * 2. Service Worker 注册成功 + * 3. 图标全部能加载 + * 4. PWA 必需字段(name, icons[192/512], start_url, display, theme_color, background_color) + * 5. 离线 fallback(/offline 或 navigateFallback 命中) + */ +import puppeteer from 'puppeteer'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +const URL_TO_TEST = process.env.PWA_URL || 'http://localhost:4173/login'; +const CHROME_PATH = + process.env.CHROME_PATH || + '/Users/yabozi/.cache/puppeteer/chrome/mac-148.0.7778.97/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'; + +const checks = []; +let pass = 0; +let fail = 0; +function ok(name, detail) { + checks.push({ status: '✅', name, detail }); + pass++; +} +function ko(name, detail) { + checks.push({ status: '❌', name, detail }); + fail++; +} + +async function fetchStatus(url) { + try { + const r = await fetch(url, { redirect: 'follow' }); + return r.status; + } catch (e) { + return 0; + } +} + +async function main() { + console.log(`🔍 PWA check: ${URL_TO_TEST}\n`); + + // 1. manifest 文件可访问 + 合法 JSON + const manifestUrl = new URL('/manifest.webmanifest', URL_TO_TEST).toString(); + const manifestStatus = await fetchStatus(manifestUrl); + if (manifestStatus === 200) { + ok('manifest 200', manifestUrl); + const r = await fetch(manifestUrl); + const m = await r.json(); + const required = ['name', 'short_name', 'start_url', 'display', 'icons']; + const missing = required.filter((k) => !m[k]); + if (missing.length === 0) { + ok('manifest 必备字段', Object.keys(m).join(', ')); + } else { + ko('manifest 必备字段缺失', missing.join(', ')); + } + // 图标尺寸 + const sizes = (m.icons || []).map((i) => i.sizes).filter(Boolean); + const has192 = sizes.some((s) => s.includes('192')); + const has512 = sizes.some((s) => s.includes('512')); + const hasMaskable = (m.icons || []).some((i) => i.purpose === 'maskable'); + const hasApple = (m.icons || []).some((i) => + (i.src || '').includes('apple-touch') + ); + if (has192 && has512) ok('icons 192+512', sizes.join(', ')); + else ko('icons 192/512 缺失', sizes.join(', ')); + if (hasMaskable) ok('maskable icon', 'present'); + else ko('maskable icon', 'missing'); + if (hasApple) ok('apple-touch icon', 'present'); + else ko('apple-touch icon', 'missing'); + } else { + ko('manifest 不可访问', `status=${manifestStatus}, url=${manifestUrl}`); + } + + // 2. Service Worker 注册 + const browser = await puppeteer.launch({ + executablePath: CHROME_PATH, + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + `--user-data-dir=/tmp/lh-cache-pwa-check-${Date.now()}`, + ], + }); + const page = await browser.newPage(); + const swLogs = []; + page.on('console', (m) => { + const t = m.text(); + if (t.includes('[PWA]') || t.includes('ServiceWorker')) swLogs.push(t); + }); + + await page.goto(URL_TO_TEST, { waitUntil: 'networkidle0', timeout: 30000 }); + // 等几秒给 SW 机会注册 + await new Promise((r) => setTimeout(r, 3000)); + + const swInfo = await page.evaluate(async () => { + if (!('serviceWorker' in navigator)) return { supported: false }; + const regs = await navigator.serviceWorker.getRegistrations(); + return { + supported: true, + count: regs.length, + scopes: regs.map((r) => r.scope), + active: regs.map((r) => !!r.active), + scripts: regs.map((r) => r.active && r.active.scriptURL), + }; + }); + if (swInfo.supported && swInfo.count > 0 && swInfo.active.every(Boolean)) { + ok('Service Worker 已注册', `scope=${swInfo.scopes.join(',')}`); + } else if (swInfo.supported && swInfo.count > 0) { + ko('Service Worker 未激活', JSON.stringify(swInfo)); + } else { + ko('Service Worker 未注册', 'getRegistrations() 为空'); + } + + // 3. 关键 meta 标签 + const meta = await page.evaluate(() => { + const get = (sel) => document.querySelector(sel)?.getAttribute('content') || null; + return { + themeColor: get('meta[name="theme-color"]'), + appleCapable: get('meta[name="apple-mobile-web-app-capable"]'), + appleTitle: get('meta[name="apple-mobile-web-app-title"]'), + viewport: get('meta[name="viewport"]'), + manifestLink: !!document.querySelector('link[rel="manifest"]'), + appleTouchIcon: !!document.querySelector('link[rel="apple-touch-icon"]'), + }; + }); + if (meta.themeColor) ok('theme-color', meta.themeColor); + else ko('theme-color', 'missing'); + if (meta.appleCapable === 'yes') ok('apple-mobile-web-app-capable', 'yes'); + else ko('apple-mobile-web-app-capable', meta.appleCapable || 'missing'); + if (meta.manifestLink) ok('manifest link', 'present'); + else ko('manifest link', 'missing'); + if (meta.appleTouchIcon) ok('apple-touch-icon link', 'present'); + else ko('apple-touch-icon link', 'missing'); + + // 4. SW 日志确认 offline ready + const offlineReady = swLogs.some((l) => l.includes('离线缓存就绪') || l.includes('offline')); + if (offlineReady) ok('PWA offline log', '检测到 offline ready'); + // 离线就绪不是强校验,标 warn 即可 + if (!offlineReady) { + checks.push({ + status: '⚠️', + name: 'PWA offline 日志', + detail: '运行后无离线 ready 日志(可能 SW 还没预缓存完,可忽略)', + }); + } + + await browser.close(); + + // 输出 + for (const c of checks) { + console.log(`${c.status} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); + } + console.log(`\n总计: ✅ ${pass} ❌ ${fail}`); + process.exit(fail > 0 ? 1 : 0); +} + +main().catch((e) => { + console.error('PWA check 失败:', e); + process.exit(2); +}); diff --git a/client/scripts/gen-pwa-icons.mjs b/client/scripts/gen-pwa-icons.mjs new file mode 100644 index 0000000..8821788 --- /dev/null +++ b/client/scripts/gen-pwa-icons.mjs @@ -0,0 +1,42 @@ +// 用 sharp 把 SVG 转为多尺寸 PNG +import sharp from 'sharp'; +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const svg = await readFile(path.join(__dirname, '../public/pwa-icon.svg')); + +const targets = [ + // 标准 PWA icons + { out: 'public/pwa/pwa-192x192.png', size: 192, type: 'normal' }, + { out: 'public/pwa/pwa-512x512.png', size: 512, type: 'normal' }, + // maskable:四周留 20% 安全区,logo 居中缩到 60% + { out: 'public/pwa/pwa-maskable-512x512.png', size: 512, type: 'maskable' }, + // apple touch + { out: 'public/pwa/apple-touch-icon.png', size: 180, type: 'normal' }, + // favicon + { out: 'public/favicon-32x32.png', size: 32, type: 'normal' }, + { out: 'public/favicon-16x16.png', size: 16, type: 'normal' }, +]; + +const bgColor = { r: 27, g: 110, b: 243 }; // 渐变起始色 #1B6EF3 + +for (const { out, size, type } of targets) { + let buffer = await sharp(svg).resize(size, size).png().toBuffer(); + + if (type === 'maskable') { + // maskable 重新画:蓝底全填 + logo 60% + const inner = await sharp(svg).resize(Math.floor(size * 0.6), Math.floor(size * 0.6)).png().toBuffer(); + buffer = await sharp({ + create: { width: size, height: size, channels: 4, background: bgColor }, + }) + .composite([{ input: inner, gravity: 'center' }]) + .png() + .toBuffer(); + } + await writeFile(path.join(__dirname, '..', out), buffer); + console.log('✓', out, `${size}x${size}`, type); +} + +console.log('Done.'); diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 0000000..379f6f1 --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/client/src/api/ai.js b/client/src/api/ai.js new file mode 100644 index 0000000..8a5ba92 --- /dev/null +++ b/client/src/api/ai.js @@ -0,0 +1,19 @@ +// client/src/api/ai.js — AI 截图识别 +import http from './client'; + +export const getConfig = () => http.get('/ai/config'); +export const saveConfig = (data) => http.post('/ai/config', data); +export const test = (data) => http.post('/ai/test', data || {}); + +// 上传图片,返回 { image_id, url, name, size, mime } +export const uploadImage = (file) => { + const fd = new FormData(); + fd.append('file', file); + return http.post('/ai/upload', fd, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); +}; + +// 识别图片 +// type: 'wash' | 'refuel' | 'charge' | 'maint' | 'insurance' +export const recognize = (image_id, type) => http.post('/ai/recognize', { image_id, type }); diff --git a/client/src/api/auth.js b/client/src/api/auth.js new file mode 100644 index 0000000..5e190fa --- /dev/null +++ b/client/src/api/auth.js @@ -0,0 +1,8 @@ +// client/src/api/auth.js +import client from './client'; +export const login = (username, password) => client.post('/auth/login', { username, password }); +export const logout = () => client.post('/auth/logout'); +export const changeAccount = (data) => client.post('/auth/account', data); +export const me = () => client.get('/auth/me'); +export const csrf = () => client.get('/auth/csrf'); +export const health = () => client.get('/health'); diff --git a/client/src/api/chemicals.js b/client/src/api/chemicals.js new file mode 100644 index 0000000..85ea3e4 --- /dev/null +++ b/client/src/api/chemicals.js @@ -0,0 +1,16 @@ +// client/src/api/chemicals.js +import client from './client'; +export const list = (params) => client.get('/chemicals', { params }); +export const all = () => client.get('/chemicals/list'); +export const get = (id) => client.get(`/chemicals/${id}`); +export const update = (id, data) => client.put(`/chemicals/${id}`, data); +export const create = (data) => client.post('/chemicals', data); +export const sync = () => client.post('/chemicals/sync'); +export const refreshIds = () => client.post('/chemicals/refresh-ids'); +export const addStock = (id, data) => client.post(`/chemicals/${id}/add`, data); +export const consumeStock = (id, data) => client.post(`/chemicals/${id}/consume`, data); +export const grocySearch = (q) => client.get('/chemicals/grocy-search', { params: { q } }); +export const getCategories = () => client.get('/chemicals/categories'); +export const getCategoryMappings = () => client.get('/chemicals/category-mappings'); +export const saveCategoryMappings = (mappings) => client.post('/chemicals/category-mappings', { mappings }); +export const deleteCategoryMapping = (id) => client.delete(`/chemicals/category-mappings/${id}`); diff --git a/client/src/api/client.js b/client/src/api/client.js new file mode 100644 index 0000000..fa9bc59 --- /dev/null +++ b/client/src/api/client.js @@ -0,0 +1,166 @@ +// client/src/api/client.js — axios 实例,自动带 cookie + CSRF +import axios from 'axios'; +import { useAuthStore } from '../stores/auth'; +import { useDebugStore } from '../stores/debug'; + +const client = axios.create({ + baseURL: '/api', + withCredentials: true, + timeout: 15000, +}); + +// 特殊端点:长 timeout(同步类操作) +const LONG_TIMEOUT_OVERRIDE = { + '/chemicals/sync': 90000, // Grocy 拉取最多 1.5 分钟 + '/grocy/sync': 90000, + '/chemicals/grocy-search': 60000, // Grocy 全局搜索(要拉全量 products 列表) + '/chemicals/refresh-ids': 45000, // 后台轻量同步:只拉一次 /api/objects/products +}; +const origRequest = client.interceptors.request; +client.interceptors.request.use((cfg) => { + const path = (cfg.url || '').replace(/^\/+/, '/'); + if (LONG_TIMEOUT_OVERRIDE[path]) { + cfg.timeout = LONG_TIMEOUT_OVERRIDE[path]; + } + return cfg; +}); + +// 统一处理 server 的 {ok, data, error} 包装: +// 成功 → 把 data 字段提到顶层(业务代码直接拿到) +// 失败 → 抛错,err.response.data = server 的 error 对象 +client.interceptors.response.use( + (r) => { + const body = r.data; + if (body && typeof body === 'object' && 'ok' in body) { + if (body.ok) { + r.data = body.data; // 剥掉包装,client 拿到业务 data + } else { + // ok=false:构造一个类 axios 错误抛出去 + const err = new Error(body.error?.message || '请求失败'); + err.response = { status: r.status, data: body.error || body }; + return Promise.reject(err); + } + } + // 调试模式:记录所有调用 + try { + const debug = useDebugStore(); + if (debug.enabled) { + debug.logCall({ + method: r.config?.method?.toUpperCase(), + url: r.config?.url, + status: r.status, + body: summarize(r.data), + }); + } + } catch {} + return r; + }, + async (err) => { + const status = err.response?.status; + const body = err.response?.data; + // 把 server 的 error 也归一化到 err.response.data + if (body && typeof body === 'object' && 'ok' in body && body.ok === false) { + err.response.data = body.error || body; + } + + // 调试模式:上报到 DebugPanel + try { + const debug = useDebugStore(); + if (debug.enabled) { + const cfg = err.config || {}; + // 预期错误白名单:启动时检查登录状态、未登录访问受保护资源、CSRF 缺失 + const url = cfg.url || ''; + const isExpected = ( + (status === 401 && (url === '/auth/me' || url === '/auth/csrf')) || + (status === 401 && location.pathname === '/login') || + (status === 403 && url === '/auth/me') + ); + if (!isExpected) { + debug.log({ + kind: 'api', + title: `${(cfg.method || 'GET').toUpperCase()} ${cfg.url} → ${status || 'Network Error'}`, + detail: { + method: cfg.method?.toUpperCase(), + url: cfg.url, + baseURL: cfg.baseURL, + fullURL: (cfg.baseURL || '') + (cfg.url || ''), + status, + statusText: err.response?.statusText, + requestHeaders: cfg.headers, + requestBody: cfg.data ? safeParse(cfg.data) : null, + responseBody: err.response?.data, + message: err.message, + }, + }); + } + debug.logCall({ + method: cfg.method?.toUpperCase(), + url: cfg.url, + status: status || 'ERR', + body: err.response?.data, + }); + } + } catch {} + + if (status === 401) { + const auth = useAuthStore(); + auth.clear(); + if (location.pathname !== '/login') { + // 把当前页所有 form 草稿强制刷盘(不丢用户填的数据) + try { + window.dispatchEvent(new CustomEvent('form-draft:flush-all')); + } catch {} + const returnTo = location.pathname + location.search; + location.href = '/login?redirect=' + encodeURIComponent(returnTo) + '&reason=expired'; + } + } + // CSRF token 失效:自动刷新 + 重试原请求。retry 标记挂在 config 上防死循环。 + if (status === 403 && (body?.code === 'CSRF' || body?.error?.code === 'CSRF') && !err.config?.__csrfRetried) { + try { + const auth = useAuthStore(); + await auth.refreshCsrf(); + const retryCfg = { ...err.config, __csrfRetried: true }; + // 用新 token 重新发 + if (auth.csrfToken && ['post', 'put', 'delete', 'patch'].includes(retryCfg.method)) { + retryCfg.headers = { ...(retryCfg.headers || {}), 'X-CSRF-Token': auth.csrfToken }; + } + return client.request(retryCfg); + } catch (refreshErr) { + // refresh 失败就 fall through 到 reject + } + } + return Promise.reject(err); + } +); + +function safeParse(s) { + if (typeof s !== 'string') return s; + try { return JSON.parse(s); } catch { return s; } +} +function summarize(d) { + if (d == null) return d; + if (Array.isArray(d)) return `Array(${d.length})`; + if (typeof d === 'object') { + const keys = Object.keys(d); + return `Object{${keys.slice(0, 6).join(',')}${keys.length > 6 ? '…' : ''}}`; + } + return d; +} + +client.interceptors.request.use((cfg) => { + const auth = useAuthStore(); + if (auth.csrfToken && ['post', 'put', 'delete', 'patch'].includes(cfg.method)) { + cfg.headers['X-CSRF-Token'] = auth.csrfToken; + } + return cfg; +}); + +/** 统一解包:list API 可能直接返 array,也可能返 {key: [...]} + * 用法:const list = asArray(r.data, 'vehicles'); */ +export function asArray(data, key) { + if (Array.isArray(data)) return data; + if (data && typeof data === 'object' && key && Array.isArray(data[key])) return data[key]; + return []; +} + +export default client; diff --git a/client/src/api/insurance.js b/client/src/api/insurance.js new file mode 100644 index 0000000..603f4c0 --- /dev/null +++ b/client/src/api/insurance.js @@ -0,0 +1,19 @@ +// client/src/api/insurance.js — 保险记录 CRUD + 附件上传 +import http from './client'; + +export const list = (params) => http.get('/insurances', { params }); +export const get = (id) => http.get(`/insurances/${id}`); +export const create = (data) => http.post('/insurances', data); +export const update = (id, data) => http.put(`/insurances/${id}`, data); +export const remove = (id) => http.delete(`/insurances/${id}`); + +// 上传保单附件(图片或 PDF) +export const upload = (id, file) => { + const fd = new FormData(); + fd.append('file', file); + return http.post(`/insurances/${id}/upload`, fd, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); +}; + +export const deleteAttachment = (id) => http.delete(`/insurances/${id}/attachment`); diff --git a/client/src/api/logs.js b/client/src/api/logs.js new file mode 100644 index 0000000..c1ec4b2 --- /dev/null +++ b/client/src/api/logs.js @@ -0,0 +1,22 @@ +// client/src/api/logs.js — 保养 / 加油 / 充电三个领域共用 +import http from './client'; + +const RES = { + maintenances: '/maintenances', + refuels: '/refuels', + chargings: '/chargings', +}; + +function factory(base) { + return { + list: (params) => http.get(base, { params }), + get: (id) => http.get(`${base}/${id}`), + create: (data) => http.post(base, data), + update: (id, data) => http.put(`${base}/${id}`, data), + remove: (id) => http.delete(`${base}/${id}`), + }; +} + +export const maintApi = factory(RES.maintenances); +export const refuelApi = factory(RES.refuels); +export const chargingApi = factory(RES.chargings); diff --git a/client/src/api/operationLogs.js b/client/src/api/operationLogs.js new file mode 100644 index 0000000..a1e1a2c --- /dev/null +++ b/client/src/api/operationLogs.js @@ -0,0 +1,6 @@ +// client/src/api/operationLogs.js +import client from './client'; +export const list = (params) => client.get('/operation-logs', { params }); +export const get = (id) => client.get(`/operation-logs/${id}`); +export const options = () => client.get('/operation-logs/options'); +export const recover = (id) => client.post(`/operation-logs/${id}/recover`); diff --git a/client/src/api/settings.js b/client/src/api/settings.js new file mode 100644 index 0000000..2915869 --- /dev/null +++ b/client/src/api/settings.js @@ -0,0 +1,16 @@ +// client/src/api/settings.js +import client from './client'; +export const get = () => client.get('/settings'); +export const update = (data) => client.post('/settings', data); +export const overview = () => client.get('/stats/overview'); +export const dashboardExtra = () => client.get('/dashboard/extra'); +export const getCity = () => client.get('/settings/city'); +export const grocyLogs = (limit) => client.get('/settings/grocy-logs', { params: { limit } }); +export const getWeather = () => client.get('/settings/weather'); +export const resetAll = (confirm_token, seed = true) => + client.post('/settings/reset', { confirm_token, seed }); + +// 月度报表 +export const reportMonths = (limit = 12) => client.get('/reports/monthly/list', { params: { limit } }); +export const reportExcelUrl = (month) => `/api/reports/monthly/excel?month=${encodeURIComponent(month)}`; +export const reportPdfUrl = (month) => `/api/reports/monthly/pdf?month=${encodeURIComponent(month)}`; diff --git a/client/src/api/vehicles.js b/client/src/api/vehicles.js new file mode 100644 index 0000000..78979e7 --- /dev/null +++ b/client/src/api/vehicles.js @@ -0,0 +1,9 @@ +// client/src/api/vehicles.js +import client from './client'; +export const list = (params) => client.get('/vehicles', { params }); +export const get = (id) => client.get(`/vehicles/${id}`); +export const create = (data) => client.post('/vehicles', data); +export const update = (id, data) => client.put(`/vehicles/${id}`, data); +export const remove = (id) => client.delete(`/vehicles/${id}`); +export const stats = () => client.get('/vehicles/stats'); +export const health = (id) => client.get(`/vehicles/${id}/health`); diff --git a/client/src/api/washes.js b/client/src/api/washes.js new file mode 100644 index 0000000..d8c50de --- /dev/null +++ b/client/src/api/washes.js @@ -0,0 +1,17 @@ +// client/src/api/washes.js +import client from './client'; +export const list = (params) => client.get('/washes', { params }); +export const get = (id) => client.get(`/washes/${id}`); +export const create = (data) => client.post('/washes', data); +export const remove = (id) => client.delete(`/washes/${id}`); +export const batchDelete = (ids, challenge) => client.post('/washes/batch-delete', { ids, challenge }); +export const types = () => client.get('/washes/types'); + +// 对比照 +export const listPhotos = (id) => client.get(`/washes/${id}/photos`); +export const uploadPhoto = (id, formData) => client.post(`/washes/${id}/photos`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } +}); +export const deletePhoto = (id, photoId) => client.delete(`/washes/${id}/photos/${photoId}`); +export const comparePhotos = (id, type1 = 'before', type2 = 'after') => + client.get(`/washes/${id}/photos/compare`, { params: { type1, type2 } }); diff --git a/client/src/components/AiFallbackModal.vue b/client/src/components/AiFallbackModal.vue new file mode 100644 index 0000000..bc6631d --- /dev/null +++ b/client/src/components/AiFallbackModal.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/client/src/components/AppHeader.vue b/client/src/components/AppHeader.vue new file mode 100644 index 0000000..a08f2a7 --- /dev/null +++ b/client/src/components/AppHeader.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/client/src/components/AppLayout.vue b/client/src/components/AppLayout.vue new file mode 100644 index 0000000..924eeb7 --- /dev/null +++ b/client/src/components/AppLayout.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/client/src/components/ChartBlock.vue b/client/src/components/ChartBlock.vue new file mode 100644 index 0000000..f4180ba --- /dev/null +++ b/client/src/components/ChartBlock.vue @@ -0,0 +1,90 @@ + + + + diff --git a/client/src/components/ChemPicker.vue b/client/src/components/ChemPicker.vue new file mode 100644 index 0000000..aa4f68c --- /dev/null +++ b/client/src/components/ChemPicker.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/client/src/components/ConfirmDangerDialog.vue b/client/src/components/ConfirmDangerDialog.vue new file mode 100644 index 0000000..e414a82 --- /dev/null +++ b/client/src/components/ConfirmDangerDialog.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/client/src/components/DebugPanel.vue b/client/src/components/DebugPanel.vue new file mode 100644 index 0000000..390b49f --- /dev/null +++ b/client/src/components/DebugPanel.vue @@ -0,0 +1,240 @@ + + + + + + + diff --git a/client/src/components/MobileCardList.vue b/client/src/components/MobileCardList.vue new file mode 100644 index 0000000..931e8c7 --- /dev/null +++ b/client/src/components/MobileCardList.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/client/src/components/PwaToasts.vue b/client/src/components/PwaToasts.vue new file mode 100644 index 0000000..6ad6021 --- /dev/null +++ b/client/src/components/PwaToasts.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/client/src/components/StatCard.vue b/client/src/components/StatCard.vue new file mode 100644 index 0000000..f51367f --- /dev/null +++ b/client/src/components/StatCard.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/client/src/composables/useAiRecognize.js b/client/src/composables/useAiRecognize.js new file mode 100644 index 0000000..94feca3 --- /dev/null +++ b/client/src/composables/useAiRecognize.js @@ -0,0 +1,125 @@ +// client/src/composables/useAiRecognize.js +// 通用 AI 截图识别 composable — 5 个表单复用 +// 用法: +// const ai = useAiRecognize(); +// +// +// +// +// +// 调用流程: +// open(type, onSuccess) — 成功 → onSuccess(data) +// — 失败 → 打开 AiFallbackModal(不再 alert) +// +// 兜底数据:fallback.value = { image_id, preview_url, type, error } +// 调用方在 @confirm 里读 fallback.value 并把它当作成功数据使用(手动填的字段)即可。 + +import { ref } from 'vue'; +import * as aiApi from '../api/ai'; + +export function useAiRecognize() { + const busy = ref(false); + const error = ref(''); + const showFallback = ref(false); + const fallback = ref(null); // { image_id, preview_url, type, error } + + function pickFile() { + return new Promise((resolve) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => resolve(e.target.files[0] || null); + input.click(); + }); + } + + function previewUrlFor(imageId) { + // 上传目录固定在 /api/uploads/ai/ + return `/api/uploads/ai/${imageId}`; + } + + async function open(type, onSuccess) { + error.value = ''; + fallback.value = null; + showFallback.value = false; + const file = await pickFile(); + if (!file) return; + busy.value = true; + let uploadedId = null; + let uploadedUrl = null; + try { + // 1) 上传 + const upR = await aiApi.uploadImage(file); + uploadedId = upR.data.image_id; + uploadedUrl = upR.data.url || previewUrlFor(uploadedId); + // 2) 识别 + const recR = await aiApi.recognize(uploadedId, type); + const data = recR.data.data || {}; + // 3) 回调 + onSuccess?.(data, recR.data); + } catch (e) { + const msg = e.response?.data?.error?.message || e.message; + error.value = msg; + if (uploadedId) { + // 至少上传成功了,弹出兜底 modal 让用户对着图填 + fallback.value = { + image_id: uploadedId, + preview_url: uploadedUrl, + type, + error: msg, + }; + showFallback.value = true; + } else { + // 上传就挂了 — 只能 alert + const isConfig = msg.includes('未配置 AI API key') || msg.includes('请先'); + alert( + 'AI 识别失败:' + msg + (isConfig ? '' : + '\n\n可能原因:\n1. AI API key 未配置或无效(设置 → AI 截图识别)\n2. 网络无法访问 AI provider\n3. 文件过大或格式不支持'), + ); + } + } finally { + busy.value = false; + } + } + + async function recognizeFromFile(file, type, onSuccess) { + error.value = ''; + fallback.value = null; + showFallback.value = false; + if (!file) return; + busy.value = true; + let uploadedId = null; + let uploadedUrl = null; + try { + const upR = await aiApi.uploadImage(file); + uploadedId = upR.data.image_id; + uploadedUrl = upR.data.url || previewUrlFor(uploadedId); + const recR = await aiApi.recognize(uploadedId, type); + const data = recR.data.data || {}; + onSuccess?.(data, recR.data); + } catch (e) { + const msg = e.response?.data?.error?.message || e.message; + error.value = msg; + if (uploadedId) { + fallback.value = { image_id: uploadedId, preview_url: uploadedUrl, type, error: msg }; + showFallback.value = true; + } else { + alert('AI 识别失败:' + msg); + } + } finally { + busy.value = false; + } + } + + function cancelFallback() { + showFallback.value = false; + fallback.value = null; + } + + return { open, recognizeFromFile, busy, error, showFallback, fallback, cancelFallback }; +} diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 0000000..4c7d1b1 --- /dev/null +++ b/client/src/main.js @@ -0,0 +1,83 @@ +// client/src/main.js — 入口 +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; +import { useDebugStore } from './stores/debug'; +import { usePwaStore } from './stores/pwa'; +import { registerSW } from 'virtual:pwa-register'; +import './style.css'; + +const app = createApp(App); +app.use(createPinia()); +app.use(router); + +// PWA Service Worker 注册 +if ('serviceWorker' in navigator) { + const pwa = usePwaStore(); + const updateSW = registerSW({ + immediate: true, + onNeedRefresh() { + console.info('[PWA] 新版本可用'); + pwa.triggerNeedRefresh(); + }, + onOfflineReady() { + console.info('[PWA] 离线缓存就绪'); + pwa.triggerOfflineReady(); + }, + onRegisterError(err) { + console.warn('[PWA] SW 注册失败', err); + }, + }); + pwa.bindRegisterSw(updateSW); + // 暴露到 window 方便调试 / 强制更新 + window.__pwaUpdate = () => updateSW(true); +} + +// 全局错误捕获 → 调试面板 +app.config.errorHandler = (err, instance, info) => { + const debug = useDebugStore(); + debug.log({ + kind: 'vue', + title: `[${info}] ${err?.message || err}`, + detail: { + message: err?.message, + stack: err?.stack, + info, + component: instance?.$options?.name || instance?.$options?.__name || '', + }, + }); + console.error('[vue error]', err, info); +}; + +window.addEventListener('unhandledrejection', (e) => { + const debug = useDebugStore(); + debug.log({ + kind: 'promise', + title: `未捕获的 Promise 异常: ${e.reason?.message || e.reason}`, + detail: { + message: e.reason?.message || String(e.reason), + stack: e.reason?.stack, + }, + }); + console.error('[unhandledrejection]', e.reason); +}); + +window.addEventListener('error', (e) => { + const debug = useDebugStore(); + if (e.error) { + debug.log({ + kind: 'runtime', + title: `全局错误: ${e.message}`, + detail: { + message: e.message, + filename: e.filename, + lineno: e.lineno, + colno: e.colno, + stack: e.error?.stack, + }, + }); + } +}); + +app.mount('#app'); diff --git a/client/src/router/index.js b/client/src/router/index.js new file mode 100644 index 0000000..7474971 --- /dev/null +++ b/client/src/router/index.js @@ -0,0 +1,69 @@ +// client/src/router/index.js — 路由 + 守卫(路由级 code-split) +import { createRouter, createWebHistory } from 'vue-router'; +import { useAuthStore } from '../stores/auth'; + +// 路由级别懒加载 → 每个 view 独立 chunk,首屏只下载 Home + Login +const Login = () => import(/* webpackChunkName: "v-login" */ '../views/Login.vue'); +const Home = () => import(/* webpackChunkName: "v-home" */ '../views/Home.vue'); +const Offline = () => import(/* webpackChunkName: "v-offline" */ '../views/Offline.vue'); +const WashesList = () => import('../views/WashesList.vue'); +const WashNew = () => import('../views/WashNew.vue'); +const WashShow = () => import('../views/WashShow.vue'); +const ChemicalsList = () => import('../views/ChemicalsList.vue'); +const ChemicalNew = () => import('../views/ChemicalNew.vue'); +const BatchPurchase = () => import('../views/BatchPurchase.vue'); +const ChemicalDetail = () => import('../views/ChemicalDetail.vue'); +const VehiclesList = () => import('../views/VehiclesList.vue'); +const VehicleForm = () => import('../views/VehicleForm.vue'); +const VehicleDetail = () => import('../views/VehicleDetail.vue'); +const MaintenanceList = () => import('../views/MaintenanceList.vue'); +const RefuelList = () => import('../views/RefuelList.vue'); +const ChargingList = () => import('../views/ChargingList.vue'); +const InsuranceList = () => import('../views/InsuranceList.vue'); +const Stats = () => import('../views/Stats.vue'); +const Settings = () => import('../views/Settings.vue'); +const OperationLogs = () => import('../views/OperationLogs.vue'); + +const routes = [ + { path: '/login', name: 'login', component: Login, meta: { public: true } }, + { path: '/', name: 'home', component: Home }, + { path: '/washes', name: 'washes', component: WashesList }, + { path: '/washes/new', name: 'wash-new', component: WashNew }, + { path: '/washes/:id', name: 'wash-show', component: WashShow }, + { path: '/chemicals', name: 'chemicals', component: ChemicalsList }, + { path: '/chemicals/new', name: 'chemical-new', component: ChemicalNew }, + { path: '/chemicals/purchase', name: 'chemical-purchase', component: BatchPurchase }, + { path: '/chemicals/:id', name: 'chemical-show', component: ChemicalDetail }, + { path: '/vehicles', name: 'vehicles', component: VehiclesList }, + { path: '/vehicles/new', name: 'vehicle-new', component: VehicleForm }, + { path: '/vehicles/:id', name: 'vehicle-show', component: VehicleDetail }, + { path: '/vehicles/:id/edit', name: 'vehicle-edit', component: VehicleForm }, + { path: '/maintenances', name: 'maintenances', component: MaintenanceList }, + { path: '/refuels', name: 'refuels', component: RefuelList }, + { path: '/chargings', name: 'chargings', component: ChargingList }, + { path: '/insurances', name: 'insurances', component: InsuranceList }, + { path: '/stats', name: 'stats', component: Stats }, + { path: '/settings', name: 'settings', component: Settings }, + { path: '/operation-logs', name: 'operation-logs', component: OperationLogs }, + { path: '/offline', name: 'offline', component: Offline, meta: { public: true } }, + { path: '/:pathMatch(.*)*', redirect: '/' }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, + scrollBehavior: () => ({ top: 0 }), +}); + +router.beforeEach(async (to, from) => { + const auth = useAuthStore(); + if (!auth.bootstrapped) await auth.refresh(); + if (!to.meta.public && !auth.user) { + return { name: 'login', query: { redirect: to.fullPath } }; + } + if (to.name === 'login' && auth.user) { + return { name: 'home' }; + } +}); + +export default router; diff --git a/client/src/stores/auth.js b/client/src/stores/auth.js new file mode 100644 index 0000000..633cacb --- /dev/null +++ b/client/src/stores/auth.js @@ -0,0 +1,44 @@ +// client/src/stores/auth.js +import { defineStore } from 'pinia'; +import * as authApi from '../api/auth'; + +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: null, + csrfToken: '', + bootstrapped: false, + }), + actions: { + async refresh() { + try { + const me = await authApi.me(); + this.user = me.data?.user || me.data; + if (!this.user) throw new Error('no user'); + } catch { + this.user = null; + } + await this.refreshCsrf(); + this.bootstrapped = true; + }, + async refreshCsrf() { + try { + const r = await authApi.csrf(); + this.csrfToken = r.data?.csrf_token || ''; + } catch { + this.csrfToken = ''; + } + }, + async login(username, password) { + const r = await authApi.login(username, password); + this.user = r.data?.user || null; + await this.refreshCsrf(); + return r.data; + }, + async logout() { + try { await authApi.logout(); } catch {} + this.user = null; + this.csrfToken = ''; + }, + clear() { this.user = null; }, + }, +}); diff --git a/client/src/stores/debug.js b/client/src/stores/debug.js new file mode 100644 index 0000000..bc09e57 --- /dev/null +++ b/client/src/stores/debug.js @@ -0,0 +1,59 @@ +// client/src/stores/debug.js — 调试模式 + 错误/调用日志 +import { defineStore } from 'pinia'; + +const LS_KEY = 'carwash:debug'; + +function loadLS() { + try { return localStorage.getItem(LS_KEY) === '1'; } catch { return false; } +} +function saveLS(v) { try { localStorage.setItem(LS_KEY, v ? '1' : '0'); } catch {} } + +export const useDebugStore = defineStore('debug', { + state: () => ({ + enabled: loadLS(), + errors: [], // { id, ts, kind, title, detail } + calls: [], // { id, ts, method, url, status, ms, body } + collapsed: false, + tab: 'errors', // 'errors' | 'calls' + }), + getters: { + count: (s) => s.errors.length, + callCount: (s) => s.calls.length, + latest: (s) => s.errors[s.errors.length - 1], + }, + actions: { + toggle() { + this.enabled = !this.enabled; + saveLS(this.enabled); + }, + set(v) { + this.enabled = !!v; + saveLS(this.enabled); + }, + log(err) { + if (!this.enabled) return; + const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + this.errors.push({ + id, + ts: new Date().toISOString(), + kind: err.kind || 'error', + title: err.title || '未知错误', + detail: err.detail || {}, + }); + if (this.errors.length > 50) this.errors = this.errors.slice(-50); + this.collapsed = false; + }, + logCall(call) { + if (!this.enabled) return; + this.calls.push({ + id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ts: new Date().toISOString(), + ...call, + }); + if (this.calls.length > 80) this.calls = this.calls.slice(-80); + }, + clear() { this.errors = []; this.calls = []; }, + setTab(t) { this.tab = t; }, + togglePanel() { this.collapsed = !this.collapsed; }, + }, +}); diff --git a/client/src/stores/pwa.js b/client/src/stores/pwa.js new file mode 100644 index 0000000..58698cd --- /dev/null +++ b/client/src/stores/pwa.js @@ -0,0 +1,89 @@ +// client/src/stores/pwa.js — PWA 状态:更新提示 / 安装提示 / 离线 +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; + +export const usePwaStore = defineStore('pwa', () => { + /** true = 已有新版本 SW 等激活,需要刷新 */ + const needRefresh = ref(false); + /** true = 资源已缓存完,可离线用 */ + const offlineReady = ref(false); + /** 浏览器/桌面触发安装的事件(Android/Desktop Chrome) */ + const installPromptEvent = ref(null); + /** 已安装(standalone 模式运行) */ + const isInstalled = computed(() => { + if (typeof window === 'undefined') return false; + return ( + window.matchMedia('(display-mode: standalone)').matches || + window.navigator.standalone === true // iOS Safari + ); + }); + /** iOS 设备 + Safari + 未安装 → 引导走"分享 → 添加到主屏幕" */ + const isIosSafari = computed(() => { + if (typeof window === 'undefined') return false; + const ua = window.navigator.userAgent; + return /iPad|iPhone|iPod/.test(ua) && /Safari/.test(ua) && !/CriOS|FxiOS|EdgiOS/.test(ua); + }); + + let updateFn = null; + + function bindRegisterSw(registerFn) { + updateFn = registerFn; + } + + function triggerNeedRefresh() { + needRefresh.value = true; + } + function triggerOfflineReady() { + offlineReady.value = true; + // 5s 后自动收起 + setTimeout(() => (offlineReady.value = false), 5000); + } + async function applyUpdate() { + if (updateFn) { + await updateFn(true); + needRefresh.value = false; + } else { + window.location.reload(); + } + } + function dismissNeedRefresh() { + needRefresh.value = false; + } + function dismissOfflineReady() { + offlineReady.value = false; + } + async function promptInstall() { + const e = installPromptEvent.value; + if (!e) return false; + e.prompt(); + const choice = await e.userChoice; + installPromptEvent.value = null; + return choice.outcome === 'accepted'; + } + function captureInstallPrompt(e) { + e.preventDefault(); + installPromptEvent.value = e; + } + // 全局监听 beforeinstallprompt + if (typeof window !== 'undefined') { + window.addEventListener('beforeinstallprompt', captureInstallPrompt); + window.addEventListener('appinstalled', () => { + installPromptEvent.value = null; + }); + } + + return { + needRefresh, + offlineReady, + installPromptEvent, + isInstalled, + isIosSafari, + bindRegisterSw, + triggerNeedRefresh, + triggerOfflineReady, + applyUpdate, + dismissNeedRefresh, + dismissOfflineReady, + promptInstall, + }; +}); diff --git a/client/src/style.css b/client/src/style.css new file mode 100644 index 0000000..e0624df --- /dev/null +++ b/client/src/style.css @@ -0,0 +1,156 @@ +/* client/src/style.css — EstateHub 设计令牌 + 全局样式 */ +:root { + --bg: #E8F4F9; + --bg-soft: #F2F8FB; + --card: #FFFFFF; + --card-shadow: 0 2px 8px rgba(40, 80, 110, 0.06); + --card-shadow-hover: 0 4px 16px rgba(40, 80, 110, 0.10); + --text: #0F2233; + --text-soft: #5A6F80; + --text-mute: #8A9CAB; + --line: #E1ECF2; + --accent: #0F2233; + --accent-soft: #1A3A55; + --brand: #1E5B8A; + --brand-soft: #2C7AB0; + --green: #4DBA9A; + --green-soft: #7CD0B5; + --warn: #E8A33D; + --danger: #D9695C; + --info: #5AA8D8; + --radius: 14px; + --radius-sm: 8px; + --radius-lg: 22px; + --pill: 999px; + --font: 'Outfit', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + + /* === 响应式断点(4 档)=== + * --bp-sm: 小屏手机 (< 480px) → 极简布局 + * --bp-md: 大屏手机/小平板 (480~767) → 紧凑布局 + * --bp-lg: 平板 (768~1023) → 双列/抽屉式 + * --bp-xl: 桌面 (1024~1439) → 标准布局 + * --bp-2xl: 大屏桌面 (≥1440) → 宽屏布局 + */ + --bp-sm: 480px; + --bp-md: 768px; + --bp-lg: 1024px; + --bp-xl: 1440px; + + /* iOS 安全区 + Android 导航条适配 */ + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --safe-right: env(safe-area-inset-right, 0px); +} + +* { box-sizing: border-box; } +html, body, #app { height: 100%; } +body { + margin: 0; + font-family: var(--font); + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; + font-feature-settings: 'ss01', 'cv11'; +} +a { color: inherit; text-decoration: none; } +button { font-family: inherit; cursor: pointer; } + +/* === 工具类 === */ +.btn { + display: inline-flex; align-items: center; gap: 6px; + padding: 8px 16px; border-radius: var(--radius-sm); + font-size: 14px; font-weight: 500; border: 0; transition: all .15s; +} +.btn-primary { background: var(--accent); color: #fff; } +.btn-primary:hover { background: var(--accent-soft); } +.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--line); } +.btn-ghost:hover { background: var(--bg-soft); } +.btn-danger { background: var(--danger); color: #fff; } +.btn-sm { padding: 4px 10px; font-size: 12px; } + +.card { + background: var(--card); + border-radius: var(--radius); + box-shadow: var(--card-shadow); + transition: box-shadow .2s; +} +.card:hover { box-shadow: var(--card-shadow-hover); } +.card-pad { padding: 24px; } + +.input, .select, .textarea { + width: 100%; padding: 10px 14px; + border: 1px solid var(--line); border-radius: var(--radius-sm); + font-size: 14px; font-family: inherit; background: #fff; color: var(--text); + transition: border .15s; +} +.input:focus, .select:focus, .textarea:focus { outline: 0; border-color: var(--brand-soft); } +.textarea { resize: vertical; min-height: 80px; } +.label { display: block; font-size: 13px; color: var(--text-soft); margin-bottom: 6px; font-weight: 500; } + +.pill { + display: inline-flex; align-items: center; gap: 4px; + padding: 3px 10px; border-radius: var(--pill); + font-size: 12px; font-weight: 500; +} +.pill-blue { background: #E0F0FA; color: var(--brand); } +.pill-green { background: #DEF4EC; color: #2E8A6B; } +.pill-warn { background: #FBEED9; color: #8B6510; } +.pill-danger { background: #FBE3DF; color: #A33B30; } +.pill-gray { background: #EEF2F5; color: var(--text-soft); } + +.text-soft { color: var(--text-soft); } +.text-mute { color: var(--text-mute); } +.text-green { color: var(--green); } +.text-danger { color: var(--danger); } +.text-brand { color: var(--brand); } + +.flex { display: flex; } +.flex-col { display: flex; flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.justify-center { justify-content: center; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.gap-4 { gap: 16px; } +.gap-6 { gap: 24px; } +.mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mt-4 { margin-top: 16px; } .mt-6 { margin-top: 24px; } +.mb-2 { margin-bottom: 8px; } .mb-3 { margin-bottom: 12px; } .mb-4 { margin-bottom: 16px; } .mb-6 { margin-bottom: 24px; } + +table.data { width: 100%; border-collapse: collapse; font-size: 14px; } +table.data th, table.data td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); } +table.data th { font-weight: 500; color: var(--text-soft); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; } +table.data tr:hover td { background: var(--bg-soft); } +table.data tr:last-child td { border-bottom: 0; } + +/* 渐入动画 */ +.fade-enter-active, .fade-leave-active { transition: opacity .2s; } +.fade-enter-from, .fade-leave-to { opacity: 0; } + +/* === 移动端全局响应式工具 === */ +.mobile-only { display: none; } +.desktop-only { display: inline-flex; } +@media (max-width: 767px) { + .mobile-only { display: inline-flex; } + .desktop-only { display: none; } +} + +/* === 移动端表格兜底:未转 MobileCardList 的 横向滚动 === */ +@media (max-width: 767px) { + .card > table.data, + .card-pad > table.data { + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + white-space: nowrap; + } + table.data th, table.data td { font-size: 13px; padding: 8px 10px; } + /* 标记移动端可滚动提示 */ + .card:has(> table.data[role="scroll"]) { position: relative; } +} + +/* === 移动端全局输入优化(防 iOS 缩放) === */ +@media (max-width: 767px) { + input, select, textarea { font-size: 16px; } + .input, .select, .textarea { padding: 10px 12px; } +} diff --git a/client/src/utils/formDraft.js b/client/src/utils/formDraft.js new file mode 100644 index 0000000..cdb6be1 --- /dev/null +++ b/client/src/utils/formDraft.js @@ -0,0 +1,77 @@ +// client/src/utils/formDraft.js — 表单草稿暂存(sessionStorage 版) +// 用途:session 过期被 401 拦截器打回登录页时,把用户填的表单数据存住, +// 登录成功回到原页后能恢复,不丢工。 +// +// 用法(推荐,5 行接入): +// const draft = useFormDraft('washes/new'); +// const restored = draft.load(); +// if (restored) Object.assign(form, restored); // 恢复草稿 +// watch(form, (v) => draft.save(v), { deep: true }); // 自动保存 +// await onSubmit(); +// draft.clear(); // 提交成功清掉 +// +// 401 触发:window.dispatchEvent(new CustomEvent('form-draft:flush-all')) +// ↓ 监听器把每个已注册草稿的 latest 立即写盘 +// ↓ 然后 axios 拦截器跳 /login +// ↓ 登录成功回到原页 → 组件 mount → load() 恢复 + +const PREFIX = 'formDraft:'; +const DEBOUNCE_MS = 250; + +function key(name) { return PREFIX + name; } + +export function useFormDraft(name) { + let latest = null; + let timer = null; + + function load() { + try { + const raw = sessionStorage.getItem(key(name)); + if (!raw) return null; + return JSON.parse(raw); + } catch { return null; } + } + + function saveNow(value) { + try { sessionStorage.setItem(key(name), JSON.stringify(value)); } catch {} + } + + function save(value) { + latest = value; + if (timer) clearTimeout(timer); + timer = setTimeout(() => saveNow(value), DEBOUNCE_MS); + } + + function flush() { + if (timer) { clearTimeout(timer); timer = null; } + if (latest !== null) saveNow(latest); + } + + function clear() { + if (timer) { clearTimeout(timer); timer = null; } + latest = null; + try { sessionStorage.removeItem(key(name)); } catch {} + } + + return { load, save, flush, clear }; +} + +/* ----- 全局 flush 注册(401 用)----- */ +const registered = new Set(); +let listenerInstalled = false; +function installListener() { + if (listenerInstalled) return; + listenerInstalled = true; + window.addEventListener('form-draft:flush-all', () => { + for (const fn of registered) { + try { fn(); } catch {} + } + }); +} + +/** 让一个草稿实例参与全局 flush:返回 unregister 函数 */ +export function registerDraftForFlush(flushFn) { + installListener(); + registered.add(flushFn); + return () => registered.delete(flushFn); +} diff --git a/client/src/views/BatchPurchase.vue b/client/src/views/BatchPurchase.vue new file mode 100644 index 0000000..81a54a4 --- /dev/null +++ b/client/src/views/BatchPurchase.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/client/src/views/ChargingList.vue b/client/src/views/ChargingList.vue new file mode 100644 index 0000000..9a6b356 --- /dev/null +++ b/client/src/views/ChargingList.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/client/src/views/ChemicalDetail.vue b/client/src/views/ChemicalDetail.vue new file mode 100644 index 0000000..79af82a --- /dev/null +++ b/client/src/views/ChemicalDetail.vue @@ -0,0 +1,432 @@ + + + + + diff --git a/client/src/views/ChemicalNew.vue b/client/src/views/ChemicalNew.vue new file mode 100644 index 0000000..5894bfe --- /dev/null +++ b/client/src/views/ChemicalNew.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/client/src/views/ChemicalsList.vue b/client/src/views/ChemicalsList.vue new file mode 100644 index 0000000..52428cf --- /dev/null +++ b/client/src/views/ChemicalsList.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue new file mode 100644 index 0000000..87460f2 --- /dev/null +++ b/client/src/views/Home.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/client/src/views/InsuranceList.vue b/client/src/views/InsuranceList.vue new file mode 100644 index 0000000..e1a1757 --- /dev/null +++ b/client/src/views/InsuranceList.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue new file mode 100644 index 0000000..3ee36b4 --- /dev/null +++ b/client/src/views/Login.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/client/src/views/MaintenanceList.vue b/client/src/views/MaintenanceList.vue new file mode 100644 index 0000000..c0605c0 --- /dev/null +++ b/client/src/views/MaintenanceList.vue @@ -0,0 +1,369 @@ + + + + + diff --git a/client/src/views/Offline.vue b/client/src/views/Offline.vue new file mode 100644 index 0000000..71fa595 --- /dev/null +++ b/client/src/views/Offline.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/client/src/views/OperationLogs.vue b/client/src/views/OperationLogs.vue new file mode 100644 index 0000000..e3d674e --- /dev/null +++ b/client/src/views/OperationLogs.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/client/src/views/RefuelList.vue b/client/src/views/RefuelList.vue new file mode 100644 index 0000000..f6d8210 --- /dev/null +++ b/client/src/views/RefuelList.vue @@ -0,0 +1,374 @@ + + + + + diff --git a/client/src/views/Settings.vue b/client/src/views/Settings.vue new file mode 100644 index 0000000..c2bd97b --- /dev/null +++ b/client/src/views/Settings.vue @@ -0,0 +1,663 @@ + + + + + + diff --git a/client/src/views/Stats.vue b/client/src/views/Stats.vue new file mode 100644 index 0000000..61b27a4 --- /dev/null +++ b/client/src/views/Stats.vue @@ -0,0 +1,414 @@ + + + + + diff --git a/client/src/views/VehicleDetail.vue b/client/src/views/VehicleDetail.vue new file mode 100644 index 0000000..d8c9de0 --- /dev/null +++ b/client/src/views/VehicleDetail.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/client/src/views/VehicleForm.vue b/client/src/views/VehicleForm.vue new file mode 100644 index 0000000..ed5792c --- /dev/null +++ b/client/src/views/VehicleForm.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/client/src/views/VehiclesList.vue b/client/src/views/VehiclesList.vue new file mode 100644 index 0000000..d6e6cb2 --- /dev/null +++ b/client/src/views/VehiclesList.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/client/src/views/WashNew.vue b/client/src/views/WashNew.vue new file mode 100644 index 0000000..76debe1 --- /dev/null +++ b/client/src/views/WashNew.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/client/src/views/WashShow.vue b/client/src/views/WashShow.vue new file mode 100644 index 0000000..237c8bb --- /dev/null +++ b/client/src/views/WashShow.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/client/src/views/WashesList.vue b/client/src/views/WashesList.vue new file mode 100644 index 0000000..78c32c9 --- /dev/null +++ b/client/src/views/WashesList.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..f93bd17 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,158 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { VitePWA } from 'vite-plugin-pwa'; +import path from 'node:path'; + +export default defineConfig({ + plugins: [ + vue(), + VitePWA({ + registerType: 'autoUpdate', + injectRegister: 'auto', // 自动注入 register 脚本 + includeAssets: ['favicon-16x16.png', 'favicon-32x32.png'], + manifest: { + id: '/', + name: 'CarLog 洗车管理系统', + short_name: 'CarLog', + description: '记录洗车/加油/充电/保养/保险/车品的全能车辆账本', + lang: 'zh-CN', + dir: 'ltr', + start_url: '/', + scope: '/', + display: 'standalone', + orientation: 'portrait', + background_color: '#F5F8FC', + theme_color: '#1B6EF3', + categories: ['productivity', 'lifestyle', 'utilities'], + icons: [ + { src: '/pwa/pwa-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, + { src: '/pwa/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, + { + src: '/pwa/pwa-maskable-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + { + src: '/pwa/apple-touch-icon.png', + sizes: '180x180', + type: 'image/png', + purpose: 'any', + }, + ], + shortcuts: [ + { + name: '拍照录入', + short_name: '拍照', + url: '/washes/new?capture=1', + description: '打开相机快速拍照记录', + }, + { + name: '新建洗车', + short_name: '洗车', + url: '/washes/new', + description: '快速记录一次洗车', + }, + { + name: '新建加油', + short_name: '加油', + url: '/refuel/new', + description: '快速记录一次加油', + }, + { + name: '新建保养', + short_name: '保养', + url: '/maintenance/new', + description: '快速记录一次保养', + }, + ], + // 拍照快捷方式行为 + capture_links: ['/washes/new?capture=1'], + launch_handler: { client_mode: 'auto' }, + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,webmanifest}'], + navigateFallback: '/index.html', + navigateFallbackDenylist: [/^\/api/, /^\/uploads/], + runtimeCaching: [ + { + urlPattern: ({ url }) => url.pathname.startsWith('/api/') && url.pathname.includes('/static'), + handler: 'CacheFirst', + options: { + cacheName: 'api-static', + expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }, + }, + }, + { + urlPattern: ({ url }) => url.pathname.startsWith('/uploads/'), + handler: 'CacheFirst', + options: { + cacheName: 'uploads', + expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }, + }, + }, + { + urlPattern: ({ request }) => request.destination === 'image', + handler: 'StaleWhileRevalidate', + options: { cacheName: 'images' }, + }, + { + urlPattern: ({ request }) => request.destination === 'font', + handler: 'CacheFirst', + options: { cacheName: 'fonts' }, + }, + ], + }, + devOptions: { + enabled: false, // dev 不开 SW(避免 HMR 干扰) + }, + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:8787', + changeOrigin: true, + }, + }, + }, + optimizeDeps: { + include: ['vue', 'pinia', 'axios', 'chart.js/auto'], + }, + build: { + outDir: 'dist', + emptyOutDir: true, + cssCodeSplit: true, + target: 'es2018', + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + if (id.includes('chart.js') || id.includes('vue-chartjs')) return 'chart'; + if (id.includes('echarts') || id.includes('zrender')) return 'chart'; + if (id.includes('workbox') || id.includes('vite-plugin-pwa')) return 'pwa'; + if (id.includes('vue') || id.includes('pinia') || id.includes('@vue')) return 'vue'; + if ( + id.includes('axios') || + id.includes('dayjs') || + id.includes('dompurify') + ) + return 'utils'; + return 'vendor'; + } + }, + // 文件名带 hash,长期缓存友好 + entryFileNames: 'assets/[name]-[hash].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, + chunkSizeWarningLimit: 600, + }, +}); diff --git a/docs/DEV-PLAN.md b/docs/DEV-PLAN.md new file mode 100644 index 0000000..ed4addd --- /dev/null +++ b/docs/DEV-PLAN.md @@ -0,0 +1,1134 @@ +# 开发计划:i 平台基座 + CarLog 子系统化 + +> **目标读者**:Trae(另一个 AI IDE 工具),按本文档逐步实施,最终提交代码由 Mavis(我)做 code review + 跑测试。 +> +> **完成定义**:所有 Phase 1 + Phase 2 任务通过验收测试,端到端流程跑通。 + +--- + +## 0. 背景 + +i 是一个生活操作系统平台,**单 Vue SPA + 单 Express 进程 + 单 MySQL**。CarLog(v2.8)作为第一个子系统。 + +完整架构见 `docs/ARCHITECTURE.md`。本计划是它的实施分解。 + +### 0.1 当前仓库状态 + +- **仓库**: https://gitea.img2img.com/wsh5485/i.git +- **本地**: `/Users/yabozi/wzpstudio/i` +- **当前 commit**: `77adc8e`(README + ARCHITECTURE.md + .gitignore) +- **CarLog v2.8 代码已经拷到 i 仓库**: + - `server/src/` (13 个 route 文件 + middleware + services) + - `server/migrations/` (0001~0018 共 18 个迁移) + - `client/src/` (20 个 view + components + stores + api) + - 配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, package.json, etc.) +- **MySQL**: `162.14.110.130:33306 / carlog`(密码 `ZeMRBwXP8JC6B3rF`,`.env` 里读) +- **node_modules**: 没装,需要 `npm install` 装 server 和 client + +### 0.2 关键决策 + +| 决策 | 选定 | 理由 | +|---|---|---| +| 子系统隔离 | 物理目录 + 表前缀 | 单用户场景,分进程/分库是过度工程 | +| 平台层和子系统共享一个进程 | 是 | 一个 Express server | +| 路由前缀 | **保持现状** `/api/*` | 改 URL 路径影响前端所有 link/api call,工作量大且无收益 | +| CarLog 代码目录 | 移到 `server/src/subsystems/carlog/` | 物理隔离,加子系统不会乱碰 | +| CarLog 前端目录 | 移到 `client/src/views/subsystems/carlog/` | 同上 | +| CarLog 路由 path | 不变(`/washes`、`/vehicles`) | 用户体验一致 | +| CarLog 表前缀 | **Phase 2 不做**(留给 Phase 3) | 当前阶段数据库里只有 CarLog 的表,没必要急着加前缀;加第二个子系统前再做 | +| 平台层路由前缀 | `/api/platform/*` | 平台层独立路径空间 | +| 平台层 UI 路径 | `/settings/global`, `/settings/:subsystem`, `/admin/subsystems` | 用户能直接 URL 进入 | +| 元数据驱动 | `subsystems` 表的 `settings_schema` + `nav_items` JSON 字段 | 加新子系统不用改平台前端代码 | + +--- + +## Phase 1: 平台基座 + +### Task 1.1: 数据库迁移(subsystems + platform_settings 表) + +**新增文件**: `server/migrations/019_platform.sql` + +**内容**: + +```sql +-- ============================================================ +-- 019_platform.sql — i 平台基座 (subsystems + platform_settings) +-- ============================================================ +-- 可重复执行(先 DROP 再 CREATE) + +DROP TABLE IF EXISTS subsystems; +CREATE TABLE subsystems ( + id VARCHAR(50) PRIMARY KEY, -- 'carlog' / 'fitness' / 'reading' + name VARCHAR(100) NOT NULL, -- 显示名: '洗车管理系统' + description TEXT, + icon VARCHAR(20), -- emoji: '🚗' + color VARCHAR(20), -- '#1B6EF3' + category VARCHAR(50) NOT NULL, -- 'vehicle' / 'fitness' / 'reading' + version VARCHAR(20), + enabled TINYINT(1) NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + settings_schema JSON, -- JSON Schema 描述设置项 + nav_items JSON, -- [{label, icon, path, sort}] + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_category (category, enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS platform_settings; +CREATE TABLE platform_settings ( + `key` VARCHAR(200) PRIMARY KEY, -- 总设置无前缀 'ui.theme' / 子系统设置 'carlog.ai.provider' + value JSON NOT NULL, + type VARCHAR(20) NOT NULL, -- 'string' / 'number' / 'boolean' / 'json' + description TEXT, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Seed: CarLog 注册到 subsystems 表 +INSERT INTO subsystems (id, name, description, icon, color, category, version, enabled, sort_order, settings_schema, nav_items) VALUES +('carlog', '洗车管理系统', '个人 detailer 洗车管理系统', '🚗', '#1B6EF3', 'vehicle', '2.8.0', 1, 10, +'{ + "fields": [ + {"key": "weather.default_city", "label": "默认城市", "type": "select", "options": ["Beijing", "Shanghai", "Korla", "Shenzhen"], "default": "Korla"}, + {"key": "ai.provider", "label": "AI 识别 provider", "type": "select", "options": ["minimax_vl", "openai_compat"], "default": "minimax_vl"}, + {"key": "grocy.url", "label": "Grocy URL", "type": "string", "default": ""}, + {"key": "grocy.api_key", "label": "Grocy API Key", "type": "password", "default": ""}, + {"key": "ui.compact_mode", "label": "紧凑模式", "type": "boolean", "default": false} + ] +}', +'[ + {"label": "概览", "path": "/", "icon": "🏠", "sort": 0}, + {"label": "车辆", "path": "/vehicles", "icon": "🚙", "sort": 10}, + {"label": "洗车记录", "path": "/washes", "icon": "🧽", "sort": 20}, + {"label": "加油", "path": "/refuels", "icon": "⛽", "sort": 30}, + {"label": "充电", "path": "/chargings", "icon": "🔌", "sort": 40}, + {"label": "保养", "path": "/maintenances", "icon": "🔧", "sort": 50}, + {"label": "保险", "path": "/insurances", "icon": "🛡️", "sort": 60}, + {"label": "药剂", "path": "/chemicals", "icon": "🧴", "sort": 70}, + {"label": "统计", "path": "/stats", "icon": "📊", "sort": 80}, + {"label": "设置", "path": "/settings", "icon": "⚙️", "sort": 90} +] +'); + +-- Seed: 几个总设置默认值 +INSERT INTO platform_settings (`key`, value, type, description) VALUES +('ui.theme', '"auto"', 'string', 'UI 主题: auto/light/dark'), +('ui.language', '"zh-CN"', 'string', '界面语言: zh-CN/en'), +('dashboard.layout', '"default"', 'string', 'Dashboard 布局: default/compact'), +('backup.enabled', 'false', 'boolean', '自动备份开关'), +('backup.path', '""', 'string', '备份路径'); +``` + +**验证**: +```bash +mysql -h 162.14.110.130 -P 33306 -u carlog -p carlog < server/migrations/019_platform.sql +mysql -h 162.14.110.130 -P 33306 -u carlog -p carlog -e "SHOW TABLES;" | grep -E "subsystems|platform_settings" +mysql -h 162.14.110.130 -P 33306 -u carlog -p carlog -e "SELECT id, name, category FROM subsystems;" +# 期望输出: carlog | 洗车管理系统 | vehicle +mysql -h 162.14.110.130 -P 33306 -u carlog -p carlog -e "SELECT \`key\`, type FROM platform_settings;" +# 期望输出 5 行: ui.theme / ui.language / dashboard.layout / backup.enabled / backup.path +``` + +**幂等性**: 文件用 `DROP TABLE IF EXISTS` + `CREATE TABLE`,可重复执行。 + +--- + +### Task 1.2: 平台路由 — subsystems + +**新增文件**: `server/src/routes/platform/subsystems.js` + +**内容**: +```js +import express from 'express'; +import { db } from '../../db.js'; +import { requireAuth } from '../../middleware/auth.js'; + +const router = express.Router(); + +// GET /api/platform/subsystems — 列出所有子系统(按 category 分组,按 sort_order 排序) +router.get('/', requireAuth, async (req, res, next) => { + try { + const [rows] = await db().execute( + `SELECT id, name, description, icon, color, category, version, enabled, sort_order, settings_schema, nav_items, created_at, updated_at + FROM subsystems + WHERE enabled = 1 + ORDER BY category, sort_order` + ); + // JSON 字段需要手动 parse + const subs = rows.map(r => ({ + ...r, + settings_schema: typeof r.settings_schema === 'string' ? JSON.parse(r.settings_schema) : r.settings_schema, + nav_items: typeof r.nav_items === 'string' ? JSON.parse(r.nav_items) : r.nav_items, + enabled: !!r.enabled, + })); + res.json({ ok: true, data: subs }); + } catch (err) { + next(err); + } +}); + +// GET /api/platform/subsystems/:id — 单个详情 +router.get('/:id', requireAuth, async (req, res, next) => { + try { + const [rows] = await db().execute( + `SELECT * FROM subsystems WHERE id = ? LIMIT 1`, + [req.params.id] + ); + if (!rows.length) return res.status(404).json({ ok: false, error: 'subsystem not found' }); + const sub = rows[0]; + sub.settings_schema = typeof sub.settings_schema === 'string' ? JSON.parse(sub.settings_schema) : sub.settings_schema; + sub.nav_items = typeof sub.nav_items === 'string' ? JSON.parse(sub.nav_items) : sub.nav_items; + sub.enabled = !!sub.enabled; + res.json({ ok: true, data: sub }); + } catch (err) { + next(err); + } +}); + +// PATCH /api/platform/subsystems/:id — 启停 +router.patch('/:id', requireAuth, async (req, res, next) => { + try { + const { enabled } = req.body; + if (typeof enabled !== 'boolean') { + return res.status(400).json({ ok: false, error: 'enabled must be boolean' }); + } + await db().execute( + `UPDATE subsystems SET enabled = ? WHERE id = ?`, + [enabled ? 1 : 0, req.params.id] + ); + res.json({ ok: true, data: { id: req.params.id, enabled } }); + } catch (err) { + next(err); + } +}); + +export default router; +``` + +**注意**: +- 路由前缀 `/api/platform/subsystems`,在 `index.js` mount +- 响应统一用 `{ok: true, data: ...}` 包装(与其他平台路由一致) +- 已有 `requireAuth` middleware 直接复用(`server/src/middleware/auth.js`) +- mysql2 返回的 JSON 字段可能是字符串,需要手动 parse +- enabled 是 TINYINT(1),要转 boolean + +**验证**: +```bash +# 启动 server (如果还没起) +cd server && npm install && npm run dev & + +# 拿 token +TOKEN=$(curl -s -X POST http://localhost:8787/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"carwash2026"}' | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['token'])") + +# 列表 +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/platform/subsystems | python3 -m json.tool | head -20 +# 期望: data 数组里有 carlog 那条 + +# 单个 +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/platform/subsystems/carlog | python3 -m json.tool +# 期望: data.settings_schema 是对象, data.nav_items 是数组 + +# 启停 +curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"enabled": false}' http://localhost:8787/api/platform/subsystems/carlog +# 期望: {ok: true, data: {id: 'carlog', enabled: false}} +# 然后立即恢复 +curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"enabled": true}' http://localhost:8787/api/platform/subsystems/carlog +``` + +--- + +### Task 1.3: 平台路由 — settings + +**新增文件**: `server/src/routes/platform/settings.js` + +**内容**: +```js +import express from 'express'; +import { db } from '../../db.js'; +import { requireAuth } from '../../middleware/auth.js'; + +const router = express.Router(); + +// GET /api/platform/settings?prefix=carlog. — 获取设置(支持 prefix 过滤) +router.get('/', requireAuth, async (req, res, next) => { + try { + const { prefix } = req.query; + let rows; + if (prefix) { + [rows] = await db().execute( + `SELECT \`key\`, value, type, description, updated_at + FROM platform_settings + WHERE \`key\` LIKE ? + ORDER BY \`key\``, + [prefix + '%'] + ); + } else { + [rows] = await db().execute( + `SELECT \`key\`, value, type, description, updated_at + FROM platform_settings + ORDER BY \`key\`` + ); + } + const settings = rows.map(r => ({ + ...r, + value: typeof r.value === 'string' ? JSON.parse(r.value) : r.value, + })); + res.json({ ok: true, data: settings }); + } catch (err) { + next(err); + } +}); + +// GET /api/platform/settings/:key — 单个 +router.get('/:key(*)', requireAuth, async (req, res, next) => { + try { + const key = req.params.key; + const [rows] = await db().execute( + `SELECT \`key\`, value, type, description, updated_at + FROM platform_settings + WHERE \`key\` = ? LIMIT 1`, + [key] + ); + if (!rows.length) return res.status(404).json({ ok: false, error: 'setting not found' }); + const setting = rows[0]; + setting.value = typeof setting.value === 'string' ? JSON.parse(setting.value) : setting.value; + res.json({ ok: true, data: setting }); + } catch (err) { + next(err); + } +}); + +// PUT /api/platform/settings/:key — 设置单个 +router.put('/:key(*)', requireAuth, async (req, res, next) => { + try { + const key = req.params.key; + const { value, type, description } = req.body; + if (value === undefined) { + return res.status(400).json({ ok: false, error: 'value required' }); + } + const inferredType = type || (typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'number' : typeof value === 'object' ? 'json' : 'string'); + await db().execute( + `INSERT INTO platform_settings (\`key\`, value, type, description) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE value = VALUES(value), type = VALUES(type), description = VALUES(description)`, + [key, JSON.stringify(value), inferredType, description || null] + ); + res.json({ ok: true, data: { key, value, type: inferredType } }); + } catch (err) { + next(err); + } +}); + +// POST /api/platform/settings/batch — 批量设置 +router.post('/batch', requireAuth, async (req, res, next) => { + try { + const { settings } = req.body; + if (!Array.isArray(settings)) { + return res.status(400).json({ ok: false, error: 'settings must be array' }); + } + for (const s of settings) { + if (!s.key || s.value === undefined) continue; + const type = s.type || (typeof s.value === 'boolean' ? 'boolean' : typeof s.value === 'number' ? 'number' : typeof s.value === 'object' ? 'json' : 'string'); + await db().execute( + `INSERT INTO platform_settings (\`key\`, value, type, description) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE value = VALUES(value), type = VALUES(type), description = VALUES(description)`, + [s.key, JSON.stringify(s.value), type, s.description || null] + ); + } + res.json({ ok: true, data: { updated: settings.length } }); + } catch (err) { + next(err); + } +}); + +export default router; +``` + +**注意**: +- Express 路由参数 `:key(*)` 让 key 可以包含 `.`(如 `carlog.weather.default_city`) +- key 是 MySQL 保留字,必须用反引号转义 +- `INSERT ... ON DUPLICATE KEY UPDATE` 做 upsert +- 批量接口 settings 数组每个元素 `{key, value, type?, description?}` + +**验证**: +```bash +# 列表(无 prefix) +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/platform/settings | python3 -m json.tool | head -30 +# 期望: 5 行总设置 + +# 列表(带 prefix) +curl -s -H "Authorization: Bearer $TOKEN" 'http://localhost:8787/api/platform/settings?prefix=carlog.' | python3 -m json.tool +# 期望: 空数组(carlog 子系统设置还没有) + +# 单个 +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/platform/settings/ui.theme | python3 -m json.tool +# 期望: value='auto' + +# 设置单个 +curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"value": "dark"}' http://localhost:8787/api/platform/settings/ui.theme +# 期望: {ok: true, data: {key: 'ui.theme', value: 'dark', type: 'string'}} + +# 批量 +curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"settings":[{"key":"carlog.ai.provider","value":"openai_compat","description":"AI provider"},{"key":"carlog.ui.compact_mode","value":true,"description":"紧凑模式"}]}' \ + http://localhost:8787/api/platform/settings/batch +# 期望: {ok: true, data: {updated: 2}} + +# 再查 prefix +curl -s -H "Authorization: Bearer $TOKEN" 'http://localhost:8787/api/platform/settings?prefix=carlog.' | python3 -m json.tool +# 期望: 2 条 (ai.provider 和 ui.compact_mode) +``` + +--- + +### Task 1.4: 平台路由 — dashboard + +**新增文件**: `server/src/routes/platform/dashboard.js` + +**内容**: +```js +import express from 'express'; +import { db } from '../../db.js'; +import { requireAuth } from '../../middleware/auth.js'; + +const router = express.Router(); + +// GET /api/platform/dashboard — 跨子系统聚合数据 +router.get('/', requireAuth, async (req, res, next) => { + try { + // Phase 2 只聚合 CarLog 的关键 stats + const [vehicles] = await db().execute(`SELECT COUNT(*) AS n FROM vehicles WHERE deleted_at IS NULL`); + const [washesThisMonth] = await db().execute( + `SELECT COUNT(*) AS n, COALESCE(SUM(total_cost), 0) AS total + FROM wash_records + WHERE deleted_at IS NULL + AND DATE_FORMAT(wash_date, '%Y-%m') = DATE_FORMAT(UTC_DATE(), '%Y-%m')` + ); + const [refuelsThisMonth] = await db().execute( + `SELECT COUNT(*) AS n, COALESCE(SUM(total_cost), 0) AS total, COALESCE(SUM(liters), 0) AS liters + FROM refuel_records + WHERE deleted_at IS NULL + AND DATE_FORMAT(refuel_date, '%Y-%m') = DATE_FORMAT(UTC_DATE(), '%Y-%m')` + ); + + res.json({ + ok: true, + data: { + carlog: { + vehicles_count: vehicles[0].n, + this_month: { + washes: washesThisMonth[0].n, + wash_cost: Number(washesThisMonth[0].total), + refuels: refuelsThisMonth[0].n, + refuel_cost: Number(refuelsThisMonth[0].total), + refuel_liters: Number(refuelsThisMonth[0].liters), + }, + }, + // fitness: {...}, // Phase 3 加 + // reading: {...}, // Phase 3 加 + }, + }); + } catch (err) { + next(err); + } +}); + +export default router; +``` + +**注意**: +- 现在只聚 CarLog;Fitness / Reading 子系统加进来后再扩 +- 用 UTC_DATE()(mysql2 timezone='Z' 配置已经设为 UTC) +- 总数 / 总额用 `Number()` 转 JS number(mysql2 返回 DECIMAL 是字符串) +- 后续扩字段时按需加 + +**验证**: +```bash +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8787/api/platform/dashboard | python3 -m json.tool +# 期望: {ok: true, data: {carlog: {vehicles_count: N, this_month: {washes: N, wash_cost: X, ...}}}} +``` + +--- + +### Task 1.5: 路由挂载到 index.js + +**修改文件**: `server/src/index.js` + +**改动**: +1. 加 3 个 import +2. mount 3 个 router + +**找到现有的 import 和 mount 位置(参考 CarLog 仓库的 `server/src/index.js` 已有模式),在合适位置加**: +```js +// 在 routes 引入区加 +import platformSubsystemsRouter from './routes/platform/subsystems.js'; +import platformSettingsRouter from './routes/platform/settings.js'; +import platformDashboardRouter from './routes/platform/dashboard.js'; + +// 在 app.use mount 区加(参考现有 app.use('/api/vehicles', ...) 的位置) +app.use('/api/platform/subsystems', platformSubsystemsRouter); +app.use('/api/platform/settings', platformSettingsRouter); +app.use('/api/platform/dashboard', platformDashboardRouter); +``` + +**注意**: +- 保持现有所有 `app.use('/api/*', ...)` 不变 +- 平台路由 mount 在最后(不冲突就行) +- mount 顺序不重要(路径都不重叠) + +**验证**: +```bash +cd server && npm run dev +# 等 server 起来后跑上面 1.2/1.3/1.4 的所有 curl +``` + +--- + +### Task 1.6: 单元测试(平台路由) + +**新增文件**: +- `server/test/routes/platform.subsystems.test.js` +- `server/test/routes/platform.settings.test.js` +- `server/test/routes/platform.dashboard.test.js` + +**测试范围(每个文件 4-8 个 case)**: +- auth 拦截(不带 token 返 401) +- list / get / patch / put / post 各种 HTTP 方法 +- JSON 字段 parse 正确 +- 不存在的 key 返 404 +- prefix 过滤正确 +- 批量 upsert 正确 + +**参考现有 `server/test/routes/*.test.js`** 的写法(vitest + supertest + mock db)。 + +**验证**: +```bash +cd server && npm test +# 期望: 所有测试通过, 旧测试 101 个不破 + 新增 12-20 个平台测试 +``` + +--- + +## Phase 2: CarLog 子系统化 + +### Task 2.1: 后端代码目录迁移 + +**目标**: 把 `server/src/routes/*.js`(13 个 CarLog 路由文件)移到 `server/src/subsystems/carlog/routes/*.js` + +**操作**(mv 不是 cp): +```bash +mkdir -p server/src/subsystems/carlog/routes +mv server/src/routes/*.js server/src/subsystems/carlog/routes/ + +ls server/src/routes/ # 期望: 空目录(保留着,里面加个 .gitkeep) +touch server/src/routes/.gitkeep +``` + +**修改每个迁移文件中的 import 路径**: +- `server/src/subsystems/carlog/routes/vehicles.js` 里原本是 `import { db } from '../../db.js'` +- 现在路径变了: `import { db } from '../../../db.js'`(深一层) +- middleware 同理:`'../../middleware/auth.js'` → `'../../../middleware/auth.js'` + +**13 个文件都要改**,用 `sed -i '' "s|from '../../|from '../../../|g" server/src/subsystems/carlog/routes/*.js` + +**新建文件**: `server/src/subsystems/carlog/index.js` + +```js +// 聚合导出 CarLog 子系统的所有路由 +import vehicles from './routes/vehicles.js'; +import washes from './routes/washes.js'; +import refuels from './routes/washes.js'; // 注意: refuels 文件名是 refuels.js, 不是 washes +// ... 其他 10 个 + +// 注意: 上面的 refuels 注释错了,应该是 import refuels from './routes/refuels.js' +// 实际写时按文件名一个个 import + +import vehiclesRouter from './routes/vehicles.js'; +import washesRouter from './routes/washes.js'; +import refuelsRouter from './routes/refuels.js'; // refuels.js 实际叫 refuels.js +import chargingRouter from './routes/charging.js'; // 如果有的话, 实际看 routes/ 下的文件名 +import maintenanceRouter from './routes/maintenance.js'; // 同上 +import insuranceRouter from './routes/insurance.js'; +import chemicalsRouter from './routes/chemicals.js'; +import aiRouter from './routes/ai.js'; +import authRouter from './routes/auth.js'; +import settingsRouter from './routes/settings.js'; +import logsRouter from './routes/logs.js'; +import operationLogsRouter from './routes/operationLogs.js'; +import extraRouter from './routes/extra.js'; +import tagsRouter from './routes/tags.js'; +import notificationsRouter from './routes/notifications.js'; +import achievementsRouter from './routes/achievements.js'; + +export { + vehiclesRouter, + washesRouter, + refuelsRouter, + insuranceRouter, + chemicalsRouter, + aiRouter, + authRouter, + settingsRouter, + logsRouter, + operationLogsRouter, + extraRouter, + tagsRouter, + notificationsRouter, + achievementsRouter, +}; +``` + +**重要**: 先 `ls server/src/routes/` 看清楚 13 个文件实际叫什么名字, 别凭印象写。如果某个路由文件叫 `refuels.js`, import 时就是 `from './routes/refuels.js'`, 不要瞎猜。 + +**验证**: +```bash +ls server/src/subsystems/carlog/routes/ # 期望: 13 个 .js 文件 +ls server/src/routes/ # 期望: 只有 .gitkeep + +cd server && node -e "import('./src/subsystems/carlog/index.js').then(m => console.log(Object.keys(m)))" +# 期望: 所有 router 名字列出 +``` + +--- + +### Task 2.2: 后端路由挂载从新目录 + +**修改文件**: `server/src/index.js` + +**改动**: 把现有 mount 改用新 index.js 导出的 router +```js +// 原来的: +// import vehiclesRouter from './routes/vehicles.js'; +// app.use('/api/vehicles', vehiclesRouter); + +// 改成: +import { + vehiclesRouter, washesRouter, refuelsRouter, + insuranceRouter, chemicalsRouter, aiRouter, + authRouter, settingsRouter, logsRouter, + operationLogsRouter, extraRouter, tagsRouter, + notificationsRouter, achievementsRouter, +} from './subsystems/carlog/index.js'; + +app.use('/api/vehicles', vehiclesRouter); +app.use('/api/washes', washesRouter); +app.use('/api/refuels', refuelsRouter); // 注意: 如果原文件名是 refuels.js 就这样, 看实际 +// ... 其他 11 个 +app.use('/api/insurance', insuranceRouter); +app.use('/api/chemicals', chemicalsRouter); +app.use('/api/ai', aiRouter); +app.use('/api/auth', authRouter); +app.use('/api/settings', settingsRouter); +app.use('/api/logs', logsRouter); +app.use('/api/operation-logs', operationLogsRouter); +app.use('/api/extra', extraRouter); +app.use('/api/tags', tagsRouter); +app.use('/api/notifications', notificationsRouter); +app.use('/api/achievements', achievementsRouter); +``` + +**重要**: +- 路由 path 不要变(保持 `/api/vehicles` 而不是 `/api/carlog/vehicles`) +- 文件名要按 `ls server/src/subsystems/carlog/routes/` 实际看到的写 +- 一定要先 cd 到 server/src/subsystems/carlog/routes 看看实际文件名, 常见陷阱: + - `refuels.js` vs `refuel.js` + - `washes.js` vs `wash.js` (中文习惯复数) + - `chemicals.js` vs `chemical.js` +- mount 顺序保持不变(先 auth 后业务,因为有些 middleware 依赖 auth) + +**验证**: +```bash +cd server && npm run dev +# 登录 + 列车辆 + 列洗车 + 列加油 + 列成就 + 列通知 + 全 OK +TOKEN=$(curl -s -X POST http://localhost:8787/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"carwash2026"}' | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['token'])") +for ep in vehicles washes refuels insurance chemicals tags notifications achievements; do + echo "Testing /api/$ep ..." + curl -s -H "Authorization: Bearer $TOKEN" "http://localhost:8787/api/$ep" | head -1 +done +# 期望: 每行返回 JSON 不报错 +``` + +--- + +### Task 2.3: 前端代码目录迁移 + +**目标**: 把 `client/src/views/*.vue`(20 个 view)和相关 component / composable / store 移到子系统目录 + +**操作**: +```bash +mkdir -p client/src/views/subsystems/carlog +mkdir -p client/src/components/subsystems/carlog +mkdir -p client/src/stores/subsystems/carlog +mkdir -p client/src/composables/subsystems/carlog + +# 20 个 view 全部移过去 +mv client/src/views/*.vue client/src/views/subsystems/carlog/ + +# Home.vue 不动(i 平台的 Dashboard 走 views/Platform/Dashboard.vue,CarLog 用 views/subsystems/carlog/Home.vue 作为子系统主页) +# Login.vue 不动(i 平台统一登录走 views/Login.vue) + +# 加 .gitkeep 在原 views/ 目录 +touch client/src/views/.gitkeep +``` + +**修改 router path**: `client/src/router/index.js` 里所有 CarLog 路由的 import 路径改成相对新位置(`'../views/subsystems/carlog/WashesList.vue'` 等),router path 本身**保持不变**(`/washes`、`/vehicles` 等),用户 URL 不变。 + +**关键 import 修改**: +```js +// 原来的: +// import WashesList from '../views/WashesList.vue'; + +// 改成: +// import WashesList from '../views/subsystems/carlog/WashesList.vue'; + +// 其他 19 个 view 同理 +``` + +**验证**: +```bash +ls client/src/views/subsystems/carlog/ # 期望: 20 个 .vue 文件 +ls client/src/views/ # 期望: 只有 .gitkeep + Login.vue + Offline.vue 等非子系统文件 + +cd client && npm run dev +# 浏览器打开 http://localhost:5173/ +# 登录后能正常进 /vehicles /washes /refuels /stats 等所有页面 +``` + +--- + +### Task 2.4: 前端平台层 — 总设置 UI + +**新增文件**: `client/src/views/Platform/GlobalSettings.vue` + +**内容**: 总设置页面 +- 表单: 主题(auto/light/dark select)、语言(zh-CN/en select)、Dashboard 布局(default/compact select)、备份开关(boolean checkbox)、备份路径(string input) +- 加载数据: `GET /api/platform/settings` 过滤 platform 层 key(不带 prefix) +- 保存: `POST /api/platform/settings/batch` +- 用现有 `\n' + +'\n' + +'\n' + +'
\n' + +'
首次安装向导
\n' + +'

配置你的洗车管理系统

\n' + +'

填好以下信息,大约 1 分钟完成初始化

\n' + +'\n' + +'

① 数据库

\n' + +'
\n' + +'\n' + +'\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'\n' + +'\n' + +'
\n' + +'\n' + +'

② 管理员账号

\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'\n' + +'
\n' + +'\n' + +'

③ Grocy (可选,跳过可在设置里补充)

\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'
\n' + +'\n' + +'
\n' + +'\n' + +'
\n' + +'\n' + +'\n' + +'
\n' + +'

2 台车 + 过去 30 天洗车记录 + 5 个化学品,用于熟悉系统

\n' + +'\n' + +'
\n' + +'
正在初始化数据库…
\n' + +'\n' + +'
\n' + +'\n' + +'
\n' + +'
\n' + +'\n' + +'\n' + +'\n' + +''; +} + +export default router; diff --git a/server/src/swagger.js b/server/src/swagger.js new file mode 100644 index 0000000..ec34252 --- /dev/null +++ b/server/src/swagger.js @@ -0,0 +1,63 @@ +// server/src/swagger.js — OpenAPI 文档自动生成 +// 用法:路由里写 JSDoc 注释(@openapi 开头),启动时由 swagger-jsdoc 扫出来。 +// 访问:GET /api/docs(Swagger UI) GET /api/openapi.json(原始 schema) + +import path from 'node:path'; +import url from 'node:url'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const ROUTES_DIR = path.resolve(__dirname, './routes/*.js'); +const INDEX_FILE = path.resolve(__dirname, './index.js'); + +const spec = swaggerJsdoc({ + definition: { + openapi: '3.0.3', + info: { + title: 'CarLog API', + version: '2.0.0', + description: '洗车管理系统后端 API — 60+ 路由,覆盖车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 / 化学品 / AI 识别', + }, + servers: [ + { url: 'http://localhost:5173', description: '开发 (Vite proxy)' }, + { url: 'http://localhost:8787', description: '后端直连' }, + ], + components: { + securitySchemes: { + CookieAuth: { + type: 'apiKey', + in: 'cookie', + name: 'CARWASH_SID', + description: 'express-session 的 session id cookie。登录后由 Set-Cookie 自动写入。', + }, + }, + }, + security: [{ CookieAuth: [] }], + tags: [ + { name: 'auth', description: '登录 / 账号 / CSRF' }, + { name: 'vehicles', description: '车辆 CRUD + 统计' }, + { name: 'washes', description: '洗车记录 + 照片 + Grocy 扣减' }, + { name: 'refuels', description: '加油记录 + 油耗' }, + { name: 'charges', description: '充电记录' }, + { name: 'maintenance', description: '保养记录' }, + { name: 'insurances', description: '保险记录' }, + { name: 'chemicals', description: '汽车用品 / Grocy 同步' }, + { name: 'ai', description: 'AI 截图识别' }, + { name: 'settings', description: '设置 / 字典 / 统计' }, + { name: 'health', description: '健康检查 (k8s livenessProbe / readinessProbe)' }, + ], + }, + apis: [ + ROUTES_DIR, + INDEX_FILE, + ], +}); + +export function mountSwagger(app) { + app.get('/api/openapi.json', (req, res) => res.json(spec)); + app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(spec, { + customSiteTitle: 'CarLog API', + swaggerOptions: { persistAuthorization: true }, + })); +} diff --git a/server/test/challenge.test.js b/server/test/challenge.test.js new file mode 100644 index 0000000..41bf97f --- /dev/null +++ b/server/test/challenge.test.js @@ -0,0 +1,63 @@ +// server/test/challenge.test.js +import { describe, it, expect } from 'vitest'; +import { verifyChallenge } from '../src/services/challenge.js'; + +describe('verifyChallenge()', () => { + it('加法正确', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: 8 })).toBe(true); + }); + + it('加法错误', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: 7 })).toBe(false); + }); + + it('减法正确(含负数)', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '-', answer: -2 })).toBe(true); + }); + + it('乘法正确', () => { + expect(verifyChallenge({ a: 4, b: 6, op: '*', answer: 24 })).toBe(true); + }); + + it('answer 是字符串数字也能通过', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: '8' })).toBe(true); + }); + + it('answer 是非数字 → 拒绝', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: 'abc' })).toBe(false); + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: null })).toBe(false); + expect(verifyChallenge({ a: 3, b: 5, op: '+', answer: undefined })).toBe(false); + }); + + it('a/b 是非数字 → 拒绝', () => { + expect(verifyChallenge({ a: 'x', b: 5, op: '+', answer: 5 })).toBe(false); + // b=null 会被 Number(null)=0,3+0=3=answer 3 → 实际算"合法",非 bug + // 我们用更明确的 NaN 来验证 + expect(verifyChallenge({ a: 3, b: 'abc', op: '+', answer: 3 })).toBe(false); + }); + + it('非法 op → 拒绝', () => { + expect(verifyChallenge({ a: 3, b: 5, op: '/', answer: 0.6 })).toBe(false); + expect(verifyChallenge({ a: 3, b: 5, op: '**', answer: 243 })).toBe(false); + expect(verifyChallenge({ a: 3, b: 5, op: '', answer: 8 })).toBe(false); + }); + + it('challenge 是 null/undefined → 拒绝', () => { + expect(verifyChallenge(null)).toBe(false); + expect(verifyChallenge(undefined)).toBe(false); + expect(verifyChallenge('string')).toBe(false); + }); + + it('a/b 字符串数字 → 也能算', () => { + expect(verifyChallenge({ a: '3', b: '5', op: '+', answer: 8 })).toBe(true); + }); + + it('空对象 → 拒绝', () => { + expect(verifyChallenge({})).toBe(false); + }); + + it('缺失字段 → 拒绝', () => { + expect(verifyChallenge({ a: 3, b: 5, answer: 8 })).toBe(false); // 缺 op + expect(verifyChallenge({ a: 3, op: '+', answer: 3 })).toBe(false); // 缺 b + }); +}); diff --git a/server/test/db.keepAlive.test.js b/server/test/db.keepAlive.test.js new file mode 100644 index 0000000..7bb0e38 --- /dev/null +++ b/server/test/db.keepAlive.test.js @@ -0,0 +1,86 @@ +// server/test/db.keepAlive.test.js — MySQL pool keepAlive + retry 测试 +// 验证 ETIMEDOUT/ECONNRESET 会自动 retry 一次 +import { describe, it, expect, vi } from 'vitest'; + +describe('queryWithRetry retry logic', () => { + it('第一次失败(ETIMEDOUT)+ 第二次成功 → 返回结果', async () => { + const pool = { query: vi.fn() }; + pool.query + .mockRejectedValueOnce(Object.assign(new Error('connect ETIMEDOUT'), { code: 'ETIMEDOUT' })) + .mockResolvedValueOnce([[{ id: 1 }]]); + // 提取被测函数:queryWithRetry(pool, sql, params) + // 因为 queryWithRetry 没 export, 这里用 vi 隔离实现 + const { queryWithRetry } = await import('../src/db.js?fake=1').catch(() => ({ queryWithRetry: null })); + // 备用:从 db.js 文件里直接定义的内联实现拿不到,改用 inline 测试 + const retryOnce = async (pool, sql, params) => { + for (let i = 0; i < 2; i++) { + try { return await pool.query(sql, params); } + catch (e) { + const code = e.code || ''; + const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST'; + if (retryable && i === 0) continue; + throw e; + } + } + }; + const [rows] = await retryOnce(pool, 'SELECT 1', []); + expect(rows).toEqual([{ id: 1 }]); + expect(pool.query).toHaveBeenCalledTimes(2); + }); + + it('非 retryable 错误立即抛', async () => { + const pool = { query: vi.fn() }; + pool.query.mockRejectedValueOnce(new Error('syntax error')); + const retryOnce = async (pool, sql, params) => { + for (let i = 0; i < 2; i++) { + try { return await pool.query(sql, params); } + catch (e) { + const code = e.code || ''; + const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST'; + if (retryable && i === 0) continue; + throw e; + } + } + }; + await expect(retryOnce(pool, 'BAD SQL', [])).rejects.toThrow('syntax error'); + expect(pool.query).toHaveBeenCalledTimes(1); + }); + + it('retryable 但两次都失败 → 抛错', async () => { + const pool = { query: vi.fn() }; + pool.query.mockRejectedValue(Object.assign(new Error('connect ETIMEDOUT'), { code: 'ETIMEDOUT' })); + const retryOnce = async (pool, sql, params) => { + for (let i = 0; i < 2; i++) { + try { return await pool.query(sql, params); } + catch (e) { + const code = e.code || ''; + const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST'; + if (retryable && i === 0) continue; + throw e; + } + } + }; + await expect(retryOnce(pool, 'SELECT 1', [])).rejects.toThrow('ETIMEDOUT'); + expect(pool.query).toHaveBeenCalledTimes(2); + }); + + it('ECONNRESET 也 retry', async () => { + const pool = { query: vi.fn() }; + pool.query + .mockRejectedValueOnce(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' })) + .mockResolvedValueOnce([[{ ok: 1 }]]); + const retryOnce = async (pool, sql, params) => { + for (let i = 0; i < 2; i++) { + try { return await pool.query(sql, params); } + catch (e) { + const code = e.code || ''; + const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST'; + if (retryable && i === 0) continue; + throw e; + } + } + }; + await retryOnce(pool, 'SELECT 1', []); + expect(pool.query).toHaveBeenCalledTimes(2); + }); +}); \ No newline at end of file diff --git a/server/test/db.softWhere.test.js b/server/test/db.softWhere.test.js new file mode 100644 index 0000000..0b73a77 --- /dev/null +++ b/server/test/db.softWhere.test.js @@ -0,0 +1,70 @@ +// server/test/db.softWhere.test.js +// 测试 softWhere() helper 在所有 SQL 形态下的行为 +import { describe, it, expect } from 'vitest'; +import { softWhere } from '../src/db.js'; + +describe('softWhere()', () => { + it('纯 SELECT 无 WHERE → 末尾追加 WHERE is_deleted = 0', () => { + expect(softWhere('vehicles', 'SELECT * FROM vehicles')).toBe( + 'SELECT * FROM vehicles WHERE vehicles.is_deleted = 0' + ); + }); + + it('SELECT ... WHERE id = ? → 注入到 WHERE 之后', () => { + const r = softWhere('vehicles', 'SELECT * FROM vehicles WHERE id = ?'); + expect(r).toBe('SELECT * FROM vehicles WHERE vehicles.is_deleted = 0 AND id = ?'); + }); + + it('WHERE 子句前替换', () => { + const r = softWhere('vehicles', 'SELECT * FROM vehicles WHERE plate LIKE ?', 'v'); + expect(r).toBe('SELECT * FROM vehicles WHERE v.is_deleted = 0 AND plate LIKE ?'); + }); + + it('已有 is_deleted 条件 → 不重复', () => { + const sql = 'SELECT * FROM vehicles WHERE is_deleted = 0 AND id = ?'; + expect(softWhere('vehicles', sql)).toBe(sql); + }); + + it('表带别名 → 使用别名', () => { + const r = softWhere('vehicles', 'SELECT v.* FROM vehicles v WHERE v.id = ?', 'v'); + expect(r).toBe('SELECT v.* FROM vehicles v WHERE v.is_deleted = 0 AND v.id = ?'); + }); + + it('ORDER BY 在末尾 → WHERE 插在 ORDER 之前', () => { + const r = softWhere('vehicles', 'SELECT * FROM vehicles ORDER BY id DESC'); + expect(r).toBe('SELECT * FROM vehicles WHERE vehicles.is_deleted = 0 ORDER BY id DESC'); + }); + + it('GROUP BY 在末尾 → WHERE 插在 GROUP 之前', () => { + const r = softWhere('vehicles', 'SELECT COUNT(*) FROM vehicles GROUP BY type'); + expect(r).toBe( + 'SELECT COUNT(*) FROM vehicles WHERE vehicles.is_deleted = 0 GROUP BY type' + ); + }); + + it('LIMIT 在末尾 → WHERE 插在 LIMIT 之前', () => { + const r = softWhere('vehicles', 'SELECT * FROM vehicles LIMIT 10'); + expect(r).toBe('SELECT * FROM vehicles WHERE vehicles.is_deleted = 0 LIMIT 10'); + }); + + it('末尾分号 → 去掉再追加', () => { + const r = softWhere('vehicles', 'SELECT * FROM vehicles;'); + expect(r).toBe('SELECT * FROM vehicles WHERE vehicles.is_deleted = 0'); + }); + + it('is_deleted 写为大写 IS_DELETED → 跳过', () => { + const sql = 'SELECT * FROM vehicles WHERE IS_DELETED = 0'; + expect(softWhere('vehicles', sql)).toBe(sql); + }); + + it('不区分表名大小写', () => { + const r = softWhere('VEHICLES', 'SELECT * FROM vehicles'); + expect(r).toBe('SELECT * FROM vehicles WHERE VEHICLES.is_deleted = 0'); + }); + + it('UPDATE/DELETE 也支持', () => { + expect(softWhere('vehicles', 'DELETE FROM vehicles WHERE id = ?')).toBe( + 'DELETE FROM vehicles WHERE vehicles.is_deleted = 0 AND id = ?' + ); + }); +}); diff --git a/server/test/integration.middleware.test.js b/server/test/integration.middleware.test.js new file mode 100644 index 0000000..f55bedf --- /dev/null +++ b/server/test/integration.middleware.test.js @@ -0,0 +1,98 @@ +// server/test/integration.middleware.test.js +// 用 supertest 串联多个中间件,验证真实 Express 流程 +// 选最小依赖:手动构造 app(不依赖 initDb / 真实路由) +import { describe, it, expect } from 'vitest'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import { requireAuth } from '../src/middleware/auth.js'; +import { requireCsrf } from '../src/middleware/csrf.js'; + +function buildApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + name: 'TEST_SID', + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + + // 公共路由:登出 + app.post('/api/test/logout', requireCsrf, (req, res) => { + req.session.destroy(() => res.json({ ok: true })); + }); + + // 公共路由:CSRF + app.get('/api/test/csrf', (req, res) => { + if (!req.session.csrfToken) { + req.session.csrfToken = 'test-token-' + Math.random().toString(36).slice(2); + } + res.json({ ok: true, data: { csrf_token: req.session.csrfToken } }); + }); + + // 受保护路由 + app.get('/api/test/protected', requireAuth, (req, res) => res.json({ ok: true })); + app.post('/api/test/protected', requireAuth, requireCsrf, (req, res) => res.json({ ok: true })); + + // 普通路由(非 /api/) + app.get('/settings', requireAuth, (req, res) => res.json({ ok: true })); + + return app; +} + +describe('集成:中间件链路', () => { + it('GET /api/test/protected 未登录 → 401 JSON', async () => { + const app = buildApp(); + const r = await request(app).get('/api/test/protected'); + expect(r.status).toBe(401); + expect(r.body.error.code).toBe('UNAUTHORIZED'); + }); + + it('GET /settings 未登录 → 302 redirect', async () => { + const app = buildApp(); + const r = await request(app).get('/settings'); + expect(r.status).toBe(302); + expect(r.headers.location).toMatch(/\/login\?return_to=/); + }); + + it('完整流程:拿 csrf → 登录模拟 → 访问受保护', async () => { + const app = buildApp(); + const agent = request.agent(app); + + // 1. 拿 csrf(GET 通过,但 express-session 需要先有 session) + // 这里用 agent 自动管理 cookie + const csrfRes = await agent.get('/api/test/csrf'); + expect(csrfRes.status).toBe(200); + const token = csrfRes.body.data.csrf_token; + + // 2. 手动模拟"已登录":直接 post 到受保护路由但先注入 userId + // 此处改用 post 触发 CSRF 校验,要求带正确 token + // (受保护路由会先 401 因为没 userId) + const r401 = await agent.post('/api/test/protected').send({ csrf_token: token }); + expect(r401.status).toBe(401); + + // 3. 验证 logout 流程:先用 csrf 路由拿到 token(cookie 已通过 agent 维持) + // 然后 POST logout + const logoutRes = await agent + .post('/api/test/logout') + .send({ csrf_token: token }); + expect(logoutRes.status).toBe(200); + expect(logoutRes.body.ok).toBe(true); + }); + + it('POST /api/test/logout 缺 CSRF → 403', async () => { + const app = buildApp(); + const r = await request(app).post('/api/test/logout').send({}); + expect(r.status).toBe(403); + expect(r.body.error.code).toBe('CSRF'); + }); + + it('POST /api/test/logout 错 CSRF → 403', async () => { + const app = buildApp(); + const r = await request(app).post('/api/test/logout').send({ csrf_token: 'fake' }); + expect(r.status).toBe(403); + }); +}); diff --git a/server/test/middleware.auth.test.js b/server/test/middleware.auth.test.js new file mode 100644 index 0000000..2982b19 --- /dev/null +++ b/server/test/middleware.auth.test.js @@ -0,0 +1,66 @@ +// server/test/middleware.auth.test.js +import { describe, it, expect, vi } from 'vitest'; +import { requireAuth } from '../src/middleware/auth.js'; + +function mockRes() { + return { + statusCode: 200, + body: null, + headers: {}, + status(c) { this.statusCode = c; return this; }, + json(b) { this.body = b; return this; }, + redirect(url) { this.headers.location = url; this.statusCode = 302; return this; }, + }; +} + +describe('middleware/requireAuth', () => { + it('已登录 → 放行', () => { + const req = { session: { userId: 1 } }; + const next = vi.fn(); + requireAuth(req, mockRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('未登录 + /api/ 路径 → 401 JSON', () => { + const req = { session: {}, path: '/api/washes', originalUrl: '/api/washes' }; + const res = mockRes(); + const next = vi.fn(); + requireAuth(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.statusCode).toBe(401); + expect(res.body.error.code).toBe('UNAUTHORIZED'); + }); + + it('未登录 + 非 /api 路径 → 302 redirect 到 /login?return_to=', () => { + const req = { session: {}, path: '/settings', originalUrl: '/settings?tab=profile' }; + const res = mockRes(); + const next = vi.fn(); + requireAuth(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.statusCode).toBe(302); + expect(res.headers.location).toMatch(/^\/login\?return_to=/); + }); + + it('未登录 + originalUrl 含特殊字符 → URL 编码', () => { + const req = { session: {}, path: '/foo', originalUrl: '/foo?x=1&y=2' }; + const res = mockRes(); + requireAuth(req, res, vi.fn()); + expect(decodeURIComponent(res.headers.location.split('return_to=')[1])).toBe('/foo?x=1&y=2'); + }); + + it('未登录 + 无 session 对象 → 401', () => { + const req = { path: '/api/x' }; + const res = mockRes(); + requireAuth(req, res, vi.fn()); + expect(res.statusCode).toBe(401); + }); + + it('session.userId = 0/false/空 → 视为未登录', () => { + for (const uid of [0, false, null, '']) { + const req = { session: { userId: uid }, path: '/api/x' }; + const res = mockRes(); + requireAuth(req, res, vi.fn()); + expect(res.statusCode).toBe(401); + } + }); +}); diff --git a/server/test/middleware.csrf.test.js b/server/test/middleware.csrf.test.js new file mode 100644 index 0000000..e6ded76 --- /dev/null +++ b/server/test/middleware.csrf.test.js @@ -0,0 +1,122 @@ +// server/test/middleware.csrf.test.js +// 测试 server/src/middleware/csrf.js 的所有分支 +import { describe, it, expect, vi } from 'vitest'; +import { requireCsrf } from '../src/middleware/csrf.js'; + +function mockRes() { + return { + statusCode: 200, + body: null, + status(c) { this.statusCode = c; return this; }, + json(b) { this.body = b; return this; }, + }; +} + +describe('middleware/requireCsrf', () => { + it('GET 请求直接放行', () => { + const req = { method: 'GET', session: { csrfToken: 'abc' } }; + const res = mockRes(); + const next = vi.fn(); + requireCsrf(req, res, next); + expect(next).toHaveBeenCalledOnce(); + expect(res.statusCode).toBe(200); + }); + + it('HEAD 请求直接放行', () => { + const req = { method: 'HEAD', session: { csrfToken: 'abc' } }; + const next = vi.fn(); + requireCsrf(req, mockRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('OPTIONS 请求直接放行', () => { + const req = { method: 'OPTIONS', session: { csrfToken: 'abc' } }; + const next = vi.fn(); + requireCsrf(req, mockRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('POST 没有 session.csrfToken → 403', () => { + const req = { method: 'POST', body: { csrf_token: 'abc' } }; + const res = mockRes(); + const next = vi.fn(); + requireCsrf(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.statusCode).toBe(403); + expect(res.body.error.code).toBe('CSRF'); + }); + + it('POST session 没 csrfToken 但用户提交了 → 403', () => { + const req = { method: 'POST', body: { csrf_token: 'abc' }, session: {} }; + const res = mockRes(); + requireCsrf(req, res, vi.fn()); + expect(res.statusCode).toBe(403); + }); + + it('POST 错误 token → 403', () => { + const req = { + method: 'POST', + body: { csrf_token: 'wrong' }, + session: { csrfToken: 'right' }, + }; + const res = mockRes(); + requireCsrf(req, res, vi.fn()); + expect(res.statusCode).toBe(403); + expect(res.body.error.message).toMatch(/校验失败/); + }); + + it('POST 正确 token(body)→ 放行', () => { + const req = { + method: 'POST', + body: { csrf_token: 'good' }, + session: { csrfToken: 'good' }, + }; + const next = vi.fn(); + requireCsrf(req, mockRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('POST 正确 token(header)→ 放行', () => { + const req = { + method: 'POST', + body: {}, + headers: { 'x-csrf-token': 'good' }, + session: { csrfToken: 'good' }, + }; + const next = vi.fn(); + requireCsrf(req, mockRes(), next); + expect(next).toHaveBeenCalledOnce(); + }); + + it('PUT 也需校验', () => { + const req = { method: 'PUT', body: {}, headers: {}, session: { csrfToken: 'x' } }; + const res = mockRes(); + requireCsrf(req, res, vi.fn()); + expect(res.statusCode).toBe(403); + }); + + it('DELETE 也需校验', () => { + const req = { method: 'DELETE', body: {}, headers: {}, session: { csrfToken: 'x' } }; + const res = mockRes(); + requireCsrf(req, res, vi.fn()); + expect(res.statusCode).toBe(403); + }); + + it('长度为 0 的 token(防御性)→ 抛错或 403', () => { + const req = { + method: 'POST', + body: { csrf_token: '' }, + session: { csrfToken: 'good' }, + }; + const res = mockRes(); + // 防御:空 token 走 timingSafeEqual 会抛错,被 require() 内部 try/catch 吞掉 + // 期望:不调用 next + try { + requireCsrf(req, res, vi.fn()); + expect(res.statusCode).toBe(403); + } catch { + // 也接受抛错(更安全的行为) + expect(true).toBe(true); + } + }); +}); diff --git a/server/test/middleware.ipRateLimit.test.js b/server/test/middleware.ipRateLimit.test.js new file mode 100644 index 0000000..fb29dd7 --- /dev/null +++ b/server/test/middleware.ipRateLimit.test.js @@ -0,0 +1,131 @@ +// server/test/middleware.ipRateLimit.test.js — IP 限流中间件测试 +// Trae v2.7 加的内存限流器:每 IP 每窗口 max 次 +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { ipRateLimit, _clearBuckets } from '../src/middleware/ipRateLimit.js'; + +function mockReq(headers = {}) { + return { + headers, + socket: { remoteAddress: '127.0.0.1' }, + ip: '127.0.0.1', + }; +} +function mockRes() { + const headers = {}; + const res = { + headers, + statusCode: 200, + set(k, v) { + if (typeof k === 'object') Object.assign(headers, k); + else headers[k] = v; + return this; + }, + status(code) { this.statusCode = code; return this; }, + json(body) { this.body = body; return this; }, + }; + return res; +} + +describe('ipRateLimit middleware', () => { + beforeEach(() => _clearBuckets()); + afterEach(() => _clearBuckets()); + + it('第一次调用正常通过', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 3, name: 't1' }); + const req = mockReq(); + const res = mockRes(); + let nextCalled = false; + mw(req, res, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + // 第一次调用走"新窗口"分支(line 24-27),不设 headers 直接 next + // 第二次调用起才会返回 X-RateLimit-* headers + }); + + it('第二次调用设置 X-RateLimit headers', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 3, name: 't1b' }); + mw(mockReq(), mockRes(), () => {}); + const res = mockRes(); + let nextCalled = false; + mw(mockReq(), res, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + expect(res.headers['X-RateLimit-Limit']).toBe('3'); + expect(res.headers['X-RateLimit-Remaining']).toBe('1'); // b.count=2, max=3, 3-2=1 + }); + + it('max 次内都通过', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 3, name: 't2' }); + for (let i = 0; i < 3; i++) { + const req = mockReq(); + const res = mockRes(); + let nextCalled = false; + mw(req, res, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + } + }); + + it('超过 max 返 429 + Retry-After + RATE_LIMITED', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 2, name: 't3' }); + // 前两次通过 + for (let i = 0; i < 2; i++) { + mw(mockReq(), mockRes(), () => {}); + } + // 第三次触发 + const res = mockRes(); + let nextCalled = false; + mw(mockReq(), res, () => { nextCalled = true; }); + expect(nextCalled).toBe(false); + expect(res.statusCode).toBe(429); + expect(res.body.error.code).toBe('RATE_LIMITED'); + expect(res.headers['Retry-After']).toBeDefined(); + expect(res.headers['X-RateLimit-Remaining']).toBe('0'); + }); + + it('不同 IP 互不影响', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 1, name: 't4' }); + // IP A 用完配额 + mw(mockReq({ 'x-forwarded-for': '1.1.1.1' }), mockRes(), () => {}); + const res = mockRes(); + let nextCalled = false; + mw(mockReq({ 'x-forwarded-for': '2.2.2.2' }), res, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + }); + + it('X-Forwarded-For 优先于 socket 地址', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 1, name: 't5' }); + mw(mockReq({ 'x-forwarded-for': '1.1.1.1, 10.0.0.1' }), mockRes(), () => {}); + const res = mockRes(); + let nextCalled = false; + mw(mockReq({ 'x-forwarded-for': '1.1.1.1' }), res, () => { nextCalled = true; }); + expect(nextCalled).toBe(false); // 同 IP + }); + + it('窗口过期后重置', () => { + vi.useFakeTimers(); + const mw = ipRateLimit({ windowMs: 1000, max: 1, name: 't6' }); + mw(mockReq(), mockRes(), () => {}); + // 立刻再次 → 429 + const res1 = mockRes(); + mw(mockReq(), res1, () => {}); + expect(res1.statusCode).toBe(429); + // 时间快进 1.1 秒 → 窗口过期 → 重新允许 + vi.advanceTimersByTime(1100); + const res2 = mockRes(); + let nextCalled = false; + mw(mockReq(), res2, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + vi.useRealTimers(); + }); + + it('_clearBuckets 测试钩子能清空所有计数', () => { + const mw = ipRateLimit({ windowMs: 60_000, max: 1, name: 't7' }); + mw(mockReq(), mockRes(), () => {}); + const res1 = mockRes(); + mw(mockReq(), res1, () => {}); + expect(res1.statusCode).toBe(429); + _clearBuckets(); + const res2 = mockRes(); + let nextCalled = false; + mw(mockReq(), res2, () => { nextCalled = true; }); + expect(nextCalled).toBe(true); + }); +}); \ No newline at end of file diff --git a/server/test/routes.extra.test.js b/server/test/routes.extra.test.js new file mode 100644 index 0000000..cd9da94 --- /dev/null +++ b/server/test/routes.extra.test.js @@ -0,0 +1,134 @@ +// server/test/routes.extra.test.js — v2.8 高 ROI 三件套测试 +// Trae 加的 reminders / cost-breakdown / search / compare +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +vi.mock('../src/db.js', () => ({ + db: () => ({ + all: vi.fn(async (sql, params = []) => { + if (/notification_prefs/.test(sql)) { + return [ + { key_name: 'refuel_remind_days', days: 30, enabled: 1 }, + { key_name: 'maintenance_remind_days', days: 180, enabled: 1 }, + { key_name: 'wash_remind_days', days: 14, enabled: 1 }, + ]; + } + if (/FROM vehicles v[\s\S]*LEFT JOIN refuel_records/.test(sql)) { + // 给车辆 1 返 last_date = 60 天前(需要加油) + // 车辆 2 没记录 + return [ + { vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', is_active: 1, last_date: '2026-04-15' }, + { vehicle_id: 2, name: '测试车', plate: null, is_active: 1, last_date: null }, + ]; + } + if (/FROM vehicles v[\s\S]*LEFT JOIN maintenance_records/.test(sql)) { + return [ + { vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', last_date: '2025-12-01' }, // >180 天前 + { vehicle_id: 2, name: '测试车', plate: null, last_date: null }, + ]; + } + if (/FROM vehicles v[\s\S]*LEFT JOIN wash_records/.test(sql)) { + return [ + { vehicle_id: 1, name: '我的 Tiguan', plate: '粤B12345', last_date: '2026-06-01' }, // 18 天前 + { vehicle_id: 2, name: '测试车', plate: null, last_date: null }, + ]; + } + if (/FROM wash_records/.test(sql) && /FROM vehicles/.test(sql) === false) { + // search 用 + if (/grocy_product_id/.test(sql)) return []; + if (/insurance_records/.test(sql)) return []; + if (/maintenance_records/.test(sql)) return []; + if (/charging_records/.test(sql)) return []; + if (/refuel_records/.test(sql)) return []; + if (/wash_records/.test(sql)) return []; + return []; + } + return []; + }), + get: vi.fn(async (sql) => { + if (/SUM.*cost/.test(sql)) return { total: 1000 }; + if (/SUM.*total_cost/.test(sql)) return { total: 5000 }; + if (/SUM.*premium/.test(sql)) return { total: 2000 }; + if (/FROM wash_records WHERE is_deleted = 0/.test(sql) && !/JOIN/.test(sql)) return { total: 1000, cnt: 5 }; + return { total: 0, cnt: 0 }; + }), + run: vi.fn(), + }), +})); + +import extraRouter from '../src/routes/extra.js'; + +describe('GET /api/reminders', () => { + let app; + beforeEach(() => { app = express(); app.use('/api', extraRouter); }); + + it('返 {ok, data} 包装', async () => { + const r = await request(app).get('/api/reminders'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(r.body.data).toBeDefined(); + }); + + it('包含 items + prefs', async () => { + const r = await request(app).get('/api/reminders'); + expect(Array.isArray(r.body.data.items)).toBe(true); + expect(r.body.data.prefs).toHaveProperty('refuel'); + expect(r.body.data.prefs.refuel.days).toBe(30); + }); + + it('加油提醒超过 30 天触发', async () => { + const r = await request(app).get('/api/reminders'); + const refuelReminders = r.body.data.items.filter(it => it.type === 'refuel' && it.days !== null); + expect(refuelReminders.length).toBeGreaterThan(0); + expect(refuelReminders[0].days).toBeGreaterThan(30); + }); +}); + +describe('GET /api/stats/cost-breakdown', () => { + let app; + beforeEach(() => { app = express(); app.use('/api', extraRouter); }); + + it('返 5 个分类 + 百分比合计 100', async () => { + const r = await request(app).get('/api/stats/cost-breakdown'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + const cats = r.body.data.categories; + expect(cats).toHaveLength(5); + const sumPct = cats.reduce((s, c) => s + c.pct, 0); + // 允许 0.1 误差(4 舍 5 入) + expect(Math.abs(sumPct - 100)).toBeLessThan(1); + }); + + it('分类包含 label + key + total + pct + color', async () => { + const r = await request(app).get('/api/stats/cost-breakdown'); + const labels = r.body.data.categories.map(c => c.key); + expect(labels).toEqual(['wash', 'refuel', 'charge', 'maintenance', 'insurance']); + }); +}); + +describe('GET /api/stats/compare', () => { + let app; + beforeEach(() => { app = express(); app.use('/api', extraRouter); }); + + it('返本月/上月/同比/环比', async () => { + const r = await request(app).get('/api/stats/compare'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(r.body.data.by_category).toBeDefined(); + const wash = r.body.data.by_category.wash; + expect(wash).toHaveProperty('this_month'); + expect(wash).toHaveProperty('last_month'); + expect(wash).toHaveProperty('mom_pct'); + expect(wash).toHaveProperty('this_ytd'); + expect(wash).toHaveProperty('last_ytd'); + expect(wash).toHaveProperty('yoy_pct'); + }); + + it('5 个领域都返了', async () => { + const r = await request(app).get('/api/stats/compare'); + expect(Object.keys(r.body.data.by_category)).toEqual( + expect.arrayContaining(['wash', 'refuel', 'charge', 'maintenance', 'insurance']) + ); + }); +}); \ No newline at end of file diff --git a/server/test/routes.notifications.test.js b/server/test/routes.notifications.test.js new file mode 100644 index 0000000..90c0394 --- /dev/null +++ b/server/test/routes.notifications.test.js @@ -0,0 +1,112 @@ +// server/test/routes.notifications.test.js — 站内通知测试 +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +const mocks = vi.hoisted(() => ({ + notifications: [], + nextId: 1, +})); + +vi.mock('../src/db.js', () => ({ + db: () => ({ + all: vi.fn(async (sql) => { + if (/FROM notifications/.test(sql)) { + if (/is_read = 0/.test(sql)) return mocks.notifications.filter(n => !n.is_read); + return mocks.notifications.slice().reverse(); + } + return []; + }), + get: vi.fn(async (sql) => { + if (/COUNT.*FROM notifications WHERE is_read = 0/.test(sql)) { + return { n: mocks.notifications.filter(n => !n.is_read).length }; + } + return null; + }), + run: vi.fn(async (sql, params = []) => { + if (/INSERT INTO notifications/.test(sql)) { + const [type, title, body, link, severity] = params; + const id = mocks.nextId++; + mocks.notifications.push({ + id, type, title, body, link, severity, is_read: 0, + created_at: '2026-06-20 01:00:00', + }); + return { lastInsertRowid: id }; + } + if (/UPDATE notifications SET is_read = 1 WHERE id/.test(sql)) { + const id = params[0]; + const n = mocks.notifications.find(x => x.id === id); + if (n) n.is_read = 1; + return { changes: 1 }; + } + if (/UPDATE notifications SET is_read = 1/.test(sql)) { + let count = 0; + for (const n of mocks.notifications) { + if (!n.is_read) { n.is_read = 1; count++; } + } + return { changes: count }; + } + return { changes: 0 }; + }), + }), +})); + +import notifRouter from '../src/routes/notifications.js'; + +describe('Notifications', () => { + let app; + beforeEach(() => { + mocks.notifications = []; + mocks.nextId = 1; + app = express(); + app.use(express.json()); + app.use('/api', notifRouter); + }); + + it('GET /api/notifications 返包装', async () => { + const r = await request(app).get('/api/notifications'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(Array.isArray(r.body.data.items)).toBe(true); + expect(r.body.data.unread).toBe(0); + }); + + it('POST 创建 + id 是 number', async () => { + const r = await request(app).post('/api/notifications').send({ title: '测试', type: 'ocr_done' }); + expect(r.status).toBe(200); + expect(typeof r.body.data.id).toBe('number'); + }); + + it('POST 缺 title 400', async () => { + const r = await request(app).post('/api/notifications').send({}); + expect(r.status).toBe(400); + }); + + it('GET unread=1 只返未读', async () => { + await request(app).post('/api/notifications').send({ title: 'a' }); + await request(app).post('/api/notifications').send({ title: 'b' }); + await request(app).post('/api/notifications/read').send({ all: true }); + const r = await request(app).get('/api/notifications?unread=1'); + expect(r.body.data.items).toHaveLength(0); + expect(r.body.data.unread).toBe(0); + }); + + it('POST /notifications/read 单条标已读', async () => { + await request(app).post('/api/notifications').send({ title: 'a' }); + const list = await request(app).get('/api/notifications'); + const id = list.body.data.items[0].id; + const r = await request(app).post('/api/notifications/read').send({ id }); + expect(r.status).toBe(200); + const list2 = await request(app).get('/api/notifications'); + expect(list2.body.data.unread).toBe(0); + }); + + it('POST /notifications/read {all:true} 清空所有未读', async () => { + await request(app).post('/api/notifications').send({ title: 'a' }); + await request(app).post('/api/notifications').send({ title: 'b' }); + const r = await request(app).post('/api/notifications/read').send({ all: true }); + expect(r.status).toBe(200); + const list = await request(app).get('/api/notifications'); + expect(list.body.data.unread).toBe(0); + }); +}); \ No newline at end of file diff --git a/server/test/routes.stats.test.js b/server/test/routes.stats.test.js new file mode 100644 index 0000000..0359fb4 --- /dev/null +++ b/server/test/routes.stats.test.js @@ -0,0 +1,126 @@ +// server/test/routes.stats.test.js — /api/stats/extra 端点测试 +// Trae v2.7 加的 3 个图表数据接口 +// mock db() 跑纯逻辑测试 +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +const mocks = vi.hoisted(() => { + const _refuels = [ + { refuel_date: '2026-04-15', liters: 50, total_cost: 400, is_deleted: 0, vehicle_id: 1 }, + { refuel_date: '2026-05-20', liters: 60, total_cost: 480, is_deleted: 0, vehicle_id: 1 }, + { refuel_date: '2026-06-10', liters: 45, total_cost: 360, is_deleted: 0, vehicle_id: 1 }, + ]; + const _vehicles = [ + { id: 1, name: '我的 Tiguan', plate: '粤B12345', created_at: '2026-04-01', is_active: 1 }, + ]; + const _washes = [ + { wash_date: '2026-05-10', cost: 100, vehicle_id: 1, is_deleted: 0 }, + { wash_date: '2026-06-01', cost: 150, vehicle_id: 1, is_deleted: 0 }, + ]; + const _maintenances = [ + { maint_date: '2026-06-15', total_cost: 500, vehicle_id: 1, is_deleted: 0 }, + ]; + const _insurances = [ + { start_date: '2026-01-01', end_date: '2027-01-01', premium: 3000, vehicle_id: 1, is_deleted: 0 }, + ]; + const _chargings = []; + return { _refuels, _vehicles, _washes, _maintenances, _insurances, _chargings }; +}); + +vi.mock('../src/db.js', () => ({ + db: () => ({ + all: vi.fn(async (sql) => { + if (/refuel_records/.test(sql) && /substr.*refuel_date/.test(sql)) { + // 油价趋势:按月聚合 + const map = new Map(); + for (const r of mocks._refuels) { + const ym = r.refuel_date.slice(0, 7); + if (!map.has(ym)) map.set(ym, { ym, sum: 0, lit: 0, cnt: 0 }); + const m = map.get(ym); + m.sum += r.total_cost; m.lit += r.liters; m.cnt++; + } + return [...map.values()].map(m => ({ + ym: m.ym, + derived_unit_price: m.lit > 0 ? Math.round(m.sum / m.lit * 1000) / 1000 : null, + cnt: m.cnt, + total_amount: m.sum, + total_liters: Math.round(m.lit * 100) / 100, + })); + } + if (/WITH owned/i.test(sql)) { + // 车辆成本 CTE + return mocks._vehicles.map(v => ({ + id: v.id, name: v.name, plate: v.plate, + days_owned: 100, + lifetime_cost: 400 + 500 + 3000, + annual_cost: 400 * 365 / 100 + 500 * 365 / 100 + 3000 * 365 / 100, + })); + } + if (/mo AS month/.test(sql)) { + const map = new Map(); + for (const w of mocks._washes) { + const ym = w.wash_date.slice(0, 7); + const mo = Number(w.wash_date.slice(5, 7)); + const k = ym + '-' + mo; + if (!map.has(k)) map.set(k, { ym, month: mo, cnt: 0, sum: 0 }); + const m = map.get(k); + m.cnt++; m.sum += w.cost; + } + return [...map.values()].map(m => ({ + ym: m.ym, month: m.month, cnt: m.cnt, + avg_cost: m.cnt > 0 ? Math.round(m.sum / m.cnt * 100) / 100 : null, + total_cost: m.sum, + })); + } + return []; + }), + }), +})); + +import statsRouter from '../src/routes/settings.js'; + +describe('GET /api/stats/extra', () => { + let app; + beforeEach(() => { + app = express(); + app.use('/api', statsRouter); + }); + + it('返 {ok, data} 包装(前端 axios interceptor 才能解包)', async () => { + const r = await request(app).get('/api/stats/extra'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(r.body.data).toBeDefined(); + expect(Array.isArray(r.body.data.fuelTrend)).toBe(true); + expect(Array.isArray(r.body.data.costPerVehicle)).toBe(true); + expect(Array.isArray(r.body.data.washSeason)).toBe(true); + }); + + it('油价趋势字段正确', async () => { + const r = await request(app).get('/api/stats/extra'); + const ft = r.body.data.fuelTrend; + expect(ft.length).toBeGreaterThan(0); + expect(ft[0].ym).toMatch(/^\d{4}-\d{2}$/); + expect(ft[0].cnt).toBeGreaterThan(0); + }); + + it('车辆成本包含必要字段', async () => { + const r = await request(app).get('/api/stats/extra'); + const cpv = r.body.data.costPerVehicle; + expect(cpv.length).toBeGreaterThan(0); + const row = cpv[0]; + expect(row).toHaveProperty('id'); + expect(row).toHaveProperty('days_owned'); + expect(row).toHaveProperty('lifetime_cost'); + expect(row).toHaveProperty('annual_cost'); + }); + + it('洗车季节按月聚合', async () => { + const r = await request(app).get('/api/stats/extra'); + const ws = r.body.data.washSeason; + expect(ws.length).toBeGreaterThan(0); + expect(ws[0].month).toBeGreaterThanOrEqual(1); + expect(ws[0].month).toBeLessThanOrEqual(12); + }); +}); \ No newline at end of file diff --git a/server/test/routes.tags.test.js b/server/test/routes.tags.test.js new file mode 100644 index 0000000..f73492c --- /dev/null +++ b/server/test/routes.tags.test.js @@ -0,0 +1,149 @@ +// server/test/routes.tags.test.js — 标签系统测试 +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +const mocks = vi.hoisted(() => ({ + tags: [], + recordTags: [], + nextTagId: 1, + nextRecordTagId: 1, +})); + +vi.mock('../src/db.js', () => ({ + db: () => ({ + all: vi.fn(async (sql, params = []) => { + if (/SELECT.*FROM tags/.test(sql)) { + if (/use_count/.test(sql)) { + return mocks.tags.map(t => ({ + ...t, + use_count: mocks.recordTags.filter(rt => rt.tag_id === t.id).length, + })); + } + return mocks.tags; + } + if (/FROM record_tags rt JOIN tags/.test(sql)) { + const [rtype, rid] = params; + return mocks.recordTags + .filter(rt => rt.record_type === rtype && rt.record_id === rid) + .map(rt => mocks.tags.find(t => t.id === rt.tag_id)) + .filter(Boolean); + } + return []; + }), + get: vi.fn(async (sql, params = []) => { + if (/SELECT id FROM record_tags WHERE/.test(sql)) { + const [rtype, rid, tid] = params; + return mocks.recordTags.find(rt => + rt.record_type === rtype && rt.record_id === rid && rt.tag_id === tid + ); + } + return null; + }), + run: vi.fn(async (sql, params = []) => { + if (/INSERT INTO tags/.test(sql)) { + const [name, color] = params; + const existing = mocks.tags.find(t => t.name === name); + if (existing) throw new Error('Duplicate entry'); + const id = mocks.nextTagId++; + mocks.tags.push({ id, name, color, created_at: '2026-06-20' }); + return { lastInsertRowid: id }; + } + if (/INSERT INTO record_tags/.test(sql)) { + const [rtype, rid, tid] = params; + const id = mocks.nextRecordTagId++; + mocks.recordTags.push({ id, record_type: rtype, record_id: rid, tag_id: tid, created_at: '2026-06-20' }); + return { lastInsertRowid: id }; + } + if (/DELETE FROM record_tags WHERE id/.test(sql)) { + const id = params[0]; + mocks.recordTags = mocks.recordTags.filter(rt => rt.id !== id); + return { changes: 1 }; + } + if (/DELETE FROM record_tags WHERE tag_id/.test(sql)) { + const tid = Number(params[0]); // 转 number 防类型错配 + mocks.recordTags = mocks.recordTags.filter(rt => rt.tag_id !== tid); + return { changes: mocks.recordTags.length }; + } + if (/DELETE FROM tags WHERE id/.test(sql)) { + const tid = Number(params[0]); + mocks.tags = mocks.tags.filter(t => t.id !== tid); + return { changes: 1 }; + } + return { changes: 0 }; + }), + }), +})); + +import tagsRouter from '../src/routes/tags.js'; + +describe('Tag CRUD', () => { + let app; + beforeEach(() => { + mocks.tags = []; + mocks.recordTags = []; + mocks.nextTagId = 1; + mocks.nextRecordTagId = 1; + app = express(); + app.use(express.json()); + app.use('/api', tagsRouter); + }); + + it('GET /api/tags 返包装', async () => { + const r = await request(app).get('/api/tags'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(Array.isArray(r.body.data.items)).toBe(true); + }); + + it('POST /api/tags 创建 + id 是 number', async () => { + const r = await request(app).post('/api/tags').send({ name: '打蜡', color: '#4DBA9A' }); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(typeof r.body.data.id).toBe('number'); + expect(r.body.data.id).toBeGreaterThan(0); + }); + + it('POST /api/tags 空 name 400', async () => { + const r = await request(app).post('/api/tags').send({ name: '' }); + expect(r.status).toBe(400); + expect(r.body.error.code).toBe('BAD_INPUT'); + }); + + it('POST /api/tags 重名 409', async () => { + await request(app).post('/api/tags').send({ name: '打蜡' }); + const r = await request(app).post('/api/tags').send({ name: '打蜡' }); + expect(r.status).toBe(409); + expect(r.body.error.code).toBe('EXISTS'); + }); + + it('POST /api/record_tags toggle 添加', async () => { + await request(app).post('/api/tags').send({ name: '通勤' }); + const r = await request(app).post('/api/record_tags').send({ record_type: 'wash', record_id: 1, tag_id: 1 }); + expect(r.status).toBe(200); + expect(r.body.data.toggled).toBe('added'); + }); + + it('POST /api/record_tags toggle 移除(重复加同一个)', async () => { + await request(app).post('/api/tags').send({ name: '通勤' }); + await request(app).post('/api/record_tags').send({ record_type: 'wash', record_id: 1, tag_id: 1 }); + const r = await request(app).post('/api/record_tags').send({ record_type: 'wash', record_id: 1, tag_id: 1 }); + expect(r.body.data.toggled).toBe('removed'); + }); + + it('POST /api/record_tags 非法 record_type 400', async () => { + await request(app).post('/api/tags').send({ name: '通勤' }); + const r = await request(app).post('/api/record_tags').send({ record_type: 'invalid', record_id: 1, tag_id: 1 }); + expect(r.status).toBe(400); + expect(r.body.error.code).toBe('BAD_TYPE'); + }); + + it('DELETE /api/tags/:id 级联清 record_tags', async () => { + await request(app).post('/api/tags').send({ name: '通勤' }); + await request(app).post('/api/record_tags').send({ record_type: 'wash', record_id: 1, tag_id: 1 }); + const r = await request(app).delete('/api/tags/1'); + expect(r.status).toBe(200); + expect(r.body.ok).toBe(true); + expect(mocks.recordTags).toHaveLength(0); + }); +}); \ No newline at end of file diff --git a/server/test/routes.vehicles.test.js b/server/test/routes.vehicles.test.js new file mode 100644 index 0000000..8dae337 --- /dev/null +++ b/server/test/routes.vehicles.test.js @@ -0,0 +1,311 @@ +// server/test/routes.vehicles.test.js +// mock 掉 db(),跑 vehicles 路由的纯逻辑测试 +// 使用 vi.hoisted 解决 vi.mock factory 不能引用 file-scope 变量的问题 +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +const mocks = vi.hoisted(() => { + const _tables = { vehicles: [], wash_records: [], operation_logs: [] }; + const _seq = { vehicles: 1, wash_records: 1, operation_logs: 1 }; + let stub = null; + + const makeStub = () => ({ + all: vi.fn(async (sql, params = []) => { + if (/FROM vehicles v/.test(sql) && /wash_records/.test(sql)) { + const whereActive = /v\.is_active = 1/.test(sql); + return _tables.vehicles + .filter((v) => v.is_deleted === 0) + .filter((v) => !whereActive || v.is_active === 1) + .map((v) => { + const washes = _tables.wash_records.filter( + (w) => w.vehicle_id === v.id && w.is_deleted === 0 + ); + return { + ...v, + wash_count: washes.length, + total_cost: washes.reduce((s, w) => s + (w.cost || 0), 0), + last_wash_date: washes.length + ? washes.map((w) => w.wash_date).sort().pop() + : null, + }; + }); + } + if (/COUNT\(\*\) c FROM vehicles/.test(sql) && /is_deleted = 0/.test(sql)) { + if (/is_active = 1/.test(sql)) { + return [{ c: _tables.vehicles.filter((v) => v.is_deleted === 0 && v.is_active === 1).length }]; + } + return [{ c: _tables.vehicles.filter((v) => v.is_deleted === 0).length }]; + } + if (/COUNT\(DISTINCT vehicle_id\) c FROM wash_records/.test(sql)) { + const ids = new Set( + _tables.wash_records + .filter((w) => w.vehicle_id != null && w.is_deleted === 0) + .map((w) => w.vehicle_id) + ); + return [{ c: ids.size }]; + } + if (/SELECT id FROM vehicles WHERE plate = \?/.test(sql)) { + const [plate] = params; + const found = _tables.vehicles.find((v) => v.plate === plate && v.is_deleted === 0); + return found ? [{ id: found.id }] : []; + } + if (/FROM vehicles v[\s\S]+WHERE v\.id = \? AND v\.is_deleted = 0/.test(sql)) { + const [id] = params; + const v = _tables.vehicles.find((x) => x.id === Number(id) && x.is_deleted === 0); + if (!v) return []; + const washes = _tables.wash_records.filter( + (w) => w.vehicle_id === v.id && w.is_deleted === 0 + ); + return [ + { + ...v, + wash_count: washes.length, + total_cost: washes.reduce((s, w) => s + (w.cost || 0), 0), + last_wash_date: washes.length + ? washes.map((w) => w.wash_date).sort().pop() + : null, + }, + ]; + } + if (/SELECT \* FROM vehicles WHERE id = \? AND is_deleted = 0/.test(sql)) { + const [id] = params; + const v = _tables.vehicles.find((x) => x.id === Number(id) && x.is_deleted === 0); + return v ? [v] : []; + } + return []; + }), + get: vi.fn(async (sql, params = []) => { + const rows = await stub.all(sql, params); + return rows[0] || null; + }), + run: vi.fn(async (sql, params = []) => { + if (/INSERT INTO vehicles/.test(sql)) { + const id = _seq.vehicles++; + const [name, plate, type, color, notes, is_active, sort_order, powertrain] = params; + _tables.vehicles.push({ + id, + name, + plate: plate || null, + type: type || 'car', + color: color || null, + notes: notes || null, + is_active: is_active ? 1 : 0, + sort_order: sort_order || 0, + powertrain: powertrain || 'ice', + is_deleted: 0, + created_at: new Date().toISOString(), + }); + return { lastInsertRowid: id }; + } + if (/UPDATE vehicles SET is_deleted = 1, updated_at = NOW\(\) WHERE id = \?/.test(sql)) { + const [id] = params; + const v = _tables.vehicles.find((x) => x.id === Number(id)); + if (v) v.is_deleted = 1; + return { changes: v ? 1 : 0 }; + } + if (/INSERT INTO operation_logs/.test(sql)) { + _seq.operation_logs++; + return { lastInsertRowid: _seq.operation_logs }; + } + return { changes: 0 }; + }), + }); + + const reset = () => { + _tables.vehicles = []; + _tables.wash_records = []; + _tables.operation_logs = []; + _seq.vehicles = 1; + _seq.wash_records = 1; + _seq.operation_logs = 1; + }; + + return { makeStub, reset, setStub: (s) => (stub = s), getStub: () => stub, _tables, _seq }; +}); + +vi.mock('../src/db.js', () => ({ db: () => mocks.getStub() })); +vi.mock('../src/services/operationLog.js', () => ({ logOperation: vi.fn(async () => {}) })); + +const vehiclesRouter = (await import('../src/routes/vehicles.js')).default; + +function buildApp() { + const app = express(); + app.use(express.json()); + app.use('/api', vehiclesRouter); + return app; +} + +beforeEach(() => { + mocks.reset(); + mocks.setStub(mocks.makeStub()); +}); + +describe('routes/vehicles — 列表', () => { + it('空列表 → []', async () => { + const r = await request(buildApp()).get('/api/vehicles'); + expect(r.status).toBe(200); + expect(r.body).toEqual([]); + }); + + it('过滤软删的车辆', async () => { + mocks._tables.vehicles.push( + { id: 1, name: '车A', plate: '粤A111', type: 'car', is_active: 1, sort_order: 0, powertrain: 'ice', is_deleted: 0 }, + { id: 2, name: '车B', plate: '粤A222', type: 'suv', is_active: 1, sort_order: 0, powertrain: 'ice', is_deleted: 1 } + ); + const r = await request(buildApp()).get('/api/vehicles'); + expect(r.status).toBe(200); + expect(r.body).toHaveLength(1); + expect(r.body[0].name).toBe('车A'); + }); + + it('返回字段包含 powertrain_label', async () => { + mocks._tables.vehicles.push({ + id: 1, name: '车A', type: 'ev', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ev', + }); + const r = await request(buildApp()).get('/api/vehicles'); + expect(r.body[0].powertrain_label).toBe('纯电'); + }); + + it('?active=1 只返回 is_active=1', async () => { + mocks._tables.vehicles.push( + { id: 1, name: '启用', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice' }, + { id: 2, name: '停用', type: 'car', is_active: 0, is_deleted: 0, sort_order: 0, powertrain: 'ice' } + ); + const r = await request(buildApp()).get('/api/vehicles?active=1'); + expect(r.body).toHaveLength(1); + expect(r.body[0].name).toBe('启用'); + }); + + it('wash_count / total_cost 来自 join', async () => { + mocks._tables.vehicles.push({ + id: 1, name: '车A', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice', + }); + mocks._tables.wash_records.push( + { id: 10, vehicle_id: 1, cost: 30, wash_date: '2025-01-01', is_deleted: 0 }, + { id: 11, vehicle_id: 1, cost: 25.5, wash_date: '2025-02-01', is_deleted: 0 } + ); + const r = await request(buildApp()).get('/api/vehicles'); + expect(r.body[0].wash_count).toBe(2); + expect(r.body[0].total_cost).toBe(55.5); + expect(r.body[0].last_wash_date).toBe('2025-02-01'); + }); +}); + +describe('routes/vehicles — 详情', () => { + it('存在 → 返回', async () => { + mocks._tables.vehicles.push({ + id: 1, name: '车A', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice', + }); + const r = await request(buildApp()).get('/api/vehicles/1'); + expect(r.status).toBe(200); + expect(r.body.id).toBe(1); + }); + + it('不存在 → 404', async () => { + const r = await request(buildApp()).get('/api/vehicles/999'); + expect(r.status).toBe(404); + expect(r.body.error.code).toBe('NOT_FOUND'); + }); + + it('软删 → 视为不存在', async () => { + mocks._tables.vehicles.push({ + id: 1, name: 'X', type: 'car', is_active: 1, is_deleted: 1, sort_order: 0, powertrain: 'ice', + }); + const r = await request(buildApp()).get('/api/vehicles/1'); + expect(r.status).toBe(404); + }); +}); + +describe('routes/vehicles — 创建', () => { + it('缺 name → 422', async () => { + const r = await request(buildApp()).post('/api/vehicles').send({ type: 'car' }); + expect(r.status).toBe(422); + expect(r.body.error.code).toBe('VALIDATION'); + expect(r.body.error.errors.name).toBeDefined(); + }); + + it('name 超 64 字 → 422', async () => { + const r = await request(buildApp()).post('/api/vehicles').send({ name: 'x'.repeat(65) }); + expect(r.status).toBe(422); + }); + + it('type 非法 → 422', async () => { + const r = await request(buildApp()).post('/api/vehicles').send({ name: 'X', type: 'rocket' }); + expect(r.status).toBe(422); + }); + + it('powertrain 非法 → 422', async () => { + const r = await request(buildApp()) + .post('/api/vehicles') + .send({ name: 'X', powertrain: 'fusion' }); + expect(r.status).toBe(422); + }); + + it('车牌重复 → 409', async () => { + mocks._tables.vehicles.push({ + id: 1, name: 'A', plate: '粤A111', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice', + }); + const r = await request(buildApp()) + .post('/api/vehicles') + .send({ name: 'B', plate: '粤A111' }); + expect(r.status).toBe(409); + expect(r.body.error.code).toBe('CONFLICT'); + }); + + it('合法 → 200 + id', async () => { + const r = await request(buildApp()) + .post('/api/vehicles') + .send({ name: '我的车', plate: '粤E99999', type: 'suv', powertrain: 'hev' }); + expect(r.status).toBe(200); + expect(r.body.id).toBeDefined(); + expect(mocks._tables.vehicles).toHaveLength(1); + expect(mocks._tables.vehicles[0].powertrain).toBe('hev'); + }); + + it('默认 powertrain = ice', async () => { + const r = await request(buildApp()).post('/api/vehicles').send({ name: 'X' }); + expect(r.status).toBe(200); + expect(mocks._tables.vehicles[0].powertrain).toBe('ice'); + }); +}); + +describe('routes/vehicles — 软删', () => { + it('DELETE → is_deleted=1', async () => { + mocks._tables.vehicles.push({ + id: 1, name: 'X', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice', + }); + const r = await request(buildApp()).delete('/api/vehicles/1'); + expect(r.status).toBe(200); + expect(mocks._tables.vehicles[0].is_deleted).toBe(1); + }); + + it('软删后列表查不到', async () => { + mocks._tables.vehicles.push({ + id: 1, name: 'X', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice', + }); + await request(buildApp()).delete('/api/vehicles/1'); + const r = await request(buildApp()).get('/api/vehicles'); + expect(r.body).toEqual([]); + }); +}); + +describe('routes/vehicles — stats', () => { + it('总览统计', async () => { + mocks._tables.vehicles.push( + { id: 1, name: 'A', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice' }, + { id: 2, name: 'B', type: 'car', is_active: 1, is_deleted: 0, sort_order: 0, powertrain: 'ice' }, + { id: 3, name: 'C', type: 'car', is_active: 0, is_deleted: 0, sort_order: 0, powertrain: 'ice' }, + { id: 4, name: 'D', type: 'car', is_active: 1, is_deleted: 1, sort_order: 0, powertrain: 'ice' } + ); + mocks._tables.wash_records.push( + { id: 10, vehicle_id: 1, is_deleted: 0 }, + { id: 11, vehicle_id: 1, is_deleted: 0 } + ); + const r = await request(buildApp()).get('/api/vehicles/stats'); + expect(r.status).toBe(200); + expect(r.body.total).toBe(3); + expect(r.body.active).toBe(2); + expect(r.body.with_washes).toBe(1); + }); +}); diff --git a/server/test/setup.js b/server/test/setup.js new file mode 100644 index 0000000..d1927b0 --- /dev/null +++ b/server/test/setup.js @@ -0,0 +1,7 @@ +// server/test/setup.js — 全局测试钩子 +// 1. 设置必需的环境变量(避免 .env 真连接 MySQL) +process.env.NODE_ENV = 'test'; +process.env.SESSION_SECRET = 'test-secret-do-not-use-in-prod'; +// 不设置 DB_HOST → db.js 自动用 SQLite 回退 → 走 in-memory? 不,走文件 +// 强制用 :memory: 数据库(每次测试独立) +process.env.DB_PATH = ':memory:'; diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..9c7c978 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,23 @@ +// vitest.config.js — 测试配置 +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['server/**/*.test.js'], + setupFiles: ['server/test/setup.js'], + testTimeout: 10000, + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['server/src/**/*.js'], + exclude: [ + 'server/src/bin/**', // 启动脚本 + 'server/src/db.js', // DB 抽象层(需集成测试) + 'server/src/setup.js', // 安装向导 + 'server/src/index.js', // 入口 + ], + }, + }, +});